mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0177810e2d | |||
| a7f51d460e | |||
| f6ce156acc | |||
| 8260856638 | |||
| a7bdcc9ab9 | |||
| 854901d79a | |||
| d899c6e17a | |||
| 9a0dd36b8d | |||
| 5bdaf478c4 | |||
| 666adb705c | |||
| 203d55d2f7 | |||
| 7b70cdba75 | |||
| 7ff448b325 | |||
| e8a06f51b7 | |||
| 900d41bcef | |||
| f4cbc3270d | |||
| 9a623738cd | |||
| 3b730233bc | |||
| f9497606f8 | |||
| ec52428e06 | |||
| 4426437130 | |||
| 585f0e1104 | |||
| 23ecec65a9 | |||
| 38eb184b4c | |||
| 346e58fd62 | |||
| cf10a1edaa | |||
| 677c407755 | |||
| e3ae714248 | |||
| fb9a2aa4d6 | |||
| 5164822090 | |||
| 6b013bcf60 | |||
| 05a49ceb60 | |||
| 728c5828ba | |||
| 50c94d85a1 | |||
| 0cc188f62c | |||
| 6451671f66 | |||
| ca21a885d5 | |||
| 4af4110f6d | |||
| 51b450c05c | |||
| f9d1f71343 | |||
| 7901e4b996 | |||
| 7b6bf4b78e | |||
| 67d5619ccb | |||
| bf001f96d6 | |||
| ae2b87f03f | |||
| 957cd3502f | |||
| a109542eb8 | |||
| 8c4844b00b | |||
| eec7103910 | |||
| 43aa172829 | |||
| 9b4c483b6d | |||
| b885e206ce | |||
| 07a935f625 | |||
| d13801e976 | |||
| 5716c36b47 | |||
| f11943b956 | |||
| 8b726a9c94 | |||
| ffa3c53847 | |||
| da8833fca4 | |||
| 267feb3c09 | |||
| 3d50af0943 | |||
| 9515019641 | |||
| f0f53dfada | |||
| acef746d26 | |||
| 3356b60e97 | |||
| c988c2b387 | |||
| 3121229707 | |||
| ff85145ee8 | |||
| f61d1a11e0 | |||
| 11ba8979ff | |||
| f6956ccf12 | |||
| 977a5ac8c1 | |||
| 906c3df953 | |||
| 33e5fdc16f | |||
| 77ac17855a | |||
| 65ffcd2884 | |||
| 7ec88bdbfe | |||
| da3fac8cb4 | |||
| 3366113939 | |||
| 9039784f41 | |||
| 7f165e5bbe | |||
| c97111e3ca | |||
| e8746760fa | |||
| 9dbd75e740 | |||
| 85b2fd91b9 | |||
| 6420c218a9 | |||
| ec9402a328 | |||
| d01f06a5c2 | |||
| aee51b3b0d | |||
| afcbccd9dd | |||
| 02448000f9 | |||
| 6af8918aa8 | |||
| 08f83cc438 | |||
| a0468db121 | |||
| 4f23d566ed | |||
| dac619b5f8 | |||
| fdc9cc8074 | |||
| 40b1dabcca | |||
| 94c5af40cf | |||
| 36a3144757 | |||
| 220b61c589 | |||
| 38e93cde3e | |||
| 7e501cdb09 | |||
| da182c162d | |||
| 9a3f7f4af7 | |||
| 5ce1f682f6 | |||
| 5feb08dff2 | |||
| 1e527c1075 | |||
| c6943ae683 | |||
| 8932dacdc4 | |||
| 0be3d850ac | |||
| 57e7cf7057 | |||
| 1005585ccb | |||
| 1188566dbd | |||
| 0058212757 | |||
| dbf8fd3320 | |||
| ce295b079e | |||
| 5eb74bc1dd | |||
| da561ab792 | |||
| 80c9bb4796 | |||
| 22a47d1e59 | |||
| 83883a002c | |||
| 8dd4b71e0e | |||
| 6fe3b1563c | |||
| 44d3825c8e | |||
| d6c5484c3a | |||
| 1fd6056f3f | |||
| 525a0ae52b | |||
| 60210754d9 | |||
| 08dd787083 | |||
| 2c7233812b | |||
| d725e98220 | |||
| 0226ca1e83 | |||
| 1695b6d19e | |||
| c40cc3b236 | |||
| 754959e80d | |||
| 37888fb670 | |||
| 7207398a9e | |||
| 1a7bda209b | |||
| 7e1950b3d2 |
@@ -44,7 +44,7 @@ runs:
|
|||||||
|
|
||||||
- name: Login to builtin registry
|
- name: Login to builtin registry
|
||||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||||
username: ${{ inputs.registry_user }}
|
username: ${{ inputs.registry_user }}
|
||||||
@@ -52,7 +52,7 @@ runs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||||
@@ -61,7 +61,7 @@ runs:
|
|||||||
- name: Extract metadata (tags) for Docker
|
- name: Extract metadata (tags) for Docker
|
||||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=auto
|
latest=auto
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ runs:
|
|||||||
uses: ./.forgejo/actions/rust-toolchain
|
uses: ./.forgejo/actions/rust-toolchain
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||||
@@ -79,7 +79,7 @@ runs:
|
|||||||
|
|
||||||
- name: Login to builtin registry
|
- name: Login to builtin registry
|
||||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||||
username: ${{ inputs.registry_user }}
|
username: ${{ inputs.registry_user }}
|
||||||
@@ -87,7 +87,7 @@ runs:
|
|||||||
|
|
||||||
- name: Extract metadata (labels, annotations) for Docker
|
- name: Extract metadata (labels, annotations) for Docker
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v6
|
||||||
with:
|
with:
|
||||||
images: ${{ inputs.images }}
|
images: ${{ inputs.images }}
|
||||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||||
@@ -152,7 +152,7 @@ runs:
|
|||||||
|
|
||||||
- name: inject cache into docker
|
- name: inject cache into docker
|
||||||
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
||||||
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.3.0
|
uses: https://github.com/reproducible-containers/buildkit-cache-dance@v3.3.2
|
||||||
with:
|
with:
|
||||||
cache-map: |
|
cache-map: |
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -62,10 +62,6 @@ sync:
|
|||||||
target: registry.gitlab.com/continuwuity/continuwuity
|
target: registry.gitlab.com/continuwuity/continuwuity
|
||||||
type: repository
|
type: repository
|
||||||
<<: *tags-main
|
<<: *tags-main
|
||||||
- source: *source
|
|
||||||
target: git.nexy7574.co.uk/mirrored/continuwuity
|
|
||||||
type: repository
|
|
||||||
<<: *tags-releases
|
|
||||||
- source: *source
|
- source: *source
|
||||||
target: ghcr.io/continuwuity/continuwuity
|
target: ghcr.io/continuwuity/continuwuity
|
||||||
type: repository
|
type: repository
|
||||||
|
|||||||
@@ -30,22 +30,22 @@ jobs:
|
|||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
|
echo "distribution=$DISTRIBUTION" >> $GITHUB_OUTPUT
|
||||||
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
|
echo "Debian distribution: $DISTRIBUTION ($VERSION)"
|
||||||
- name: Work around llvm-project#153385
|
#- name: Work around llvm-project#153385
|
||||||
id: llvm-workaround
|
# id: llvm-workaround
|
||||||
run: |
|
# run: |
|
||||||
if [ -f /usr/share/apt/default-sequoia.config ]; then
|
# if [ -f /usr/share/apt/default-sequoia.config ]; then
|
||||||
echo "Applying workaround for llvm-project#153385"
|
# echo "Applying workaround for llvm-project#153385"
|
||||||
mkdir -p /etc/crypto-policies/back-ends/
|
# mkdir -p /etc/crypto-policies/back-ends/
|
||||||
cp /usr/share/apt/default-sequoia.config /etc/crypto-policies/back-ends/apt-sequoia.config
|
# cp /usr/share/apt/default-sequoia.config /etc/crypto-policies/back-ends/apt-sequoia.config
|
||||||
sed -i 's/\(sha1\.second_preimage_resistance = \)2026-02-01/\12026-06-01/' /etc/crypto-policies/back-ends/apt-sequoia.config
|
# sed -i 's/\(sha1\.second_preimage_resistance = \)2026-02-01/\12026-06-01/' /etc/crypto-policies/back-ends/apt-sequoia.config
|
||||||
else
|
# else
|
||||||
echo "No workaround needed for llvm-project#153385"
|
# echo "No workaround needed for llvm-project#153385"
|
||||||
fi
|
# fi
|
||||||
- name: Pick compatible clang version
|
- name: Pick compatible clang version
|
||||||
id: clang-version
|
id: clang-version
|
||||||
run: |
|
run: |
|
||||||
# both latest need to use clang-23, but oldstable and previous can just use clang
|
# both latest need to use clang-23, but oldstable and previous can just use clang
|
||||||
if [[ "${{ matrix.container }}" == "ubuntu-latest" || "${{ matrix.container }}" == "debian-latest" ]]; then
|
if [[ "${{ matrix.container }}" == "ubuntu-latest" ]]; then
|
||||||
echo "Using clang-23 package for ${{ matrix.container }}"
|
echo "Using clang-23 package for ${{ matrix.container }}"
|
||||||
echo "version=clang-23" >> $GITHUB_OUTPUT
|
echo "version=clang-23" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push Docker image by digest
|
- name: Build and push Docker image by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: "docker/Dockerfile"
|
file: "docker/Dockerfile"
|
||||||
@@ -146,7 +146,7 @@ jobs:
|
|||||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||||
- name: Build and push max-perf Docker image by digest
|
- name: Build and push max-perf Docker image by digest
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: "docker/Dockerfile"
|
file: "docker/Dockerfile"
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
name: Renovate
|
name: Renovate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/renovatebot/renovate:42.70.2@sha256:3c2ac1b94fa92ef2fa4d1a0493f2c3ba564454720a32fdbcac2db2846ff1ee47
|
image: ghcr.io/renovatebot/renovate:43.59.4@sha256:f951508dea1e7d71cbe6deca298ab0a05488e7631229304813f630cc06010892
|
||||||
options: --tmpfs /tmp:exec
|
options: --tmpfs /tmp:exec
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ jobs:
|
|||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
token: ${{ secrets.FORGEJO_TOKEN }}
|
token: ${{ secrets.FORGEJO_TOKEN }}
|
||||||
|
|
||||||
- uses: https://github.com/cachix/install-nix-action@4e002c8ec80594ecd40e759629461e26c8abed15 # v31.9.0
|
- uses: https://github.com/cachix/install-nix-action@19effe9fe722874e6d46dd7182e4b8b7a43c4a99 # v31.10.0
|
||||||
with:
|
with:
|
||||||
nix_path: nixpkgs=channel:nixos-unstable
|
nix_path: nixpkgs=channel:nixos-unstable
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
github: [JadedBlueEyes, nexy7574, gingershaped]
|
github: [JadedBlueEyes, nexy7574, gingershaped]
|
||||||
custom:
|
custom:
|
||||||
- https://ko-fi.com/nexy7574
|
- https://timedout.uk/donate.html
|
||||||
- https://ko-fi.com/JadedBlueEyes
|
- https://jade.ellis.link/sponsors
|
||||||
|
|||||||
+14
-2
@@ -1,5 +1,6 @@
|
|||||||
default_install_hook_types:
|
default_install_hook_types:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
|
- pre-push
|
||||||
- commit-msg
|
- commit-msg
|
||||||
default_stages:
|
default_stages:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
@@ -23,7 +24,7 @@ repos:
|
|||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
|
||||||
- repo: https://github.com/crate-ci/typos
|
- repo: https://github.com/crate-ci/typos
|
||||||
rev: v1.43.5
|
rev: v1.44.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: typos
|
- id: typos
|
||||||
- id: typos
|
- id: typos
|
||||||
@@ -31,7 +32,7 @@ repos:
|
|||||||
stages: [commit-msg]
|
stages: [commit-msg]
|
||||||
|
|
||||||
- repo: https://github.com/crate-ci/committed
|
- repo: https://github.com/crate-ci/committed
|
||||||
rev: v1.1.10
|
rev: v1.1.11
|
||||||
hooks:
|
hooks:
|
||||||
- id: committed
|
- id: committed
|
||||||
|
|
||||||
@@ -45,3 +46,14 @@ repos:
|
|||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
stages:
|
stages:
|
||||||
- pre-commit
|
- pre-commit
|
||||||
|
|
||||||
|
- repo: local
|
||||||
|
hooks:
|
||||||
|
- id: cargo-clippy
|
||||||
|
name: cargo clippy
|
||||||
|
entry: cargo clippy -- -D warnings
|
||||||
|
language: system
|
||||||
|
pass_filenames: false
|
||||||
|
types: [rust]
|
||||||
|
stages:
|
||||||
|
- pre-push
|
||||||
|
|||||||
@@ -1,3 +1,32 @@
|
|||||||
|
# Continuwuity 0.5.6 (2026-03-03)
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
- Admin escape commands received over federation will never be executed, as this is never valid in a genuine situation. Contributed by @Jade.
|
||||||
|
- Fixed data amplification vulnerability (CWE-409) that affected configurations with server-side compression enabled (non-default). Contributed by @nex.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts. Contributed by @nex. ([#1399](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1399))
|
||||||
|
- Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade. ([#1428](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1428))
|
||||||
|
- Added [MSC3202](https://github.com/matrix-org/matrix-spec-proposals/pull/3202) Device masquerading (not all of MSC3202). This should fix issues with enabling [MSC4190](https://github.com/matrix-org/matrix-spec-proposals/pull/4190) for some Mautrix bridges. Contributed by @Jade ([#1435](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1435))
|
||||||
|
- Added [MSC3814](https://github.com/matrix-org/matrix-spec-proposals/pull/3814) Dehydrated Devices - you can now decrypt messages sent while all devices were logged out. ([#1436](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1436))
|
||||||
|
- Implement [MSC4143](https://github.com/matrix-org/matrix-spec-proposals/pull/4143) MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim ([#1442](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1442))
|
||||||
|
- Updated `list-backups` admin command to output one backup per line. ([#1394](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1394))
|
||||||
|
- Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken. ([#1434](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1434))
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Removed non-compliant nor functional room alias lookups over federation. Contributed by @nex ([#1393](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1393))
|
||||||
|
- Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160. ([#1418](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1418))
|
||||||
|
- Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers` config option is set correctly. Contributed by @katie. ([#1421](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1421))
|
||||||
|
- Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim. ([#1441](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1441))
|
||||||
|
- Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim ([#1445](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1445))
|
||||||
|
- Fixed a bug that (repairably) caused a room split between continuwuity and non-continuwuity servers when the room had both `m.room.policy` and `org.matrix.msc4284.policy` in its room state. Contributed by @nex ([#1481](https://forgejo.ellis.link/continuwuation/continuwuity/pulls/1481))
|
||||||
|
- Fixed `!admin media delete --mxc <url>` responding with an error message when the media was deleted successfully. Contributed by @lynxize
|
||||||
|
- Fixed spurious 404 media errors in the logs. Contributed by @benbot.
|
||||||
|
- Fixed spurious warn about needed backfill via federation for non-federated rooms. Contributed by @kraem.
|
||||||
|
|
||||||
# Continuwuity v0.5.5 (2026-02-15)
|
# Continuwuity v0.5.5 (2026-02-15)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
+6
-10
@@ -22,22 +22,18 @@ Continuwuity uses pre-commit hooks to enforce various coding standards and catch
|
|||||||
- Validating YAML, JSON, and TOML files
|
- Validating YAML, JSON, and TOML files
|
||||||
- Checking for merge conflicts
|
- Checking for merge conflicts
|
||||||
|
|
||||||
You can run these checks locally by installing [prefligit](https://github.com/j178/prefligit):
|
You can run these checks locally by installing [prek](https://github.com/j178/prek):
|
||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Requires UV: https://docs.astral.sh/uv/getting-started/installation/
|
# Install prek using cargo-binstall
|
||||||
# Mac/linux: curl -LsSf https://astral.sh/uv/install.sh | sh
|
cargo binstall prek
|
||||||
# Windows: powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
|
|
||||||
|
|
||||||
# Install prefligit using cargo-binstall
|
|
||||||
cargo binstall prefligit
|
|
||||||
|
|
||||||
# Install git hooks to run checks automatically
|
# Install git hooks to run checks automatically
|
||||||
prefligit install
|
prek install
|
||||||
|
|
||||||
# Run all checks
|
# Run all checks
|
||||||
prefligit --all-files
|
prek --all-files
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternatively, you can use [pre-commit](https://pre-commit.com/):
|
Alternatively, you can use [pre-commit](https://pre-commit.com/):
|
||||||
@@ -54,7 +50,7 @@ pre-commit install
|
|||||||
pre-commit run --all-files
|
pre-commit run --all-files
|
||||||
```
|
```
|
||||||
|
|
||||||
These same checks are run in CI via the prefligit-checks workflow to ensure consistency. These must pass before the PR is merged.
|
These same checks are run in CI via the prek-checks workflow to ensure consistency. These must pass before the PR is merged.
|
||||||
|
|
||||||
### Running tests locally
|
### Running tests locally
|
||||||
|
|
||||||
|
|||||||
Generated
+811
-227
File diff suppressed because it is too large
Load Diff
+22
-5
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
|||||||
# See also `rust-toolchain.toml`
|
# See also `rust-toolchain.toml`
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||||
version = "0.5.5"
|
version = "0.5.7-alpha.1"
|
||||||
|
|
||||||
[workspace.metadata.crane]
|
[workspace.metadata.crane]
|
||||||
name = "conduwuit"
|
name = "conduwuit"
|
||||||
@@ -99,7 +99,7 @@ features = [
|
|||||||
[workspace.dependencies.axum-extra]
|
[workspace.dependencies.axum-extra]
|
||||||
version = "0.12.0"
|
version = "0.12.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
features = ["typed-header", "tracing"]
|
features = ["typed-header", "tracing", "cookie"]
|
||||||
|
|
||||||
[workspace.dependencies.axum-server]
|
[workspace.dependencies.axum-server]
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -144,6 +144,7 @@ features = [
|
|||||||
"socks",
|
"socks",
|
||||||
"hickory-dns",
|
"hickory-dns",
|
||||||
"http2",
|
"http2",
|
||||||
|
"stream",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.dependencies.serde]
|
[workspace.dependencies.serde]
|
||||||
@@ -158,7 +159,7 @@ features = ["raw_value"]
|
|||||||
|
|
||||||
# Used for appservice registration files
|
# Used for appservice registration files
|
||||||
[workspace.dependencies.serde-saphyr]
|
[workspace.dependencies.serde-saphyr]
|
||||||
version = "0.0.19"
|
version = "0.0.21"
|
||||||
|
|
||||||
# Used to load forbidden room/user regex from config
|
# Used to load forbidden room/user regex from config
|
||||||
[workspace.dependencies.serde_regex]
|
[workspace.dependencies.serde_regex]
|
||||||
@@ -343,7 +344,7 @@ version = "0.1.2"
|
|||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||||
#branch = "conduwuit-changes"
|
#branch = "conduwuit-changes"
|
||||||
rev = "bb12ed288a31a23aa11b10ba0fad22b7f985eb88"
|
rev = "a97b91adcc012ef04991d823b8b5a79c6686ae48"
|
||||||
features = [
|
features = [
|
||||||
"compat",
|
"compat",
|
||||||
"rand",
|
"rand",
|
||||||
@@ -363,6 +364,7 @@ features = [
|
|||||||
"unstable-msc2870",
|
"unstable-msc2870",
|
||||||
"unstable-msc3026",
|
"unstable-msc3026",
|
||||||
"unstable-msc3061",
|
"unstable-msc3061",
|
||||||
|
"unstable-msc3814",
|
||||||
"unstable-msc3245",
|
"unstable-msc3245",
|
||||||
"unstable-msc3266",
|
"unstable-msc3266",
|
||||||
"unstable-msc3381", # polls
|
"unstable-msc3381", # polls
|
||||||
@@ -557,6 +559,19 @@ version = "1.0.1"
|
|||||||
[workspace.dependencies.askama]
|
[workspace.dependencies.askama]
|
||||||
version = "0.15.0"
|
version = "0.15.0"
|
||||||
|
|
||||||
|
[workspace.dependencies.lettre]
|
||||||
|
version = "0.11.19"
|
||||||
|
default-features = false
|
||||||
|
features = ["smtp-transport", "pool", "hostname", "builder", "rustls", "aws-lc-rs", "rustls-native-certs", "tokio1", "tokio1-rustls", "tracing", "serde"]
|
||||||
|
|
||||||
|
[workspace.dependencies.governor]
|
||||||
|
version = "0.10.4"
|
||||||
|
default-features = false
|
||||||
|
features = ["std"]
|
||||||
|
|
||||||
|
[workspace.dependencies.nonzero_ext]
|
||||||
|
version = "0.3.0"
|
||||||
|
|
||||||
#
|
#
|
||||||
# Patches
|
# Patches
|
||||||
#
|
#
|
||||||
@@ -917,7 +932,6 @@ fn_to_numeric_cast_any = "warn"
|
|||||||
format_push_string = "warn"
|
format_push_string = "warn"
|
||||||
get_unwrap = "warn"
|
get_unwrap = "warn"
|
||||||
impl_trait_in_params = "warn"
|
impl_trait_in_params = "warn"
|
||||||
let_underscore_untyped = "warn"
|
|
||||||
lossy_float_literal = "warn"
|
lossy_float_literal = "warn"
|
||||||
mem_forget = "warn"
|
mem_forget = "warn"
|
||||||
missing_assert_message = "warn"
|
missing_assert_message = "warn"
|
||||||
@@ -967,3 +981,6 @@ needless_raw_string_hashes = "allow"
|
|||||||
|
|
||||||
# TODO: Enable this lint & fix all instances
|
# TODO: Enable this lint & fix all instances
|
||||||
collapsible_if = "allow"
|
collapsible_if = "allow"
|
||||||
|
|
||||||
|
# TODO: break these apart
|
||||||
|
cognitive_complexity = "allow"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Added support for associating email addresses with accounts, requiring email addresses for registration, and resetting passwords via email. Contributed by @ginger
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Added support for using an admin command to issue self-service password reset links.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Stopped left rooms from being unconditionally sent on initial sync, hopefully fixing spurious appearances of left rooms in some clients (and making sync faster as a bonus). Contributed by @ginger
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed corrupted appservice registrations causing the server to enter a crash loop. Contributed by @nex.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Re-added support for reading registration tokens from a file. Contributed by @ginger and @benbot.
|
||||||
@@ -1 +0,0 @@
|
|||||||
Removed non-compliant nor functional room alias lookups over federation. Contributed by @nex
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts. Contributed by @nex.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fixed a startup crash in the sender service if we can't detect the number of CPU cores, even if the `sender_workers' config option is set correctly. Contributed by @katie.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Improved the concurrency handling of federation transactions, vastly improving performance and reliability by more accurately handling inbound transactions and reducing the amount of repeated wasted work. Contributed by @nex and @Jade.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Added MSC3202 Device masquerading (not all of MSC3202). This should fix issues with enabling MSC4190 for some Mautrix bridges. Contributed by @Jade
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Removed the `allow_public_room_directory_without_auth` config option. Contributed by @0xnim.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Implement MSC4143 MatrixRTC transport discovery endpoint. Move RTC foci configuration from `[global.well_known]` to a new `[global.matrix_rtc]` section with a `foci` field. Contributed by @0xnim
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Fixed sliding sync v5 list ranges always starting from 0, causing extra rooms to be unnecessarily processed and returned. Contributed by @0xnim
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Prevent removing the admin room alias (`#admins`) to avoid accidentally breaking admin room functionality. Contributed by @0xnim
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Add new config option to allow or disallow search engine indexing through a `<meta ../>` tag. Defaults to blocking indexing (`content="noindex"`). Contributed by @s1lv3r and @ginger.
|
||||||
@@ -1 +0,0 @@
|
|||||||
Updated `list-backups` admin command to output one backup per line.
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
Improved URL preview fetching with a more compatible user agent for sites like YouTube Music. Added `!admin media delete-url-preview <url>` command to clear cached URL previews that were stuck and broken.
|
|
||||||
+15
-3
@@ -15,6 +15,18 @@ disallowed-macros = [
|
|||||||
{ path = "log::trace", reason = "use conduwuit_core::trace" },
|
{ path = "log::trace", reason = "use conduwuit_core::trace" },
|
||||||
]
|
]
|
||||||
|
|
||||||
disallowed-methods = [
|
[[disallowed-methods]]
|
||||||
{ path = "tokio::spawn", reason = "use and pass conduuwit_core::server::Server::runtime() to spawn from" },
|
path = "tokio::spawn"
|
||||||
]
|
reason = "use and pass conduwuit_core::server::Server::runtime() to spawn from"
|
||||||
|
|
||||||
|
[[disallowed-methods]]
|
||||||
|
path = "reqwest::Response::bytes"
|
||||||
|
reason = "bytes is unsafe, use limit_read via the conduwuit_core::utils::LimitReadExt trait instead"
|
||||||
|
|
||||||
|
[[disallowed-methods]]
|
||||||
|
path = "reqwest::Response::text"
|
||||||
|
reason = "text is unsafe, use limit_read_text via the conduwuit_core::utils::LimitReadExt trait instead"
|
||||||
|
|
||||||
|
[[disallowed-methods]]
|
||||||
|
path = "reqwest::Response::json"
|
||||||
|
reason = "json is unsafe, use limit_read_text via the conduwuit_core::utils::LimitReadExt trait instead"
|
||||||
|
|||||||
+66
-7
@@ -25,6 +25,10 @@
|
|||||||
#
|
#
|
||||||
# Also see the `[global.well_known]` config section at the very bottom.
|
# Also see the `[global.well_known]` config section at the very bottom.
|
||||||
#
|
#
|
||||||
|
# If `client` is not set under `[global.well_known]`, the server name will
|
||||||
|
# be used as the base domain for user-facing links (such as password
|
||||||
|
# reset links) created by Continuwuity.
|
||||||
|
#
|
||||||
# Examples of delegation:
|
# Examples of delegation:
|
||||||
# - https://continuwuity.org/.well-known/matrix/server
|
# - https://continuwuity.org/.well-known/matrix/server
|
||||||
# - https://continuwuity.org/.well-known/matrix/client
|
# - https://continuwuity.org/.well-known/matrix/client
|
||||||
@@ -476,18 +480,25 @@
|
|||||||
#yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = false
|
#yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse = false
|
||||||
|
|
||||||
# A static registration token that new users will have to provide when
|
# A static registration token that new users will have to provide when
|
||||||
# creating an account. If unset and `allow_registration` is true,
|
# creating an account. This token does not supersede tokens from other
|
||||||
# you must set
|
# sources, such as the `!admin token` command or the
|
||||||
# `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
# `registration_token_file` configuration option.
|
||||||
# to true to allow open registration without any conditions.
|
|
||||||
#
|
|
||||||
# If you do not want to set a static token, the `!admin token` commands
|
|
||||||
# may also be used to manage registration tokens.
|
|
||||||
#
|
#
|
||||||
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
# example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||||
#
|
#
|
||||||
#registration_token =
|
#registration_token =
|
||||||
|
|
||||||
|
# A path to a file containing static registration tokens, one per line.
|
||||||
|
# Tokens in this file do not supersede tokens from other sources, such as
|
||||||
|
# the `!admin token` command or the `registration_token` configuration
|
||||||
|
# option.
|
||||||
|
#
|
||||||
|
# The file will be read once, when Continuwuity starts. It is not
|
||||||
|
# currently reread when the server configuration is reloaded. If the file
|
||||||
|
# cannot be read, Continuwuity will fail to start.
|
||||||
|
#
|
||||||
|
#registration_token_file =
|
||||||
|
|
||||||
# The public site key for reCaptcha. If this is provided, reCaptcha
|
# The public site key for reCaptcha. If this is provided, reCaptcha
|
||||||
# becomes required during registration. If both captcha *and*
|
# becomes required during registration. If both captcha *and*
|
||||||
# registration token are enabled, both will be required during
|
# registration token are enabled, both will be required during
|
||||||
@@ -1498,6 +1509,11 @@
|
|||||||
#
|
#
|
||||||
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
|
#url_preview_user_agent = "continuwuity/<version> (bot; +https://continuwuity.org)"
|
||||||
|
|
||||||
|
# Determines whether audio and video files will be downloaded for URL
|
||||||
|
# previews.
|
||||||
|
#
|
||||||
|
#url_preview_allow_audio_video = false
|
||||||
|
|
||||||
# List of forbidden room aliases and room IDs as strings of regex
|
# List of forbidden room aliases and room IDs as strings of regex
|
||||||
# patterns.
|
# patterns.
|
||||||
#
|
#
|
||||||
@@ -1783,6 +1799,11 @@
|
|||||||
#
|
#
|
||||||
#config_reload_signal = true
|
#config_reload_signal = true
|
||||||
|
|
||||||
|
# Allow search engines and crawlers to index Continuwuity's built-in
|
||||||
|
# webpages served under the `/_continuwuity/` prefix.
|
||||||
|
#
|
||||||
|
#allow_web_indexing = false
|
||||||
|
|
||||||
[global.tls]
|
[global.tls]
|
||||||
|
|
||||||
# Path to a valid TLS certificate file.
|
# Path to a valid TLS certificate file.
|
||||||
@@ -2016,3 +2037,41 @@
|
|||||||
# web->synapseHTTPAntispam->authorization
|
# web->synapseHTTPAntispam->authorization
|
||||||
#
|
#
|
||||||
#secret =
|
#secret =
|
||||||
|
|
||||||
|
#[global.smtp]
|
||||||
|
|
||||||
|
# A `smtp://`` URI which will be used to connect to a mail server.
|
||||||
|
# Uncommenting the [global.smtp] group and setting this option enables
|
||||||
|
# features which depend on the ability to send email,
|
||||||
|
# such as self-service password resets.
|
||||||
|
#
|
||||||
|
# For most modern mail servers, format the URI like this:
|
||||||
|
# `smtps://username:password@hostname:port`
|
||||||
|
# Note that you will need to URL-encode the username and password. If your
|
||||||
|
# username _is_ your email address, you will need to replace the `@` with
|
||||||
|
# `%40`.
|
||||||
|
#
|
||||||
|
# For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||||
|
# https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||||
|
#
|
||||||
|
#connection_uri =
|
||||||
|
|
||||||
|
# The outgoing address which will be used for sending emails.
|
||||||
|
#
|
||||||
|
# For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||||
|
#
|
||||||
|
# ...or if you don't want to read the RFC, for some reason:
|
||||||
|
# - `Name <address@domain.org>` to specify a sender name
|
||||||
|
# - `address@domain.org` to not use a name
|
||||||
|
#
|
||||||
|
#sender =
|
||||||
|
|
||||||
|
# Whether to require that users provide an email address when they
|
||||||
|
# register.
|
||||||
|
#
|
||||||
|
#require_email_for_registration = false
|
||||||
|
|
||||||
|
# Whether to require that users who register with a registration token
|
||||||
|
# provide an email address.
|
||||||
|
#
|
||||||
|
#require_email_for_token_registration = false
|
||||||
|
|||||||
+9
-4
@@ -10,7 +10,7 @@ RUN rm -f /etc/apt/apt.conf.d/docker-clean
|
|||||||
|
|
||||||
# Match Rustc version as close as possible
|
# Match Rustc version as close as possible
|
||||||
# rustc -vV
|
# rustc -vV
|
||||||
ARG LLVM_VERSION=20
|
ARG LLVM_VERSION=21
|
||||||
# ENV RUSTUP_TOOLCHAIN=${RUST_VERSION}
|
# ENV RUSTUP_TOOLCHAIN=${RUST_VERSION}
|
||||||
|
|
||||||
# Install repo tools
|
# Install repo tools
|
||||||
@@ -48,7 +48,7 @@ EOF
|
|||||||
|
|
||||||
# Developer tool versions
|
# Developer tool versions
|
||||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||||
ENV BINSTALL_VERSION=1.17.5
|
ENV BINSTALL_VERSION=1.17.7
|
||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
@@ -180,6 +180,11 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
export RUSTFLAGS="${RUSTFLAGS}"
|
export RUSTFLAGS="${RUSTFLAGS}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
RUST_PROFILE_DIR="${RUST_PROFILE}"
|
||||||
|
if [[ "${RUST_PROFILE}" == "dev" ]]; then
|
||||||
|
RUST_PROFILE_DIR="debug"
|
||||||
|
fi
|
||||||
|
|
||||||
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
||||||
jq -r ".target_directory"))
|
jq -r ".target_directory"))
|
||||||
mkdir /out/sbin
|
mkdir /out/sbin
|
||||||
@@ -191,8 +196,8 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|||||||
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
||||||
for BINARY in "${BINARIES[@]}"; do
|
for BINARY in "${BINARIES[@]}"; do
|
||||||
echo $BINARY
|
echo $BINARY
|
||||||
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY
|
xx-verify $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY
|
||||||
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE}/$BINARY /out/sbin/$BINARY
|
cp $TARGET_DIR/$(xx-cargo --print-target-triple)/${RUST_PROFILE_DIR}/$BINARY /out/sbin/$BINARY
|
||||||
done
|
done
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
|
|||||||
|
|
||||||
# Developer tool versions
|
# Developer tool versions
|
||||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||||
ENV BINSTALL_VERSION=1.17.5
|
ENV BINSTALL_VERSION=1.17.7
|
||||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||||
ENV CARGO_SBOM_VERSION=0.9.1
|
ENV CARGO_SBOM_VERSION=0.9.1
|
||||||
# renovate: datasource=crate depName=lddtree
|
# renovate: datasource=crate depName=lddtree
|
||||||
|
|||||||
@@ -34,6 +34,11 @@
|
|||||||
"name": "troubleshooting",
|
"name": "troubleshooting",
|
||||||
"label": "Troubleshooting"
|
"label": "Troubleshooting"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "dir",
|
||||||
|
"name": "advanced",
|
||||||
|
"label": "Advanced"
|
||||||
|
},
|
||||||
"security",
|
"security",
|
||||||
{
|
{
|
||||||
"type": "dir-section-header",
|
"type": "dir-section-header",
|
||||||
@@ -64,6 +69,11 @@
|
|||||||
"label": "Configuration Reference",
|
"label": "Configuration Reference",
|
||||||
"name": "/reference/config"
|
"name": "/reference/config"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"label": "Environment Variables",
|
||||||
|
"name": "/reference/environment-variables"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "dir",
|
"type": "dir",
|
||||||
"label": "Admin Command Reference",
|
"label": "Admin Command Reference",
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
{
|
{
|
||||||
"text": "Guide",
|
"text": "Guide",
|
||||||
"link": "/introduction",
|
"link": "/introduction",
|
||||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting)"
|
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting|advanced)"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "Development",
|
"text": "Development",
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"name": "delegation",
|
||||||
|
"label": "Delegation / split-domain"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
# Delegation/split-domain deployment
|
||||||
|
|
||||||
|
Matrix allows clients and servers to discover a homeserver's "true" destination via **`.well-known` delegation**. This is especially useful if you would like to:
|
||||||
|
|
||||||
|
- Serve Continuwuity on a subdomain while having only the base domain for your usernames
|
||||||
|
- Use a port other than `:8448` for server-to-server connections
|
||||||
|
|
||||||
|
This guide will show you how to have `@user:example.com` usernames while serving Continuwuity on `https://matrix.example.com`. It assumes you are using port 443 for both client-to-server connections and server-to-server federation.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
First, ensure you have set up A/AAAA records for `matrix.example.com` and `example.com` pointing to your IP.
|
||||||
|
|
||||||
|
Then, ensure that the `server_name` field matches your intended username suffix. If this is not the case, you **MUST** wipe the database directory and reinstall Continuwuity with your desired `server_name`.
|
||||||
|
|
||||||
|
Then, in the `[global.well_known]` section of your config file, add the following fields:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[global.well_known]
|
||||||
|
|
||||||
|
client = "https://matrix.example.com"
|
||||||
|
|
||||||
|
# port number MUST be specified
|
||||||
|
server = "matrix.example.com:443"
|
||||||
|
|
||||||
|
# (optional) customize your support contacts
|
||||||
|
#support_page =
|
||||||
|
#support_role = "m.role.admin"
|
||||||
|
#support_email =
|
||||||
|
#support_mxid = "@user:example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively if you are using Docker, you can set the `CONTINUWUITY_WELL_KNOWN` environment variable as below:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
continuwuity:
|
||||||
|
...
|
||||||
|
environment:
|
||||||
|
CONTINUWUITY_WELL_KNOWN: |
|
||||||
|
{
|
||||||
|
client=https://matrix.example.com,
|
||||||
|
server=matrix.example.com:443
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Serving with a reverse proxy
|
||||||
|
|
||||||
|
After doing the steps above, Continuwuity will serve these 3 JSON files:
|
||||||
|
|
||||||
|
- `/.well-known/matrix/client`: for Client-Server discovery
|
||||||
|
- `/.well-known/matrix/server`: for Server-Server (federation) discovery
|
||||||
|
- `/.well-known/matrix/support`: admin contact details (strongly recommended to have)
|
||||||
|
|
||||||
|
To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>For Caddy</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
matrix.example.com:443 {
|
||||||
|
reverse_proxy 127.0.0.1:8008
|
||||||
|
}
|
||||||
|
|
||||||
|
example.com:443 {
|
||||||
|
reverse_proxy /.well-known/matrix* 127.0.0.1:8008
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>For Traefik (via Docker labels)</summary>
|
||||||
|
|
||||||
|
```
|
||||||
|
services:
|
||||||
|
continuwuity:
|
||||||
|
...
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.continuwuity.rule=(Host(`matrix.example.com`) || (Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))"
|
||||||
|
- "traefik.http.routers.continuwuity.service=continuwuity"
|
||||||
|
- "traefik.http.services.continuwuity.loadbalancer.server.port=8008"
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
Restart Continuwuity and your reverse proxy. Once that's done, visit these routes and check that the responses match the examples below:
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
|
||||||
|
<summary>`https://example.com/.well-known/matrix/server`</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"m.server": "matrix.example.com:443"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details open>
|
||||||
|
|
||||||
|
<summary>`https://example.com/.well-known/matrix/client`</summary>
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"m.homeserver": {
|
||||||
|
"base_url": "https://matrix.example.com/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Cannot log in with web clients
|
||||||
|
|
||||||
|
Make sure there is an `Access-Control-Allow-Origin: *` header in your `/.well-known/matrix/client` path. While Continuwuity serves this header by default, it may be dropped by reverse proxies or other middlewares.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Using SRV records (not recommended)
|
||||||
|
|
||||||
|
:::warning
|
||||||
|
The following methods are **not recommended** due to increased complexity with little benefits. If you have already set up `.well-known` delegation as above, you can safely skip this part.
|
||||||
|
:::
|
||||||
|
|
||||||
|
The following methods uses SRV DNS records and only work with federation traffic. They are only included for completeness.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Using only SRV records</summary>
|
||||||
|
|
||||||
|
If you can't set up `/.well-known/matrix/server` on :443 for some reason, you can set up a SRV record (via your DNS provider) as below:
|
||||||
|
|
||||||
|
- Service and name: `_matrix-fed._tcp.example.com.`
|
||||||
|
- Priority: `10` (can be any number)
|
||||||
|
- Weight: `10` (can be any number)
|
||||||
|
- Port: `443`
|
||||||
|
- Target: `matrix.example.com.`
|
||||||
|
|
||||||
|
On the target's IP at port 443, you must configure a valid route and cert for your server name, `example.com`. Therefore, this method only works to redirect traffic into the right IP/port combo, and can not delegate your federation to a different domain.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Using SRV records + .well-known</summary>
|
||||||
|
|
||||||
|
You can also set up `/.well-known/matrix/server` with a delegated domain but no ports:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[global.well_known]
|
||||||
|
server = "matrix.example.com"
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, set up a SRV record (via your DNS provider) to announce the port number as below:
|
||||||
|
|
||||||
|
- Service and name: `_matrix-fed._tcp.matrix.example.com.`
|
||||||
|
- Priority: `10` (can be any number)
|
||||||
|
- Weight: `10` (can be any number)
|
||||||
|
- Port: `443`
|
||||||
|
- Target: `matrix.example.com.`
|
||||||
|
|
||||||
|
On the target's IP at port 443, you'll need to provide a valid route and cert for `matrix.example.com`. It provides the same feature as pure `.well-known` delegation, albeit with more parts to handle.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details>
|
||||||
|
|
||||||
|
<summary>Using SRV records as a fallback for .well-known delegation</summary>
|
||||||
|
|
||||||
|
Assume your delegation is as below:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[global.well_known]
|
||||||
|
server = "example.com:443"
|
||||||
|
```
|
||||||
|
|
||||||
|
If your Continuwuity instance becomes temporarily unreachable, other servers will not be able to find your `/.well-known/matrix/server` file, and defaults to using `server_name:8448`. This incorrect cache can persist for a long time, and would hinder re-federation when your server eventually comes back online.
|
||||||
|
|
||||||
|
If you want other servers to default to using port :443 even when it is offline, you could set up a SRV record (via your DNS provider) as follows:
|
||||||
|
|
||||||
|
- Service and name: `_matrix-fed._tcp.example.com.`
|
||||||
|
- Priority: `10` (can be any number)
|
||||||
|
- Weight: `10` (can be any number)
|
||||||
|
- Port: `443`
|
||||||
|
- Target: `example.com.`
|
||||||
|
|
||||||
|
On the target's IP at port 443, you'll need to provide a valid route and cert for `example.com`.
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Documentation
|
||||||
|
|
||||||
|
See the following Matrix Specs for full details on client/server resolution mechanisms:
|
||||||
|
|
||||||
|
- [Server-to-Server resolution](https://spec.matrix.org/v1.17/server-server-api/#resolving-server-names) (see this for more information on SRV records)
|
||||||
|
- [Client-to-Server resolution](https://spec.matrix.org/v1.17/client-server-api/#server-discovery)
|
||||||
|
- [MSC1929: Homeserver Admin Contact and Support page](https://github.com/matrix-org/matrix-spec-proposals/pull/1929)
|
||||||
@@ -13,8 +13,9 @@ settings.
|
|||||||
|
|
||||||
The config file to use can be specified on the commandline when running
|
The config file to use can be specified on the commandline when running
|
||||||
Continuwuity by specifying the `-c`, `--config` flag. Alternatively, you can use
|
Continuwuity by specifying the `-c`, `--config` flag. Alternatively, you can use
|
||||||
the environment variable `CONDUWUIT_CONFIG` to specify the config file to used.
|
the environment variable `CONTINUWUITY_CONFIG` to specify the config file to be
|
||||||
Conduit's environment variables are supported for backwards compatibility.
|
used; see [the section on environment variables](#environment-variables) for
|
||||||
|
more information.
|
||||||
|
|
||||||
## Option commandline flag
|
## Option commandline flag
|
||||||
|
|
||||||
@@ -52,13 +53,15 @@ This commandline argument can be paired with the `--option` flag.
|
|||||||
|
|
||||||
All of the settings that are found in the config file can be specified by using
|
All of the settings that are found in the config file can be specified by using
|
||||||
environment variables. The environment variable names should be all caps and
|
environment variables. The environment variable names should be all caps and
|
||||||
prefixed with `CONDUWUIT_`.
|
prefixed with `CONTINUWUITY_`.
|
||||||
|
|
||||||
For example, if the setting you are changing is `max_request_size`, then the
|
For example, if the setting you are changing is `max_request_size`, then the
|
||||||
environment variable to set is `CONDUWUIT_MAX_REQUEST_SIZE`.
|
environment variable to set is `CONTINUWUITY_MAX_REQUEST_SIZE`.
|
||||||
|
|
||||||
To modify config options not in the `[global]` context such as
|
To modify config options not in the `[global]` context such as
|
||||||
`[global.well_known]`, use the `__` suffix split: `CONDUWUIT_WELL_KNOWN__SERVER`
|
`[global.well_known]`, use the `__` suffix split:
|
||||||
|
`CONTINUWUITY_WELL_KNOWN__SERVER`
|
||||||
|
|
||||||
Conduit's environment variables are supported for backwards compatibility (e.g.
|
Conduit and conduwuit's environment variables are also supported for backwards
|
||||||
|
compatibility, via the `CONDUIT_` and `CONDUWUIT_` prefixes respectively (e.g.
|
||||||
`CONDUIT_SERVER_NAME`).
|
`CONDUIT_SERVER_NAME`).
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ services:
|
|||||||
### then you are ready to go.
|
### then you are ready to go.
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: /sbin/conduwuit
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/continuwuity
|
- db:/var/lib/continuwuity
|
||||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
|
||||||
#- ./continuwuity.toml:/etc/continuwuity.toml
|
#- ./continuwuity.toml:/etc/continuwuity.toml
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
|
|||||||
@@ -16,14 +16,14 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
labels:
|
labels:
|
||||||
caddy: example.com
|
caddy: example.com
|
||||||
caddy.0_respond: /.well-known/matrix/server {"m.server":"matrix.example.com:443"}
|
caddy.reverse_proxy: /.well-known/matrix/* homeserver:6167
|
||||||
caddy.1_respond: /.well-known/matrix/client {"m.server":{"base_url":"https://matrix.example.com"},"m.homeserver":{"base_url":"https://matrix.example.com"},"org.matrix.msc3575.proxy":{"url":"https://matrix.example.com"}}
|
|
||||||
|
|
||||||
homeserver:
|
homeserver:
|
||||||
### If you already built the Continuwuity image with 'docker build' or want to use a registry image,
|
### If you already built the Continuwuity image with 'docker build' or want to use a registry image,
|
||||||
### then you are ready to go.
|
### then you are ready to go.
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: /sbin/conduwuit
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/continuwuity
|
- db:/var/lib/continuwuity
|
||||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
||||||
@@ -42,6 +42,10 @@ services:
|
|||||||
#CONTINUWUITY_LOG: warn,state_res=warn
|
#CONTINUWUITY_LOG: warn,state_res=warn
|
||||||
CONTINUWUITY_ADDRESS: 0.0.0.0
|
CONTINUWUITY_ADDRESS: 0.0.0.0
|
||||||
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
|
#CONTINUWUITY_CONFIG: '/etc/continuwuity.toml' # Uncomment if you mapped config toml above
|
||||||
|
|
||||||
|
# Required for .well-known delegation - edit these according to your chosen domain
|
||||||
|
CONTINUWUITY_WELL_KNOWN__CLIENT: https://matrix.example.com
|
||||||
|
CONTINUWUITY_WELL_KNOWN__SERVER: matrix.example.com:443
|
||||||
networks:
|
networks:
|
||||||
- caddy
|
- caddy
|
||||||
labels:
|
labels:
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
### then you are ready to go.
|
### then you are ready to go.
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: /sbin/conduwuit
|
||||||
volumes:
|
volumes:
|
||||||
- db:/var/lib/continuwuity
|
- db:/var/lib/continuwuity
|
||||||
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
- /etc/resolv.conf:/etc/resolv.conf:ro # Use the host's DNS resolver rather than Docker's.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ services:
|
|||||||
### then you are ready to go.
|
### then you are ready to go.
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
command: /sbin/conduwuit
|
||||||
ports:
|
ports:
|
||||||
- 8448:6167
|
- 8448:6167
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
+159
-122
@@ -2,28 +2,26 @@
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
To run Continuwuity with Docker, you can either build the image yourself or pull it
|
To run Continuwuity with Docker, you can either build the image yourself or pull
|
||||||
from a registry.
|
it from a registry.
|
||||||
|
|
||||||
### Use a registry
|
### Use a registry
|
||||||
|
|
||||||
OCI images for Continuwuity are available in the registries listed below.
|
Available OCI images:
|
||||||
|
|
||||||
| Registry | Image | Notes |
|
| Registry | Image | Notes |
|
||||||
| --------------- | --------------------------------------------------------------- | -----------------------|
|
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
|
| Forgejo Registry | [forgejo.ellis.link/continuwuation/continuwuity:latest](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest) | Latest tagged image. |
|
||||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
|
| Forgejo Registry | [forgejo.ellis.link/continuwuation/continuwuity:main](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main) | Main branch image. |
|
||||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
|
| Forgejo Registry | [forgejo.ellis.link/continuwuation/continuwuity:latest-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/latest-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
|
||||||
| Forgejo Registry| [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
|
| Forgejo Registry | [forgejo.ellis.link/continuwuation/continuwuity:main-maxperf](https://forgejo.ellis.link/continuwuation/-/packages/container/continuwuity/main-maxperf) | [Performance optimised version.](./generic.mdx#performance-optimised-builds) |
|
||||||
|
|
||||||
Use
|
**Example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker image pull $LINK
|
docker image pull forgejo.ellis.link/continuwuation/continuwuity:main-maxperf
|
||||||
```
|
```
|
||||||
|
|
||||||
to pull it to your machine.
|
|
||||||
|
|
||||||
#### Mirrors
|
#### Mirrors
|
||||||
|
|
||||||
Images are mirrored to multiple locations automatically, on a schedule:
|
Images are mirrored to multiple locations automatically, on a schedule:
|
||||||
@@ -33,39 +31,146 @@ Images are mirrored to multiple locations automatically, on a schedule:
|
|||||||
- `registry.gitlab.com/continuwuity/continuwuity`
|
- `registry.gitlab.com/continuwuity/continuwuity`
|
||||||
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
|
- `git.nexy7574.co.uk/mirrored/continuwuity` (releases only, no `main`)
|
||||||
|
|
||||||
### Run
|
### Quick Run
|
||||||
|
|
||||||
When you have the image, you can simply run it with
|
Get a working Continuwuity server with an admin user in four steps:
|
||||||
|
|
||||||
|
#### Prerequisites
|
||||||
|
|
||||||
|
Continuwuity requires HTTPS for Matrix federation. You'll need:
|
||||||
|
|
||||||
|
- A domain name pointing to your server
|
||||||
|
- A reverse proxy with SSL/TLS certificates (Traefik, Caddy, nginx, etc.)
|
||||||
|
|
||||||
|
See [Docker Compose](#docker-compose) for complete examples.
|
||||||
|
|
||||||
|
#### Environment Variables
|
||||||
|
|
||||||
|
- `CONTINUWUITY_SERVER_NAME` - Your Matrix server's domain name
|
||||||
|
- `CONTINUWUITY_DATABASE_PATH` - Where to store your database (must match the
|
||||||
|
volume mount)
|
||||||
|
- `CONTINUWUITY_ADDRESS` - Bind address (use `0.0.0.0` to listen on all
|
||||||
|
interfaces)
|
||||||
|
- `CONTINUWUITY_ALLOW_REGISTRATION` - Set to `false` to disable registration, or
|
||||||
|
use with `CONTINUWUITY_REGISTRATION_TOKEN` to require a token (see
|
||||||
|
[reference](../reference/environment-variables.mdx#registration--user-configuration)
|
||||||
|
for details)
|
||||||
|
|
||||||
|
See the
|
||||||
|
[Environment Variables Reference](../reference/environment-variables.mdx) for
|
||||||
|
more configuration options.
|
||||||
|
|
||||||
|
#### 1. Pull the image
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 8448:6167 \
|
docker pull forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
-v db:/var/lib/continuwuity/ \
|
|
||||||
-e CONTINUWUITY_SERVER_NAME="your.server.name" \
|
|
||||||
-e CONTINUWUITY_ALLOW_REGISTRATION=false \
|
|
||||||
--name continuwuity $LINK
|
|
||||||
```
|
```
|
||||||
|
|
||||||
or you can use [Docker Compose](#docker-compose).
|
#### 2. Start the server with initial admin user
|
||||||
|
|
||||||
The `-d` flag lets the container run in detached mode. You may supply an
|
```bash
|
||||||
optional `continuwuity.toml` config file, the example config can be found
|
docker run -d \
|
||||||
[here](../reference/config.mdx). You can pass in different env vars to
|
-p 6167:6167 \
|
||||||
change config values on the fly. You can even configure Continuwuity completely by
|
-v continuwuity_db:/var/lib/continuwuity \
|
||||||
using env vars. For an overview of possible values, please take a look at the
|
-e CONTINUWUITY_SERVER_NAME="matrix.example.com" \
|
||||||
<a href="/examples/docker-compose.yml" target="_blank">`docker-compose.yml`</a> file.
|
-e CONTINUWUITY_DATABASE_PATH="/var/lib/continuwuity" \
|
||||||
|
-e CONTINUWUITY_ADDRESS="0.0.0.0" \
|
||||||
|
-e CONTINUWUITY_ALLOW_REGISTRATION="false" \
|
||||||
|
--name continuwuity \
|
||||||
|
forgejo.ellis.link/continuwuation/continuwuity:latest \
|
||||||
|
/sbin/conduwuit --execute "users create-user admin"
|
||||||
|
```
|
||||||
|
|
||||||
If you just want to test Continuwuity for a short time, you can use the `--rm`
|
Replace `matrix.example.com` with your actual server name and `admin` with
|
||||||
flag, which cleans up everything related to your container after you stop
|
your preferred username.
|
||||||
it.
|
|
||||||
|
#### 3. Get your admin password
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs continuwuity 2>&1 | grep "Created user"
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll see output like:
|
||||||
|
|
||||||
|
```
|
||||||
|
Created user with user_id: @admin:matrix.example.com and password: `[auto-generated-password]`
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 4. Configure your reverse proxy
|
||||||
|
|
||||||
|
Configure your reverse proxy to forward HTTPS traffic to Continuwuity. See
|
||||||
|
[Docker Compose](#docker-compose) for examples.
|
||||||
|
|
||||||
|
Once configured, log in with any Matrix client using `@admin:matrix.example.com`
|
||||||
|
and the generated password. You'll automatically be invited to the admin room
|
||||||
|
where you can manage your server.
|
||||||
|
|
||||||
### Docker Compose
|
### Docker Compose
|
||||||
|
|
||||||
If the `docker run` command is not suitable for you or your setup, you can also use one
|
Docker Compose is the recommended deployment method. These examples include
|
||||||
of the provided `docker-compose` files.
|
reverse proxy configurations for Matrix federation.
|
||||||
|
|
||||||
Depending on your proxy setup, you can use one of the following files:
|
#### Matrix Federation Requirements
|
||||||
|
|
||||||
### For existing Traefik setup
|
For Matrix federation to work, you need to serve `.well-known/matrix/client` and
|
||||||
|
`.well-known/matrix/server` endpoints. You can achieve this either by:
|
||||||
|
|
||||||
|
1. **Using a well-known service** - The compose files below include an nginx
|
||||||
|
container to serve these files
|
||||||
|
2. **Using Continuwuity's built-in delegation** (easier for Traefik) - Configure
|
||||||
|
delegation files in your config, then proxy `/.well-known/matrix/*` to
|
||||||
|
Continuwuity
|
||||||
|
|
||||||
|
**Traefik example using built-in delegation:**
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
traefik.http.routers.continuwuity.rule: >-
|
||||||
|
(Host(`matrix.example.com`) ||
|
||||||
|
(Host(`example.com`) && PathPrefix(`/.well-known/matrix`)))
|
||||||
|
```
|
||||||
|
|
||||||
|
This routes your Matrix domain and well-known paths to Continuwuity.
|
||||||
|
|
||||||
|
#### Creating Your First Admin User
|
||||||
|
|
||||||
|
Add the `--execute` command to create an admin user on first startup. In your
|
||||||
|
compose file, add under the `continuwuity` service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
continuwuity:
|
||||||
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
|
command: /sbin/conduwuit --execute "users create-user admin"
|
||||||
|
# ... rest of configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
Then retrieve the auto-generated password:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose logs continuwuity | grep "Created user"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Choose Your Reverse Proxy
|
||||||
|
|
||||||
|
Select the compose file that matches your setup:
|
||||||
|
|
||||||
|
:::note DNS Performance
|
||||||
|
Docker's default DNS resolver can cause performance issues with Matrix
|
||||||
|
federation. If you experience slow federation or DNS timeouts, you may need to
|
||||||
|
use your host's DNS resolver instead. Add this volume mount to the
|
||||||
|
`continuwuity` service:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
volumes:
|
||||||
|
- /etc/resolv.conf:/etc/resolv.conf:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
See [Troubleshooting - DNS Issues](../troubleshooting.mdx#potential-dns-issues-when-using-docker)
|
||||||
|
for more details and alternative solutions.
|
||||||
|
:::
|
||||||
|
|
||||||
|
##### For existing Traefik setup
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>docker-compose.for-traefik.yml</summary>
|
<summary>docker-compose.for-traefik.yml</summary>
|
||||||
@@ -76,7 +181,7 @@ Depending on your proxy setup, you can use one of the following files:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### With Traefik included
|
##### With Traefik included
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>docker-compose.with-traefik.yml</summary>
|
<summary>docker-compose.with-traefik.yml</summary>
|
||||||
@@ -87,7 +192,7 @@ Depending on your proxy setup, you can use one of the following files:
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### With Caddy Docker Proxy
|
##### With Caddy Docker Proxy
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>docker-compose.with-caddy.yml</summary>
|
<summary>docker-compose.with-caddy.yml</summary>
|
||||||
@@ -98,9 +203,15 @@ Replace all `example.com` placeholders with your own domain.
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you don't already have a network for Caddy to monitor, create one first:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker network create caddy
|
||||||
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### For other reverse proxies
|
##### For other reverse proxies
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>docker-compose.yml</summary>
|
<summary>docker-compose.yml</summary>
|
||||||
@@ -111,7 +222,7 @@ Replace all `example.com` placeholders with your own domain.
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
### Override file
|
##### Override file for customisation
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>docker-compose.override.yml</summary>
|
<summary>docker-compose.override.yml</summary>
|
||||||
@@ -122,98 +233,24 @@ Replace all `example.com` placeholders with your own domain.
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
When picking the Traefik-related compose file, rename it to
|
#### Starting Your Server
|
||||||
`docker-compose.yml`, and rename the override file to
|
|
||||||
`docker-compose.override.yml`. Edit the latter with the values you want for your
|
|
||||||
server.
|
|
||||||
|
|
||||||
When picking the `caddy-docker-proxy` compose file, it's important to first
|
1. Choose your compose file and rename it to `docker-compose.yml`
|
||||||
create the `caddy` network before spinning up the containers:
|
2. If using the override file, rename it to `docker-compose.override.yml` and
|
||||||
|
edit your values
|
||||||
```bash
|
3. Start the server:
|
||||||
docker network create caddy
|
|
||||||
```
|
|
||||||
|
|
||||||
After that, you can rename it to `docker-compose.yml` and spin up the
|
|
||||||
containers!
|
|
||||||
|
|
||||||
Additional info about deploying Continuwuity can be found [here](generic.mdx).
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
Official Continuwuity images are built using **Docker Buildx** and the Dockerfile found at [`docker/Dockerfile`][dockerfile-path]. This approach uses common Docker tooling and enables efficient multi-platform builds.
|
|
||||||
|
|
||||||
The resulting images are widely compatible with Docker and other container runtimes like Podman or containerd.
|
|
||||||
|
|
||||||
The images *do not contain a shell*. They contain only the Continuwuity binary, required libraries, TLS certificates, and metadata.
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>Click to view the Dockerfile</summary>
|
|
||||||
|
|
||||||
You can also <a href="https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile" target="_blank">view the Dockerfile on Forgejo</a>.
|
|
||||||
|
|
||||||
```dockerfile file="../../docker/Dockerfile"
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
To build an image locally using Docker Buildx, you can typically run a command like:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build for the current platform and load into the local Docker daemon
|
|
||||||
docker buildx build --load --tag continuwuity:latest -f docker/Dockerfile .
|
|
||||||
|
|
||||||
# Example: Build for specific platforms and push to a registry.
|
|
||||||
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
|
|
||||||
|
|
||||||
# Example: Build binary optimised for the current CPU (standard release profile)
|
|
||||||
# docker buildx build --load \
|
|
||||||
# --tag continuwuity:latest \
|
|
||||||
# --build-arg TARGET_CPU=native \
|
|
||||||
# -f docker/Dockerfile .
|
|
||||||
|
|
||||||
# Example: Build maxperf variant (release-max-perf profile with LTO)
|
|
||||||
# Optimised for runtime performance and smaller binary size, but requires longer build time
|
|
||||||
# docker buildx build --load \
|
|
||||||
# --tag continuwuity:latest-maxperf \
|
|
||||||
# --build-arg TARGET_CPU=native \
|
|
||||||
# --build-arg RUST_PROFILE=release-max-perf \
|
|
||||||
# -f docker/Dockerfile .
|
|
||||||
```
|
|
||||||
|
|
||||||
Refer to the Docker Buildx documentation for more advanced build options.
|
|
||||||
|
|
||||||
[dockerfile-path]: https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile
|
|
||||||
|
|
||||||
### Run
|
|
||||||
|
|
||||||
If you have already built the image or want to use one from the registries, you
|
|
||||||
can start the container and everything else in the compose file in detached
|
|
||||||
mode with:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Note:** Don't forget to modify and adjust the compose file to your needs.
|
See the [generic deployment guide](generic.mdx) for more deployment options.
|
||||||
|
|
||||||
### Use Traefik as Proxy
|
### Building Custom Images
|
||||||
|
|
||||||
As a container user, you probably know about Traefik. It is an easy-to-use
|
For information on building your own Continuwuity Docker images, see the
|
||||||
reverse proxy for making containerized apps and services available through the
|
[Building Docker Images](../development/index.mdx#building-docker-images)
|
||||||
web. With the Traefik-related docker-compose files provided above, it is equally easy
|
section in the development documentation.
|
||||||
to deploy and use Continuwuity, with a small caveat. If you have already looked at
|
|
||||||
the files, you should have seen the `well-known` service, which is the
|
|
||||||
small caveat. Traefik is simply a proxy and load balancer and cannot
|
|
||||||
serve any kind of content. For Continuwuity to federate, we need to either
|
|
||||||
expose ports `443` and `8448` or serve two endpoints: `.well-known/matrix/client`
|
|
||||||
and `.well-known/matrix/server`.
|
|
||||||
|
|
||||||
With the service `well-known`, we use a single `nginx` container that serves
|
|
||||||
those two files.
|
|
||||||
|
|
||||||
Alternatively, you can use Continuwuity's built-in delegation file capability. Set up the delegation files in the configuration file, and then proxy paths under `/.well-known/matrix` to continuwuity. For example, the label ``traefik.http.routers.continuwuity.rule=(Host(`matrix.ellis.link`) || (Host(`ellis.link`) && PathPrefix(`/.well-known/matrix`)))`` does this for the domain `ellis.link`.
|
|
||||||
|
|
||||||
## Voice communication
|
## Voice communication
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Continuwuity for FreeBSD
|
# Continuwuity for FreeBSD
|
||||||
|
|
||||||
Continuwuity currently does not provide FreeBSD builds or FreeBSD packaging. However, Continuwuity does build and work on FreeBSD using the system-provided RocksDB.
|
Continuwuity doesn't provide official FreeBSD packages; however, a community-maintained set of packages is available on [Forgejo](https://forgejo.ellis.link/katie/continuwuity-bsd). Note that these are provided as standalone packages and are not part of a FreeBSD package repository (yet), so updates need to be downloaded and installed manually.
|
||||||
|
|
||||||
Contributions to get Continuwuity packaged for FreeBSD are welcome.
|
Please see the installation instructions in that repository. Direct any questions to its issue tracker or to [@katie:kat5.dev](https://matrix.to/#/@katie:kat5.dev).
|
||||||
|
|
||||||
Please join our [Continuwuity BSD](https://matrix.to/#/%23bsd:continuwuity.org) community room.
|
For general BSD support, please join our [Continuwuity BSD](https://matrix.to/#/%23bsd:continuwuity.org) community room.
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ spec:
|
|||||||
- name: continuwuity
|
- name: continuwuity
|
||||||
# use a sha hash <3
|
# use a sha hash <3
|
||||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||||
|
command: ["/sbin/conduwuit"]
|
||||||
imagePullPolicy: IfNotPresent
|
imagePullPolicy: IfNotPresent
|
||||||
ports:
|
ports:
|
||||||
- name: http
|
- name: http
|
||||||
|
|||||||
+133
-70
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
Information about developing the project. If you are only interested in using
|
Information about developing the project. If you are only interested in using
|
||||||
it, you can safely ignore this page. If you plan on contributing, see the
|
it, you can safely ignore this page. If you plan on contributing, see the
|
||||||
[contributor's guide](./contributing.mdx) and [code style guide](./code_style.mdx).
|
[contributor's guide](./contributing.mdx) and
|
||||||
|
[code style guide](./code_style.mdx).
|
||||||
|
|
||||||
## Continuwuity project layout
|
## Continuwuity project layout
|
||||||
|
|
||||||
@@ -12,86 +13,98 @@ members are under `src/`. The workspace definition is at the top level / root
|
|||||||
`Cargo.toml`.
|
`Cargo.toml`.
|
||||||
|
|
||||||
The crate names are generally self-explanatory:
|
The crate names are generally self-explanatory:
|
||||||
|
|
||||||
- `admin` is the admin room
|
- `admin` is the admin room
|
||||||
- `api` is the HTTP API, Matrix C-S and S-S endpoints, etc
|
- `api` is the HTTP API, Matrix C-S and S-S endpoints, etc
|
||||||
- `core` is core Continuwuity functionality like config loading, error definitions,
|
- `core` is core Continuwuity functionality like config loading, error
|
||||||
global utilities, logging infrastructure, etc
|
definitions, global utilities, logging infrastructure, etc
|
||||||
- `database` is RocksDB methods, helpers, RocksDB config, and general database definitions,
|
- `database` is RocksDB methods, helpers, RocksDB config, and general database
|
||||||
utilities, or functions
|
definitions, utilities, or functions
|
||||||
- `macros` are Continuwuity Rust [macros][macros] like general helper macros, logging
|
- `macros` are Continuwuity Rust [macros][macros] like general helper macros,
|
||||||
and error handling macros, and [syn][syn] and [procedural macros][proc-macro]
|
logging and error handling macros, and [syn][syn] and [procedural
|
||||||
used for admin room commands and others
|
macros][proc-macro] used for admin room commands and others
|
||||||
- `main` is the "primary" sub-crate. This is where the `main()` function lives,
|
- `main` is the "primary" sub-crate. This is where the `main()` function lives,
|
||||||
tokio worker and async initialisation, Sentry initialisation, [clap][clap] init,
|
tokio worker and async initialisation, Sentry initialisation, [clap][clap]
|
||||||
and signal handling. If you are adding new [Rust features][features], they *must*
|
init, and signal handling. If you are adding new [Rust features][features],
|
||||||
go here.
|
they _must_ go here.
|
||||||
- `router` is the webserver and request handling bits, using axum, tower, tower-http,
|
- `router` is the webserver and request handling bits, using axum, tower,
|
||||||
hyper, etc, and the [global server state][state] to access `services`.
|
tower-http, hyper, etc, and the [global server state][state] to access
|
||||||
|
`services`.
|
||||||
- `service` is the high-level database definitions and functions for data,
|
- `service` is the high-level database definitions and functions for data,
|
||||||
outbound/sending code, and other business logic such as media fetching.
|
outbound/sending code, and other business logic such as media fetching.
|
||||||
|
|
||||||
It is highly unlikely you will ever need to add a new workspace member, but
|
It is highly unlikely you will ever need to add a new workspace member, but if
|
||||||
if you truly find yourself needing to, we recommend reaching out to us in
|
you truly find yourself needing to, we recommend reaching out to us in the
|
||||||
the Matrix room for discussions about it beforehand.
|
Matrix room for discussions about it beforehand.
|
||||||
|
|
||||||
The primary inspiration for this design was apart of hot reloadable development,
|
The primary inspiration for this design was apart of hot reloadable development,
|
||||||
to support "Continuwuity as a library" where specific parts can simply be swapped out.
|
to support "Continuwuity as a library" where specific parts can simply be
|
||||||
There is evidence Conduit wanted to go this route too as `axum` is technically an
|
swapped out. There is evidence Conduit wanted to go this route too as `axum` is
|
||||||
optional feature in Conduit, and can be compiled without the binary or axum library
|
technically an optional feature in Conduit, and can be compiled without the
|
||||||
for handling inbound web requests; but it was never completed or worked.
|
binary or axum library for handling inbound web requests; but it was never
|
||||||
|
completed or worked.
|
||||||
|
|
||||||
See the Rust documentation on [Workspaces][workspaces] for general questions
|
See the Rust documentation on [Workspaces][workspaces] for general questions and
|
||||||
and information on Cargo workspaces.
|
information on Cargo workspaces.
|
||||||
|
|
||||||
## Adding compile-time [features][features]
|
## Adding compile-time [features][features]
|
||||||
|
|
||||||
If you'd like to add a compile-time feature, you must first define it in
|
If you'd like to add a compile-time feature, you must first define it in the
|
||||||
the `main` workspace crate located in `src/main/Cargo.toml`. The feature must
|
`main` workspace crate located in `src/main/Cargo.toml`. The feature must enable
|
||||||
enable a feature in the other workspace crate(s) you intend to use it in. Then
|
a feature in the other workspace crate(s) you intend to use it in. Then the said
|
||||||
the said workspace crate(s) must define the feature there in its `Cargo.toml`.
|
workspace crate(s) must define the feature there in its `Cargo.toml`.
|
||||||
|
|
||||||
So, if this is adding a feature to the API such as `woof`, you define the feature
|
So, if this is adding a feature to the API such as `woof`, you define the
|
||||||
in the `api` crate's `Cargo.toml` as `woof = []`. The feature definition in `main`'s
|
feature in the `api` crate's `Cargo.toml` as `woof = []`. The feature definition
|
||||||
`Cargo.toml` will be `woof = ["conduwuit-api/woof"]`.
|
in `main`'s `Cargo.toml` will be `woof = ["conduwuit-api/woof"]`.
|
||||||
|
|
||||||
The rationale for this is due to Rust / Cargo not supporting
|
The rationale for this is due to Rust / Cargo not supporting ["workspace level
|
||||||
["workspace level features"][9], we must make a choice of; either scattering
|
features"][9], we must make a choice of; either scattering features all over the
|
||||||
features all over the workspace crates, making it difficult for anyone to add
|
workspace crates, making it difficult for anyone to add or remove default
|
||||||
or remove default features; or define all the features in one central workspace
|
features; or define all the features in one central workspace crate that
|
||||||
crate that propagate down/up to the other workspace crates. It is a Cargo pitfall,
|
propagate down/up to the other workspace crates. It is a Cargo pitfall, and we'd
|
||||||
and we'd like to see better developer UX in Rust's Workspaces.
|
like to see better developer UX in Rust's Workspaces.
|
||||||
|
|
||||||
Additionally, the definition of one single place makes "feature collection" in our
|
Additionally, the definition of one single place makes "feature collection" in
|
||||||
Nix flake a million times easier instead of collecting and deduping them all from
|
our Nix flake a million times easier instead of collecting and deduping them all
|
||||||
searching in all the workspace crates' `Cargo.toml`s. Though we wouldn't need to
|
from searching in all the workspace crates' `Cargo.toml`s. Though we wouldn't
|
||||||
do this if Rust supported workspace-level features to begin with.
|
need to do this if Rust supported workspace-level features to begin with.
|
||||||
|
|
||||||
## List of forked dependencies
|
## List of forked dependencies
|
||||||
|
|
||||||
During Continuwuity (and prior projects) development, we have had to fork some dependencies to support our use-cases.
|
During Continuwuity (and prior projects) development, we have had to fork some
|
||||||
These forks exist for various reasons including features that upstream projects won't accept,
|
dependencies to support our use-cases. These forks exist for various reasons
|
||||||
faster-paced development, Continuwuity-specific usecases, or lack of time to upstream changes.
|
including features that upstream projects won't accept, faster-paced
|
||||||
|
development, Continuwuity-specific usecases, or lack of time to upstream
|
||||||
|
changes.
|
||||||
|
|
||||||
All forked dependencies are maintained under the [continuwuation organization on Forgejo](https://forgejo.ellis.link/continuwuation):
|
All forked dependencies are maintained under the
|
||||||
|
[continuwuation organization on Forgejo](https://forgejo.ellis.link/continuwuation):
|
||||||
|
|
||||||
- [ruwuma][continuwuation-ruwuma] - Fork of [ruma/ruma][ruma] with various performance improvements, more features and better client/server interop
|
- [ruwuma][continuwuation-ruwuma] - Fork of [ruma/ruma][ruma] with various
|
||||||
- [rocksdb][continuwuation-rocksdb] - Fork of [facebook/rocksdb][rocksdb] via [`@zaidoon1`][8] with liburing build fixes and GCC debug build fixes
|
performance improvements, more features and better client/server interop
|
||||||
- [jemallocator][continuwuation-jemallocator] - Fork of [tikv/jemallocator][jemallocator] fixing musl builds, suspicious code,
|
- [rocksdb][continuwuation-rocksdb] - Fork of [facebook/rocksdb][rocksdb] via
|
||||||
and adding support for redzones in Valgrind
|
[`@zaidoon1`][8] with liburing build fixes and GCC debug build fixes
|
||||||
- [rustyline-async][continuwuation-rustyline-async] - Fork of [zyansheep/rustyline-async][rustyline-async] with tab completion callback
|
- [jemallocator][continuwuation-jemallocator] - Fork of
|
||||||
and `CTRL+\` signal quit event for Continuwuity console CLI
|
[tikv/jemallocator][jemallocator] fixing musl builds, suspicious code, and
|
||||||
- [rust-rocksdb][continuwuation-rust-rocksdb] - Fork of [rust-rocksdb/rust-rocksdb][rust-rocksdb] fixing musl build issues,
|
adding support for redzones in Valgrind
|
||||||
removing unnecessary `gtest` include, and using our RocksDB and jemallocator forks
|
- [rustyline-async][continuwuation-rustyline-async] - Fork of
|
||||||
- [tracing][continuwuation-tracing] - Fork of [tokio-rs/tracing][tracing] implementing `Clone` for `EnvFilter` to
|
[zyansheep/rustyline-async][rustyline-async] with tab completion callback and
|
||||||
support dynamically changing tracing environments
|
`CTRL+\` signal quit event for Continuwuity console CLI
|
||||||
|
- [rust-rocksdb][continuwuation-rust-rocksdb] - Fork of
|
||||||
|
[rust-rocksdb/rust-rocksdb][rust-rocksdb] fixing musl build issues, removing
|
||||||
|
unnecessary `gtest` include, and using our RocksDB and jemallocator forks
|
||||||
|
- [tracing][continuwuation-tracing] - Fork of [tokio-rs/tracing][tracing]
|
||||||
|
implementing `Clone` for `EnvFilter` to support dynamically changing tracing
|
||||||
|
environments
|
||||||
|
|
||||||
## Debugging with `tokio-console`
|
## Debugging with `tokio-console`
|
||||||
|
|
||||||
[`tokio-console`][7] can be a useful tool for debugging and profiling. To make a
|
[`tokio-console`][7] can be a useful tool for debugging and profiling. To make a
|
||||||
`tokio-console`-enabled build of Continuwuity, enable the `tokio_console` feature,
|
`tokio-console`-enabled build of Continuwuity, enable the `tokio_console`
|
||||||
disable the default `release_max_log_level` feature, and set the `--cfg
|
feature, disable the default `release_max_log_level` feature, and set the
|
||||||
tokio_unstable` flag to enable experimental tokio APIs. A build might look like
|
`--cfg tokio_unstable` flag to enable experimental tokio APIs. A build might
|
||||||
this:
|
look like this:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
|
RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
|
||||||
@@ -100,34 +113,84 @@ RUSTFLAGS="--cfg tokio_unstable" cargo +nightly build \
|
|||||||
--features=systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console
|
--features=systemd,element_hacks,gzip_compression,brotli_compression,zstd_compression,tokio_console
|
||||||
```
|
```
|
||||||
|
|
||||||
You will also need to enable the `tokio_console` config option in Continuwuity when
|
You will also need to enable the `tokio_console` config option in Continuwuity
|
||||||
starting it. This was due to tokio-console causing gradual memory leak/usage
|
when starting it. This was due to tokio-console causing gradual memory
|
||||||
if left enabled.
|
leak/usage if left enabled.
|
||||||
|
|
||||||
## Building Docker Images
|
## Building Docker Images
|
||||||
|
|
||||||
To build a Docker image for Continuwuity, use the standard Docker build command:
|
Official Continuwuity images are built using **Docker Buildx** and the
|
||||||
|
Dockerfile found at [`docker/Dockerfile`][dockerfile-path].
|
||||||
|
|
||||||
|
The images are compatible with Docker and other container runtimes like Podman
|
||||||
|
or containerd.
|
||||||
|
|
||||||
|
The images _do not contain a shell_. They contain only the Continuwuity binary,
|
||||||
|
required libraries, TLS certificates, and metadata.
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click to view the Dockerfile</summary>
|
||||||
|
|
||||||
|
You can also
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="<https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile>"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
view the Dockerfile on Forgejo
|
||||||
|
</a>
|
||||||
|
.
|
||||||
|
|
||||||
|
```dockerfile file="../../docker/Dockerfile"
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -f docker/Dockerfile .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
The image can be cross-compiled for different architectures.
|
</details>
|
||||||
|
|
||||||
|
### Building Locally
|
||||||
|
|
||||||
|
To build an image locally using Docker Buildx:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Build for the current platform and load into the local Docker daemon
|
||||||
|
docker buildx build --load --tag continuwuity:latest -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Example: Build for specific platforms and push to a registry
|
||||||
|
# docker buildx build --platform linux/amd64,linux/arm64 --tag registry.io/org/continuwuity:latest -f docker/Dockerfile . --push
|
||||||
|
|
||||||
|
# Example: Build binary optimised for the current CPU (standard release profile)
|
||||||
|
# docker buildx build --load \
|
||||||
|
# --tag continuwuity:latest \
|
||||||
|
# --build-arg TARGET_CPU=native \
|
||||||
|
# -f docker/Dockerfile .
|
||||||
|
|
||||||
|
# Example: Build maxperf variant (release-max-perf profile with LTO)
|
||||||
|
# docker buildx build --load \
|
||||||
|
# --tag continuwuity:latest-maxperf \
|
||||||
|
# --build-arg TARGET_CPU=native \
|
||||||
|
# --build-arg RUST_PROFILE=release-max-perf \
|
||||||
|
# -f docker/Dockerfile .
|
||||||
|
```
|
||||||
|
|
||||||
|
Refer to the Docker Buildx documentation for more advanced build options.
|
||||||
|
|
||||||
|
[dockerfile-path]:
|
||||||
|
https://forgejo.ellis.link/continuwuation/continuwuation/src/branch/main/docker/Dockerfile
|
||||||
[continuwuation-ruwuma]: https://forgejo.ellis.link/continuwuation/ruwuma
|
[continuwuation-ruwuma]: https://forgejo.ellis.link/continuwuation/ruwuma
|
||||||
[continuwuation-rocksdb]: https://forgejo.ellis.link/continuwuation/rocksdb
|
[continuwuation-rocksdb]: https://forgejo.ellis.link/continuwuation/rocksdb
|
||||||
[continuwuation-jemallocator]: https://forgejo.ellis.link/continuwuation/jemallocator
|
[continuwuation-jemallocator]:
|
||||||
[continuwuation-rustyline-async]: https://forgejo.ellis.link/continuwuation/rustyline-async
|
https://forgejo.ellis.link/continuwuation/jemallocator
|
||||||
[continuwuation-rust-rocksdb]: https://forgejo.ellis.link/continuwuation/rust-rocksdb
|
[continuwuation-rustyline-async]:
|
||||||
|
https://forgejo.ellis.link/continuwuation/rustyline-async
|
||||||
|
[continuwuation-rust-rocksdb]:
|
||||||
|
https://forgejo.ellis.link/continuwuation/rust-rocksdb
|
||||||
[continuwuation-tracing]: https://forgejo.ellis.link/continuwuation/tracing
|
[continuwuation-tracing]: https://forgejo.ellis.link/continuwuation/tracing
|
||||||
|
|
||||||
[ruma]: https://github.com/ruma/ruma/
|
[ruma]: https://github.com/ruma/ruma/
|
||||||
[rocksdb]: https://github.com/facebook/rocksdb/
|
[rocksdb]: https://github.com/facebook/rocksdb/
|
||||||
[jemallocator]: https://github.com/tikv/jemallocator/
|
[jemallocator]: https://github.com/tikv/jemallocator/
|
||||||
[rustyline-async]: https://github.com/zyansheep/rustyline-async/
|
[rustyline-async]: https://github.com/zyansheep/rustyline-async/
|
||||||
[rust-rocksdb]: https://github.com/rust-rocksdb/rust-rocksdb/
|
[rust-rocksdb]: https://github.com/rust-rocksdb/rust-rocksdb/
|
||||||
[tracing]: https://github.com/tokio-rs/tracing/
|
[tracing]: https://github.com/tokio-rs/tracing/
|
||||||
|
|
||||||
[7]: https://docs.rs/tokio-console/latest/tokio_console/
|
[7]: https://docs.rs/tokio-console/latest/tokio_console/
|
||||||
[8]: https://github.com/zaidoon1/
|
[8]: https://github.com/zaidoon1/
|
||||||
[9]: https://github.com/rust-lang/cargo/issues/12162
|
[9]: https://github.com/rust-lang/cargo/issues/12162
|
||||||
|
|||||||
@@ -6,10 +6,10 @@
|
|||||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 9,
|
"id": 10,
|
||||||
"mention_room": false,
|
"mention_room": false,
|
||||||
"date": "2026-02-09",
|
"date": "2026-03-03",
|
||||||
"message": "Yesterday we released [v0.5.4](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.4). Bugfixes, performance improvements and more moderation features! There's also a security fix, so please update as soon as possible. Don't forget to join [our announcements channel](https://matrix.to/#/!jIdNjSM5X-V5JVx2h2kAhUZIIQ08GyzPL55NFZAH1vM/%2489TY9CqRg4-ff1MGo3Ulc5r5X4pakfdzT-99RD8Docc?via=ellis.link&via=explodie.org&via=matrix.org) to get important information sooner <3 "
|
"message": "We've just released [v0.5.6](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.6), which contains a few security improvements - plus significant reliability and performance improvements. Please update as soon as possible. \n\nWe released [v0.5.5](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.5) two weeks ago, but it skipped your admin room straight to [our announcements channel](https://matrix.to/#/!jIdNjSM5X-V5JVx2h2kAhUZIIQ08GyzPL55NFZAH1vM?via=ellis.link&via=gingershaped.computer&via=matrix.org). Make sure you're there to get important information as soon as we announce it! [Our space](https://matrix.to/#/!8cR4g-i9ucof69E4JHNg9LbPVkGprHb3SzcrGBDDJgk?via=continuwuity.org&via=ellis.link&via=matrix.org) has also gained a bunch of new and interesting rooms - be there or be square."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"m.homeserver":{"base_url": "https://matrix.continuwuity.org"},"org.matrix.msc3575.proxy":{"url": "https://matrix.continuwuity.org"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.ellis.link"}]}
|
{"m.homeserver":{"base_url": "https://matrix.continuwuity.org"},"org.matrix.msc4143.rtc_foci":[{"type":"livekit","livekit_service_url":"https://livekit.ellis.link"}]}
|
||||||
|
|||||||
@@ -4,6 +4,11 @@
|
|||||||
"name": "config",
|
"name": "config",
|
||||||
"label": "Configuration"
|
"label": "Configuration"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"name": "environment-variables",
|
||||||
|
"label": "Environment Variables"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"name": "admin",
|
"name": "admin",
|
||||||
|
|||||||
@@ -130,6 +130,10 @@ Trim memory usage
|
|||||||
|
|
||||||
List database files
|
List database files
|
||||||
|
|
||||||
|
## `!admin debug send-test-email`
|
||||||
|
|
||||||
|
Send a test email to the invoking admin's email address
|
||||||
|
|
||||||
## `!admin debug tester`
|
## `!admin debug tester`
|
||||||
|
|
||||||
Developer test stubs
|
Developer test stubs
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ default.
|
|||||||
* Delete all remote and local media from 3 days ago, up until now:
|
* Delete all remote and local media from 3 days ago, up until now:
|
||||||
|
|
||||||
`!admin media delete-past-remote-media -a 3d
|
`!admin media delete-past-remote-media -a 3d
|
||||||
-yes-i-want-to-delete-local-media`
|
--yes-i-want-to-delete-local-media`
|
||||||
|
|
||||||
## `!admin media delete-all-from-user`
|
## `!admin media delete-all-from-user`
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,24 @@ Create a new user
|
|||||||
|
|
||||||
Reset user password
|
Reset user password
|
||||||
|
|
||||||
|
## `!admin users issue-password-reset-link`
|
||||||
|
|
||||||
|
Issue a self-service password reset link for a user
|
||||||
|
|
||||||
|
## `!admin users get-email`
|
||||||
|
|
||||||
|
Get a user's associated email address
|
||||||
|
|
||||||
|
## `!admin users get-user-by-email`
|
||||||
|
|
||||||
|
Get the user with the given email address
|
||||||
|
|
||||||
|
## `!admin users change-email`
|
||||||
|
|
||||||
|
Update or remove a user's email address.
|
||||||
|
|
||||||
|
If `email` is not supplied, the user's existing address will be removed.
|
||||||
|
|
||||||
## `!admin users deactivate`
|
## `!admin users deactivate`
|
||||||
|
|
||||||
Deactivate a user
|
Deactivate a user
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
# Environment Variables
|
||||||
|
|
||||||
|
Continuwuity can be configured entirely through environment variables, making it
|
||||||
|
ideal for containerised deployments and infrastructure-as-code scenarios.
|
||||||
|
|
||||||
|
This is a convenience reference and may not be exhaustive. The
|
||||||
|
[Configuration Reference](./config.mdx) is the primary source for all
|
||||||
|
configuration options.
|
||||||
|
|
||||||
|
## Prefix System
|
||||||
|
|
||||||
|
Continuwuity supports three environment variable prefixes for backwards
|
||||||
|
compatibility:
|
||||||
|
|
||||||
|
- `CONTINUWUITY_*` (current, recommended)
|
||||||
|
- `CONDUWUIT_*` (compatibility)
|
||||||
|
- `CONDUIT_*` (legacy)
|
||||||
|
|
||||||
|
All three prefixes work identically. Use double underscores (`__`) to represent
|
||||||
|
nested configuration sections from the TOML config.
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Simple top-level config
|
||||||
|
CONTINUWUITY_SERVER_NAME="matrix.example.com"
|
||||||
|
CONTINUWUITY_PORT="8008"
|
||||||
|
|
||||||
|
# Nested config sections use double underscores
|
||||||
|
# This maps to [database] section in TOML
|
||||||
|
CONTINUWUITY_DATABASE__PATH="/var/lib/continuwuity"
|
||||||
|
|
||||||
|
# This maps to [tls] section in TOML
|
||||||
|
CONTINUWUITY_TLS__CERTS="/path/to/cert.pem"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration File Override
|
||||||
|
|
||||||
|
You can specify a custom configuration file path:
|
||||||
|
|
||||||
|
- `CONTINUWUITY_CONFIG` - Path to continuwuity.toml (current)
|
||||||
|
- `CONDUWUIT_CONFIG` - Path to config file (compatibility)
|
||||||
|
- `CONDUIT_CONFIG` - Path to config file (legacy)
|
||||||
|
|
||||||
|
## Essential Variables
|
||||||
|
|
||||||
|
These are the minimum variables needed for a working deployment:
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ---------------------------- | ---------------------------------- | ---------------------- |
|
||||||
|
| `CONTINUWUITY_SERVER_NAME` | Your Matrix server's domain name | Required |
|
||||||
|
| `CONTINUWUITY_DATABASE_PATH` | Path to RocksDB database directory | `/var/lib/conduwuit` |
|
||||||
|
| `CONTINUWUITY_ADDRESS` | IP address to bind to | `["127.0.0.1", "::1"]` |
|
||||||
|
| `CONTINUWUITY_PORT` | Port to listen on | `8008` |
|
||||||
|
|
||||||
|
## Network Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| -------------------------------- | ----------------------------------------------- | ---------------------- |
|
||||||
|
| `CONTINUWUITY_ADDRESS` | Bind address (use `0.0.0.0` for all interfaces) | `["127.0.0.1", "::1"]` |
|
||||||
|
| `CONTINUWUITY_PORT` | HTTP port | `8008` |
|
||||||
|
| `CONTINUWUITY_UNIX_SOCKET_PATH` | UNIX socket path (alternative to TCP) | - |
|
||||||
|
| `CONTINUWUITY_UNIX_SOCKET_PERMS` | Socket permissions (octal) | `660` |
|
||||||
|
|
||||||
|
## Database Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ------------------------------------------ | --------------------------- | -------------------- |
|
||||||
|
| `CONTINUWUITY_DATABASE_PATH` | RocksDB data directory | `/var/lib/conduwuit` |
|
||||||
|
| `CONTINUWUITY_DATABASE_BACKUP_PATH` | Backup directory | - |
|
||||||
|
| `CONTINUWUITY_DATABASE_BACKUPS_TO_KEEP` | Number of backups to retain | `1` |
|
||||||
|
| `CONTINUWUITY_DB_CACHE_CAPACITY_MB` | Database read cache (MB) | - |
|
||||||
|
| `CONTINUWUITY_DB_WRITE_BUFFER_CAPACITY_MB` | Write cache (MB) | - |
|
||||||
|
|
||||||
|
## Cache Configuration
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ---------------------------------------- | ------------------------ |
|
||||||
|
| `CONTINUWUITY_CACHE_CAPACITY_MODIFIER` | LRU cache multiplier |
|
||||||
|
| `CONTINUWUITY_PDU_CACHE_CAPACITY` | PDU cache entries |
|
||||||
|
| `CONTINUWUITY_AUTH_CHAIN_CACHE_CAPACITY` | Auth chain cache entries |
|
||||||
|
|
||||||
|
## DNS Configuration
|
||||||
|
|
||||||
|
Configure DNS resolution behaviour for federation and external requests.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ------------------------------------ | ---------------------------- | -------- |
|
||||||
|
| `CONTINUWUITY_DNS_CACHE_ENTRIES` | Max DNS cache entries | `32768` |
|
||||||
|
| `CONTINUWUITY_DNS_MIN_TTL` | Minimum cache TTL (seconds) | `10800` |
|
||||||
|
| `CONTINUWUITY_DNS_MIN_TTL_NXDOMAIN` | NXDOMAIN cache TTL (seconds) | `259200` |
|
||||||
|
| `CONTINUWUITY_DNS_ATTEMPTS` | Retry attempts | - |
|
||||||
|
| `CONTINUWUITY_DNS_TIMEOUT` | Query timeout (seconds) | - |
|
||||||
|
| `CONTINUWUITY_DNS_TCP_FALLBACK` | Allow TCP fallback | - |
|
||||||
|
| `CONTINUWUITY_QUERY_ALL_NAMESERVERS` | Query all nameservers | - |
|
||||||
|
| `CONTINUWUITY_QUERY_OVER_TCP_ONLY` | TCP-only queries | - |
|
||||||
|
|
||||||
|
## Request Configuration
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ------------------------------------ | ----------------------------- |
|
||||||
|
| `CONTINUWUITY_MAX_REQUEST_SIZE` | Max HTTP request size (bytes) |
|
||||||
|
| `CONTINUWUITY_REQUEST_CONN_TIMEOUT` | Connection timeout (seconds) |
|
||||||
|
| `CONTINUWUITY_REQUEST_TIMEOUT` | Overall request timeout |
|
||||||
|
| `CONTINUWUITY_REQUEST_TOTAL_TIMEOUT` | Total timeout |
|
||||||
|
| `CONTINUWUITY_REQUEST_IDLE_TIMEOUT` | Idle timeout |
|
||||||
|
| `CONTINUWUITY_REQUEST_IDLE_PER_HOST` | Idle connections per host |
|
||||||
|
|
||||||
|
## Federation Configuration
|
||||||
|
|
||||||
|
Control how your server federates with other Matrix servers.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ---------------------------------------------- | ----------------------------- | ------- |
|
||||||
|
| `CONTINUWUITY_ALLOW_FEDERATION` | Enable federation | `true` |
|
||||||
|
| `CONTINUWUITY_FEDERATION_LOOPBACK` | Allow loopback federation | - |
|
||||||
|
| `CONTINUWUITY_FEDERATION_CONN_TIMEOUT` | Connection timeout | - |
|
||||||
|
| `CONTINUWUITY_FEDERATION_TIMEOUT` | Request timeout | - |
|
||||||
|
| `CONTINUWUITY_FEDERATION_IDLE_TIMEOUT` | Idle timeout | - |
|
||||||
|
| `CONTINUWUITY_FEDERATION_IDLE_PER_HOST` | Idle connections per host | - |
|
||||||
|
| `CONTINUWUITY_TRUSTED_SERVERS` | JSON array of trusted servers | - |
|
||||||
|
| `CONTINUWUITY_QUERY_TRUSTED_KEY_SERVERS_FIRST` | Query trusted first | - |
|
||||||
|
| `CONTINUWUITY_ONLY_QUERY_TRUSTED_KEY_SERVERS` | Only query trusted | - |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Trust matrix.org for key verification
|
||||||
|
CONTINUWUITY_TRUSTED_SERVERS='["matrix.org"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Registration & User Configuration
|
||||||
|
|
||||||
|
Control user registration and account creation behaviour.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ------------------------------------------ | --------------------- | ------- |
|
||||||
|
| `CONTINUWUITY_ALLOW_REGISTRATION` | Enable registration | `true` |
|
||||||
|
| `CONTINUWUITY_REGISTRATION_TOKEN` | Token requirement | - |
|
||||||
|
| `CONTINUWUITY_SUSPEND_ON_REGISTER` | Suspend new accounts | - |
|
||||||
|
| `CONTINUWUITY_NEW_USER_DISPLAYNAME_SUFFIX` | Display name suffix | 🏳️⚧️ |
|
||||||
|
| `CONTINUWUITY_RECAPTCHA_SITE_KEY` | reCAPTCHA site key | - |
|
||||||
|
| `CONTINUWUITY_RECAPTCHA_PRIVATE_SITE_KEY` | reCAPTCHA private key | - |
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Disable open registration
|
||||||
|
CONTINUWUITY_ALLOW_REGISTRATION="false"
|
||||||
|
|
||||||
|
# Require a registration token
|
||||||
|
CONTINUWUITY_REGISTRATION_TOKEN="your_secret_token_here"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Feature Configuration
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ---------------------------------------------------------- | -------------------------- | ------- |
|
||||||
|
| `CONTINUWUITY_ALLOW_ENCRYPTION` | Enable E2EE | `true` |
|
||||||
|
| `CONTINUWUITY_ALLOW_ROOM_CREATION` | Enable room creation | - |
|
||||||
|
| `CONTINUWUITY_ALLOW_UNSTABLE_ROOM_VERSIONS` | Allow unstable versions | - |
|
||||||
|
| `CONTINUWUITY_DEFAULT_ROOM_VERSION` | Default room version | `v11` |
|
||||||
|
| `CONTINUWUITY_REQUIRE_AUTH_FOR_PROFILE_REQUESTS` | Auth for profiles | - |
|
||||||
|
| `CONTINUWUITY_ALLOW_PUBLIC_ROOM_DIRECTORY_OVER_FEDERATION` | Federate directory | - |
|
||||||
|
| `CONTINUWUITY_ALLOW_PUBLIC_ROOM_DIRECTORY_WITHOUT_AUTH` | Unauth directory | - |
|
||||||
|
| `CONTINUWUITY_ALLOW_DEVICE_NAME_FEDERATION` | Device names in federation | - |
|
||||||
|
|
||||||
|
## TLS Configuration
|
||||||
|
|
||||||
|
Built-in TLS support is primarily for testing. **For production deployments,
|
||||||
|
especially when federating on the internet, use a reverse proxy** (Traefik,
|
||||||
|
Caddy, nginx) to handle TLS termination.
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| --------------------------------- | ------------------------- |
|
||||||
|
| `CONTINUWUITY_TLS__CERTS` | TLS certificate file path |
|
||||||
|
| `CONTINUWUITY_TLS__KEY` | TLS private key path |
|
||||||
|
| `CONTINUWUITY_TLS__DUAL_PROTOCOL` | Support TLS 1.2 + 1.3 |
|
||||||
|
|
||||||
|
**Example (testing only):**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CONTINUWUITY_TLS__CERTS="/etc/letsencrypt/live/matrix.example.com/fullchain.pem"
|
||||||
|
CONTINUWUITY_TLS__KEY="/etc/letsencrypt/live/matrix.example.com/privkey.pem"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Logging Configuration
|
||||||
|
|
||||||
|
Control log output format and verbosity.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ------------------------------ | ------------------ | ------- |
|
||||||
|
| `CONTINUWUITY_LOG` | Log filter level | - |
|
||||||
|
| `CONTINUWUITY_LOG_COLORS` | ANSI colours | `true` |
|
||||||
|
| `CONTINUWUITY_LOG_SPAN_EVENTS` | Log span events | `none` |
|
||||||
|
| `CONTINUWUITY_LOG_THREAD_IDS` | Include thread IDs | - |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Set log level to info
|
||||||
|
CONTINUWUITY_LOG="info"
|
||||||
|
|
||||||
|
# Enable debug logging for specific modules
|
||||||
|
CONTINUWUITY_LOG="warn,continuwuity::api=debug"
|
||||||
|
|
||||||
|
# Disable colours for log aggregation
|
||||||
|
CONTINUWUITY_LOG_COLORS="false"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Observability Configuration
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ---------------------------------------- | --------------------- |
|
||||||
|
| `CONTINUWUITY_ALLOW_OTLP` | Enable OpenTelemetry |
|
||||||
|
| `CONTINUWUITY_OTLP_FILTER` | OTLP filter level |
|
||||||
|
| `CONTINUWUITY_OTLP_PROTOCOL` | Protocol (http/grpc) |
|
||||||
|
| `CONTINUWUITY_TRACING_FLAME` | Enable flame graphs |
|
||||||
|
| `CONTINUWUITY_TRACING_FLAME_FILTER` | Flame graph filter |
|
||||||
|
| `CONTINUWUITY_TRACING_FLAME_OUTPUT_PATH` | Output directory |
|
||||||
|
| `CONTINUWUITY_SENTRY` | Enable Sentry |
|
||||||
|
| `CONTINUWUITY_SENTRY_ENDPOINT` | Sentry DSN |
|
||||||
|
| `CONTINUWUITY_SENTRY_SEND_SERVER_NAME` | Include server name |
|
||||||
|
| `CONTINUWUITY_SENTRY_TRACES_SAMPLE_RATE` | Sample rate (0.0-1.0) |
|
||||||
|
|
||||||
|
## Admin Configuration
|
||||||
|
|
||||||
|
Configure admin users and automated command execution.
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
| ------------------------------------------ | -------------------------------- | ----------------- |
|
||||||
|
| `CONTINUWUITY_ADMINS_LIST` | JSON array of admin user IDs | - |
|
||||||
|
| `CONTINUWUITY_ADMINS_FROM_ROOM` | Derive admins from room | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_ESCAPE_COMMANDS` | Allow `\` prefix in public rooms | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_CONSOLE_AUTOMATIC` | Auto-activate console | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_EXECUTE` | JSON array of startup commands | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_EXECUTE_ERRORS_IGNORE` | Ignore command errors | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_SIGNAL_EXECUTE` | Commands on SIGUSR2 | - |
|
||||||
|
| `CONTINUWUITY_ADMIN_ROOM_TAG` | Admin room tag | `m.server_notice` |
|
||||||
|
|
||||||
|
**Examples:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Create admin user on startup
|
||||||
|
CONTINUWUITY_ADMIN_EXECUTE='["users create-user admin", "users make-user-admin admin"]'
|
||||||
|
|
||||||
|
# Specify admin users directly
|
||||||
|
CONTINUWUITY_ADMINS_LIST='["@alice:example.com", "@bob:example.com"]'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Media & URL Preview Configuration
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ---------------------------------------------------- | ------------------ |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_BOUND_INTERFACE` | Bind interface |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_DOMAIN_CONTAINS_ALLOWLIST` | Domain allowlist |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_DOMAIN_EXPLICIT_ALLOWLIST` | Explicit allowlist |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_DOMAIN_EXPLICIT_DENYLIST` | Explicit denylist |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_MAX_SPIDER_SIZE` | Max fetch size |
|
||||||
|
| `CONTINUWUITY_URL_PREVIEW_TIMEOUT` | Fetch timeout |
|
||||||
|
| `CONTINUWUITY_IP_RANGE_DENYLIST` | IP range denylist |
|
||||||
|
|
||||||
|
## Tokio Runtime Configuration
|
||||||
|
|
||||||
|
These can be set as environment variables or CLI arguments:
|
||||||
|
|
||||||
|
| Variable | Description |
|
||||||
|
| ----------------------------------------- | -------------------------- |
|
||||||
|
| `TOKIO_WORKER_THREADS` | Worker thread count |
|
||||||
|
| `TOKIO_GLOBAL_QUEUE_INTERVAL` | Global queue interval |
|
||||||
|
| `TOKIO_EVENT_INTERVAL` | Event interval |
|
||||||
|
| `TOKIO_MAX_IO_EVENTS_PER_TICK` | Max I/O events per tick |
|
||||||
|
| `CONTINUWUITY_RUNTIME_HISTOGRAM_INTERVAL` | Histogram bucket size (μs) |
|
||||||
|
| `CONTINUWUITY_RUNTIME_HISTOGRAM_BUCKETS` | Bucket count |
|
||||||
|
| `CONTINUWUITY_RUNTIME_WORKER_AFFINITY` | Enable worker affinity |
|
||||||
|
|
||||||
|
## See Also
|
||||||
|
|
||||||
|
- [Configuration Reference](./config.mdx) - Complete TOML configuration
|
||||||
|
documentation
|
||||||
|
- [Admin Commands](./admin/) - Admin command reference
|
||||||
+12
-12
@@ -6,7 +6,7 @@ misconfigurations to cause issues, particularly with networking and permissions.
|
|||||||
Please check that your issues are not due to problems with your Docker setup.
|
Please check that your issues are not due to problems with your Docker setup.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Continuwuity and Matrix issues
|
## Continuwuity issues
|
||||||
|
|
||||||
### Slow joins to rooms
|
### Slow joins to rooms
|
||||||
|
|
||||||
@@ -23,6 +23,16 @@ which is a longstanding bug with synchronizing room joins to clients. In this si
|
|||||||
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
|
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
|
||||||
have a button to clear their cache in the "About" section of their settings.
|
have a button to clear their cache in the "About" section of their settings.
|
||||||
|
|
||||||
|
### Configuration not working as expected
|
||||||
|
|
||||||
|
Sometimes you can make a mistake in your configuration that
|
||||||
|
means things don't get passed to Continuwuity correctly.
|
||||||
|
This is particularly easy to do with environment variables.
|
||||||
|
To check what configuration Continuwuity actually sees, you can
|
||||||
|
use the `!admin server show-config` command in your admin room.
|
||||||
|
Beware that this prints out any secrets in your configuration,
|
||||||
|
so you might want to delete the result afterwards!
|
||||||
|
|
||||||
### Lost access to admin room
|
### Lost access to admin room
|
||||||
|
|
||||||
You can reinvite yourself to the admin room through the following methods:
|
You can reinvite yourself to the admin room through the following methods:
|
||||||
@@ -33,17 +43,7 @@ argument once to invite yourslf to the admin room on startup
|
|||||||
- Or specify the `emergency_password` config option to allow you to temporarily
|
- Or specify the `emergency_password` config option to allow you to temporarily
|
||||||
log into the server account (`@conduit`) from a web client
|
log into the server account (`@conduit`) from a web client
|
||||||
|
|
||||||
## General potential issues
|
## DNS issues
|
||||||
|
|
||||||
### Configuration not working as expected
|
|
||||||
|
|
||||||
Sometimes you can make a mistake in your configuration that
|
|
||||||
means things don't get passed to Continuwuity correctly.
|
|
||||||
This is particularly easy to do with environment variables.
|
|
||||||
To check what configuration Continuwuity actually sees, you can
|
|
||||||
use the `!admin server show-config` command in your admin room.
|
|
||||||
Beware that this prints out any secrets in your configuration,
|
|
||||||
so you might want to delete the result afterwards!
|
|
||||||
|
|
||||||
### Potential DNS issues when using Docker
|
### Potential DNS issues when using Docker
|
||||||
|
|
||||||
|
|||||||
Generated
+27
-27
@@ -3,11 +3,11 @@
|
|||||||
"advisory-db": {
|
"advisory-db": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766324728,
|
"lastModified": 1773786698,
|
||||||
"narHash": "sha256-9C+WyE5U3y5w4WQXxmb0ylRyMMsPyzxielWXSHrcDpE=",
|
"narHash": "sha256-o/J7ZculgwSs1L4H4UFlFZENOXTJzq1X0n71x6oNNvY=",
|
||||||
"owner": "rustsec",
|
"owner": "rustsec",
|
||||||
"repo": "advisory-db",
|
"repo": "advisory-db",
|
||||||
"rev": "c88b88c62bda077be8aa621d4e89d8701e39cb5d",
|
"rev": "99e9de91bb8b61f06ef234ff84e11f758ecd5384",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766194365,
|
"lastModified": 1773189535,
|
||||||
"narHash": "sha256-4AFsUZ0kl6MXSm4BaQgItD0VGlEKR3iq7gIaL7TjBvc=",
|
"narHash": "sha256-E1G/Or6MWeP+L6mpQ0iTFLpzSzlpGrITfU2220Gq47g=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "7d8ec2c71771937ab99790b45e6d9b93d15d9379",
|
"rev": "6fa2fb4cf4a89ba49fc9dd5a3eb6cde99d388269",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766299592,
|
"lastModified": 1773732206,
|
||||||
"narHash": "sha256-7u+q5hexu2eAxL2VjhskHvaUKg+GexmelIR2ve9Nbb4=",
|
"narHash": "sha256-HKibxaUXyWd4Hs+ZUnwo6XslvaFqFqJh66uL9tphU4Q=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "381579dee168d5ced412e2990e9637ecc7cf1c5d",
|
"rev": "0aa13c1b54063a8d8679b28a5cd357ba98f4a56b",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -55,11 +55,11 @@
|
|||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765121682,
|
"lastModified": 1767039857,
|
||||||
"narHash": "sha256-4VBOP18BFeiPkyhy9o4ssBNQEvfvv1kXkasAYd0+rrA=",
|
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||||
"owner": "edolstra",
|
"owner": "edolstra",
|
||||||
"repo": "flake-compat",
|
"repo": "flake-compat",
|
||||||
"rev": "65f23138d8d09a92e30f1e5c87611b23ef451bf3",
|
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765835352,
|
"lastModified": 1772408722,
|
||||||
"narHash": "sha256-XswHlK/Qtjasvhd1nOa1e8MgZ8GS//jBoTqWtrS1Giw=",
|
"narHash": "sha256-rHuJtdcOjK7rAHpHphUb1iCvgkU3GpfvicLMwwnfMT0=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "a34fae9c08a15ad73f295041fec82323541400a9",
|
"rev": "f20dc5d9b8027381c474144ecabc9034d6a839a3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -89,11 +89,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766070988,
|
"lastModified": 1773734432,
|
||||||
"narHash": "sha256-G/WVghka6c4bAzMhTwT2vjLccg/awmHkdKSd2JrycLc=",
|
"narHash": "sha256-IF5ppUWh6gHGHYDbtVUyhwy/i7D261P7fWD1bPefOsw=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "c6245e83d836d0433170a16eb185cefe0572f8b8",
|
"rev": "cda48547b432e8d3b18b4180ba07473762ec8558",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -105,11 +105,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"nixpkgs-lib": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1765674936,
|
"lastModified": 1772328832,
|
||||||
"narHash": "sha256-k00uTP4JNfmejrCLJOwdObYC9jHRrr/5M/a/8L2EIdo=",
|
"narHash": "sha256-e+/T/pmEkLP6BHhYjx6GmwP5ivonQQn0bJdH9YrRB+Q=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs.lib",
|
||||||
"rev": "2075416fcb47225d9b68ac469a5c4801a9c4dd85",
|
"rev": "c185c7a5e5dd8f9add5b2f8ebeff00888b070742",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -132,11 +132,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766253897,
|
"lastModified": 1773697963,
|
||||||
"narHash": "sha256-ChK07B1aOlJ4QzWXpJo+y8IGAxp1V9yQ2YloJ+RgHRw=",
|
"narHash": "sha256-xdKI77It9PM6eNrCcDZsnP4SKulZwk8VkDgBRVMnCb8=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "765b7bdb432b3740f2d564afccfae831d5a972e4",
|
"rev": "2993637174252ff60a582fd1f55b9ab52c39db6d",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -153,11 +153,11 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1766000401,
|
"lastModified": 1773297127,
|
||||||
"narHash": "sha256-+cqN4PJz9y0JQXfAK5J1drd0U05D5fcAGhzhfVrDlsI=",
|
"narHash": "sha256-6E/yhXP7Oy/NbXtf1ktzmU8SdVqJQ09HC/48ebEGBpk=",
|
||||||
"owner": "numtide",
|
"owner": "numtide",
|
||||||
"repo": "treefmt-nix",
|
"repo": "treefmt-nix",
|
||||||
"rev": "42d96e75aa56a3f70cab7e7dc4a32868db28e8fd",
|
"rev": "71b125cd05fbfd78cab3e070b73544abe24c5016",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
|
|
||||||
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
||||||
enableJemalloc = true;
|
enableJemalloc = true;
|
||||||
enableLiburing = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
commonAttrs = (uwulib.build.commonAttrs { }) // {
|
commonAttrs = (uwulib.build.commonAttrs { }) // {
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
commonAttrsArgs.profile = "release";
|
commonAttrsArgs.profile = "release";
|
||||||
rocksdb = self'.packages.rocksdb.override {
|
rocksdb = self'.packages.rocksdb.override {
|
||||||
enableJemalloc = true;
|
enableJemalloc = true;
|
||||||
enableLiburing = true;
|
|
||||||
};
|
};
|
||||||
features = {
|
features = {
|
||||||
enabledFeatures = "all";
|
enabledFeatures = "all";
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
rust-jemalloc-sys-unprefixed,
|
rust-jemalloc-sys-unprefixed,
|
||||||
|
|
||||||
enableJemalloc ? false,
|
enableJemalloc ? false,
|
||||||
enableLiburing ? false,
|
|
||||||
|
|
||||||
fetchFromGitea,
|
fetchFromGitea,
|
||||||
|
|
||||||
@@ -32,7 +31,7 @@ in
|
|||||||
|
|
||||||
# for some reason enableLiburing in nixpkgs rocksdb is default true
|
# for some reason enableLiburing in nixpkgs rocksdb is default true
|
||||||
# which breaks Darwin entirely
|
# which breaks Darwin entirely
|
||||||
enableLiburing = enableLiburing && notDarwin;
|
enableLiburing = notDarwin;
|
||||||
}).overrideAttrs
|
}).overrideAttrs
|
||||||
(old: {
|
(old: {
|
||||||
src = fetchFromGitea {
|
src = fetchFromGitea {
|
||||||
@@ -74,7 +73,7 @@ in
|
|||||||
"USE_RTTI"
|
"USE_RTTI"
|
||||||
]);
|
]);
|
||||||
|
|
||||||
enableLiburing = enableLiburing && notDarwin;
|
enableLiburing = notDarwin;
|
||||||
|
|
||||||
# outputs has "tools" which we don't need or use
|
# outputs has "tools" which we don't need or use
|
||||||
outputs = [ "out" ];
|
outputs = [ "out" ];
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
file = inputs.self + "/rust-toolchain.toml";
|
file = inputs.self + "/rust-toolchain.toml";
|
||||||
|
|
||||||
# See also `rust-toolchain.toml`
|
# See also `rust-toolchain.toml`
|
||||||
sha256 = "sha256-SJwZ8g0zF2WrKDVmHrVG3pD2RGoQeo24MEXnNx5FyuI=";
|
sha256 = "sha256-sqSWJDUxc+zaz1nBWMAJKTAGBuGWP25GCftIOlCEAtA=";
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
uwulib = inputs.self.uwulib.init pkgs;
|
uwulib = inputs.self.uwulib.init pkgs;
|
||||||
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
rocksdbAllFeatures = self'.packages.rocksdb.override {
|
||||||
enableJemalloc = true;
|
enableJemalloc = true;
|
||||||
enableLiburing = true;
|
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+390
-228
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ Environment="CONTINUWUITY_DATABASE_PATH=%S/conduwuit"
|
|||||||
Environment="CONTINUWUITY_CONFIG_RELOAD_SIGNAL=true"
|
Environment="CONTINUWUITY_CONFIG_RELOAD_SIGNAL=true"
|
||||||
|
|
||||||
LoadCredential=conduwuit.toml:/etc/conduwuit/conduwuit.toml
|
LoadCredential=conduwuit.toml:/etc/conduwuit/conduwuit.toml
|
||||||
|
RefreshOnReload=yes
|
||||||
|
|
||||||
ExecStart=/usr/bin/conduwuit --config ${CREDENTIALS_DIRECTORY}/conduwuit.toml
|
ExecStart=/usr/bin/conduwuit --config ${CREDENTIALS_DIRECTORY}/conduwuit.toml
|
||||||
|
|
||||||
|
|||||||
+11
-3
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended", "replacements:all"],
|
"extends": ["config:recommended", "replacements:all", ":semanticCommitTypeAll(chore)"],
|
||||||
"dependencyDashboard": true,
|
"dependencyDashboard": true,
|
||||||
"osvVulnerabilityAlerts": true,
|
"osvVulnerabilityAlerts": true,
|
||||||
"lockFileMaintenance": {
|
"lockFileMaintenance": {
|
||||||
@@ -36,10 +36,18 @@
|
|||||||
},
|
},
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"description": "Batch patch-level Rust dependency updates",
|
"description": "Batch minor and patch Rust dependency updates",
|
||||||
|
"matchManagers": ["cargo"],
|
||||||
|
"matchUpdateTypes": ["minor", "patch"],
|
||||||
|
"matchCurrentVersion": ">=1.0.0",
|
||||||
|
"groupName": "rust-non-major"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Batch patch-level zerover Rust dependency updates",
|
||||||
"matchManagers": ["cargo"],
|
"matchManagers": ["cargo"],
|
||||||
"matchUpdateTypes": ["patch"],
|
"matchUpdateTypes": ["patch"],
|
||||||
"groupName": "rust-patch-updates"
|
"matchCurrentVersion": ">=0.1.0,<1.0.0",
|
||||||
|
"groupName": "rust-zerover-patch-updates"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Limit concurrent Cargo PRs",
|
"description": "Limit concurrent Cargo PRs",
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
[toolchain]
|
[toolchain]
|
||||||
profile = "minimal"
|
profile = "minimal"
|
||||||
channel = "1.90.0"
|
channel = "1.92.0"
|
||||||
components = [
|
components = [
|
||||||
# For rust-analyzer
|
# For rust-analyzer
|
||||||
"rust-src",
|
"rust-src",
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ conduwuit-macros.workspace = true
|
|||||||
conduwuit-service.workspace = true
|
conduwuit-service.workspace = true
|
||||||
const-str.workspace = true
|
const-str.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
|
lettre.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
ruma.workspace = true
|
ruma.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ use conduwuit::{
|
|||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||||
|
use lettre::message::Mailbox;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
||||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId,
|
||||||
@@ -876,3 +877,31 @@ pub(super) async fn trim_memory(&self) -> Result {
|
|||||||
|
|
||||||
writeln!(self, "done").await
|
writeln!(self, "done").await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn send_test_email(&self) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
|
let mailer = self.services.mailer.expect_mailer()?;
|
||||||
|
let Some(sender) = self.sender else {
|
||||||
|
return Err!("No sender user provided in context");
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(email) = self
|
||||||
|
.services
|
||||||
|
.threepid
|
||||||
|
.get_email_for_localpart(sender.localpart())
|
||||||
|
.await
|
||||||
|
else {
|
||||||
|
return Err!("{} has no associated email address", sender);
|
||||||
|
};
|
||||||
|
|
||||||
|
mailer
|
||||||
|
.send(Mailbox::new(None, email.clone()), service::mailer::messages::Test)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.write_str(&format!("Test email successfully sent to {email}"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -225,6 +225,9 @@ pub enum DebugCommand {
|
|||||||
level: Option<i32>,
|
level: Option<i32>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Send a test email to the invoking admin's email address
|
||||||
|
SendTestEmail,
|
||||||
|
|
||||||
/// Developer test stubs
|
/// Developer test stubs
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::fmt::Write;
|
use std::fmt::Write;
|
||||||
|
|
||||||
use conduwuit::{Err, Result};
|
use conduwuit::{Err, Result, utils::response::LimitReadExt};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId};
|
use ruma::{OwnedRoomId, OwnedServerName, OwnedUserId};
|
||||||
|
|
||||||
@@ -55,7 +55,15 @@ pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName
|
|||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let text = response.text().await?;
|
let text = response
|
||||||
|
.limit_read_text(
|
||||||
|
self.services
|
||||||
|
.config
|
||||||
|
.max_request_size
|
||||||
|
.try_into()
|
||||||
|
.expect("u64 fits into usize"),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
if text.is_empty() {
|
if text.is_empty() {
|
||||||
return Err!("Response text/body is empty.");
|
return Err!("Response text/body is empty.");
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ pub enum MediaCommand {
|
|||||||
/// * Delete all remote and local media from 3 days ago, up until now:
|
/// * Delete all remote and local media from 3 days ago, up until now:
|
||||||
///
|
///
|
||||||
/// `!admin media delete-past-remote-media -a 3d
|
/// `!admin media delete-past-remote-media -a 3d
|
||||||
///-yes-i-want-to-delete-local-media`
|
///--yes-i-want-to-delete-local-media`
|
||||||
#[command(verbatim_doc_comment)]
|
#[command(verbatim_doc_comment)]
|
||||||
DeletePastRemoteMedia {
|
DeletePastRemoteMedia {
|
||||||
/// The relative time (e.g. 30s, 5m, 7d) from now within which to
|
/// The relative time (e.g. 30s, 5m, 7d) from now within which to
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use conduwuit::{
|
|||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use lettre::Address;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
|
OwnedEventId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, UserId,
|
||||||
events::{
|
events::{
|
||||||
@@ -296,6 +297,31 @@ pub(super) async fn reset_password(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
|
||||||
|
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
|
||||||
|
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
|
let mut reset_url = self
|
||||||
|
.services
|
||||||
|
.config
|
||||||
|
.get_client_domain()
|
||||||
|
.join(PASSWORD_RESET_PATH)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let user_id = parse_local_user_id(self.services, &username)?;
|
||||||
|
let token = self.services.password_reset.issue_token(user_id).await?;
|
||||||
|
reset_url
|
||||||
|
.query_pairs_mut()
|
||||||
|
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
|
||||||
|
|
||||||
|
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[admin_command]
|
#[admin_command]
|
||||||
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
|
||||||
if self.body.len() < 2
|
if self.body.len() < 2
|
||||||
@@ -1069,3 +1095,106 @@ pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
|||||||
|
|
||||||
self.write_str(&format!("{user_id} can now log in.")).await
|
self.write_str(&format!("{user_id} can now log in.")).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn get_email(&self, user_id: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.threepid
|
||||||
|
.get_email_for_localpart(user_id.localpart())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Some(email) =>
|
||||||
|
self.write_str(&format!("{user_id} has the associated email address {email}."))
|
||||||
|
.await,
|
||||||
|
| None =>
|
||||||
|
self.write_str(&format!("{user_id} has no associated email address."))
|
||||||
|
.await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn get_user_by_email(&self, email: String) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
|
let Ok(email) = Address::try_from(email) else {
|
||||||
|
return Err!("Invalid email address.");
|
||||||
|
};
|
||||||
|
|
||||||
|
match self.services.threepid.get_localpart_for_email(&email).await {
|
||||||
|
| Some(localpart) => {
|
||||||
|
let user_id = OwnedUserId::parse(format!(
|
||||||
|
"@{localpart}:{}",
|
||||||
|
self.services.globals.server_name()
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
self.write_str(&format!("{email} belongs to {user_id}."))
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
| None =>
|
||||||
|
self.write_str(&format!("No user has {email} as their email address."))
|
||||||
|
.await,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[admin_command]
|
||||||
|
pub(super) async fn change_email(&self, user_id: String, email: Option<String>) -> Result {
|
||||||
|
self.bail_restricted()?;
|
||||||
|
|
||||||
|
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||||
|
let Ok(new_email) = email.map(Address::try_from).transpose() else {
|
||||||
|
return Err!("Invalid email address.");
|
||||||
|
};
|
||||||
|
|
||||||
|
if self.services.mailer.mailer().is_none() {
|
||||||
|
warn!("SMTP has not been configured on this server, emails cannot be sent.");
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_email = self
|
||||||
|
.services
|
||||||
|
.threepid
|
||||||
|
.get_email_for_localpart(user_id.localpart())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match (current_email, new_email) {
|
||||||
|
| (None, None) =>
|
||||||
|
self.write_str(&format!(
|
||||||
|
"{user_id} already had no associated email. No changes have been made."
|
||||||
|
))
|
||||||
|
.await,
|
||||||
|
| (current_email, Some(new_email)) => {
|
||||||
|
self.services
|
||||||
|
.threepid
|
||||||
|
.associate_localpart_email(user_id.localpart(), &new_email)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(current_email) = current_email {
|
||||||
|
self.write_str(&format!(
|
||||||
|
"The associated email of {user_id} has been changed from {current_email} to \
|
||||||
|
{new_email}."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
self.write_str(&format!(
|
||||||
|
"{user_id} has been associated with the email {new_email}."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
},
|
||||||
|
| (Some(current_email), None) => {
|
||||||
|
self.services
|
||||||
|
.threepid
|
||||||
|
.disassociate_localpart_email(user_id.localpart())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
self.write_str(&format!(
|
||||||
|
"The associated email of {user_id} has been removed (it was {current_email})."
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -29,6 +29,30 @@ pub enum UserCommand {
|
|||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// Issue a self-service password reset link for a user.
|
||||||
|
IssuePasswordResetLink {
|
||||||
|
/// Username of the user who may use the link
|
||||||
|
username: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get a user's associated email address.
|
||||||
|
GetEmail {
|
||||||
|
user_id: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Get the user with the given email address.
|
||||||
|
GetUserByEmail {
|
||||||
|
email: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
/// Update or remove a user's email address.
|
||||||
|
///
|
||||||
|
/// If `email` is not supplied, the user's existing address will be removed.
|
||||||
|
ChangeEmail {
|
||||||
|
user_id: String,
|
||||||
|
email: Option<String>,
|
||||||
|
},
|
||||||
|
|
||||||
/// Deactivate a user
|
/// Deactivate a user
|
||||||
///
|
///
|
||||||
/// User will be removed from all rooms by default.
|
/// User will be removed from all rooms by default.
|
||||||
|
|||||||
@@ -85,6 +85,7 @@ http-body-util.workspace = true
|
|||||||
hyper.workspace = true
|
hyper.workspace = true
|
||||||
ipaddress.workspace = true
|
ipaddress.workspace = true
|
||||||
itertools.workspace = true
|
itertools.workspace = true
|
||||||
|
lettre.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
reqwest.workspace = true
|
reqwest.workspace = true
|
||||||
|
|||||||
@@ -1,980 +0,0 @@
|
|||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use axum::extract::State;
|
|
||||||
use axum_client_ip::InsecureClientIp;
|
|
||||||
use conduwuit::{
|
|
||||||
Err, Error, Event, Result, debug_info, err, error, info,
|
|
||||||
matrix::pdu::PduBuilder,
|
|
||||||
utils::{self, ReadyExt, stream::BroadbandExt},
|
|
||||||
warn,
|
|
||||||
};
|
|
||||||
use conduwuit_service::Services;
|
|
||||||
use futures::{FutureExt, StreamExt};
|
|
||||||
use register::RegistrationKind;
|
|
||||||
use ruma::{
|
|
||||||
OwnedRoomId, UserId,
|
|
||||||
api::client::{
|
|
||||||
account::{
|
|
||||||
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
|
||||||
deactivate, get_3pids, get_username_availability,
|
|
||||||
register::{self, LoginType},
|
|
||||||
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
|
||||||
whoami,
|
|
||||||
},
|
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
|
||||||
},
|
|
||||||
events::{
|
|
||||||
GlobalAccountDataEventType, StateEventType,
|
|
||||||
room::{
|
|
||||||
member::{MembershipState, RoomMemberEventContent},
|
|
||||||
message::RoomMessageEventContent,
|
|
||||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
push,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, SESSION_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
|
||||||
use crate::Ruma;
|
|
||||||
|
|
||||||
const RANDOM_USER_ID_LENGTH: usize = 10;
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/register/available`
|
|
||||||
///
|
|
||||||
/// Checks if a username is valid and available on this server.
|
|
||||||
///
|
|
||||||
/// Conditions for returning true:
|
|
||||||
/// - The user id is not historical
|
|
||||||
/// - The server name of the user id matches this server
|
|
||||||
/// - No user or appservice on this server already claimed this username
|
|
||||||
///
|
|
||||||
/// Note: This will not reserve the username, so the username might become
|
|
||||||
/// invalid when trying to register
|
|
||||||
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
|
||||||
pub(crate) async fn get_register_available_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
|
||||||
body: Ruma<get_username_availability::v3::Request>,
|
|
||||||
) -> Result<get_username_availability::v3::Response> {
|
|
||||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
|
||||||
let is_matrix_appservice_irc = body.appservice_info.as_ref().is_some_and(|appservice| {
|
|
||||||
appservice.registration.id == "irc"
|
|
||||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
|
||||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
|
||||||
});
|
|
||||||
|
|
||||||
if services
|
|
||||||
.globals
|
|
||||||
.forbidden_usernames()
|
|
||||||
.is_match(&body.username)
|
|
||||||
{
|
|
||||||
return Err!(Request(Forbidden("Username is forbidden")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
|
||||||
let body_username = if is_matrix_appservice_irc {
|
|
||||||
body.username.clone()
|
|
||||||
} else {
|
|
||||||
body.username.to_lowercase()
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate user id
|
|
||||||
let user_id =
|
|
||||||
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
|
|
||||||
| Ok(user_id) => {
|
|
||||||
if let Err(e) = user_id.validate_strict() {
|
|
||||||
// unless the username is from the broken matrix appservice IRC bridge, we
|
|
||||||
// should follow synapse's behaviour on not allowing things like spaces
|
|
||||||
// and UTF-8 characters in usernames
|
|
||||||
if !is_matrix_appservice_irc {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {body_username} contains disallowed characters or spaces: \
|
|
||||||
{e}"
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {body_username} is not valid: {e}"
|
|
||||||
))));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check if username is creative enough
|
|
||||||
if services.users.exists(&user_id).await {
|
|
||||||
return Err!(Request(UserInUse("User ID is not available.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(ref info) = body.appservice_info {
|
|
||||||
if !info.is_user_match(&user_id) {
|
|
||||||
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if services.appservice.is_exclusive_user_id(&user_id).await {
|
|
||||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(get_username_availability::v3::Response { available: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/register`
|
|
||||||
///
|
|
||||||
/// Register an account on this homeserver.
|
|
||||||
///
|
|
||||||
/// You can use [`GET
|
|
||||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
|
||||||
/// html) to check if the user id is valid and available.
|
|
||||||
///
|
|
||||||
/// - Only works if registration is enabled
|
|
||||||
/// - If type is guest: ignores all parameters except
|
|
||||||
/// initial_device_display_name
|
|
||||||
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
|
||||||
/// - If type is not guest and no username is given: Always fails after UIAA
|
|
||||||
/// check
|
|
||||||
/// - Creates a new account and populates it with default account data
|
|
||||||
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
|
||||||
/// access_token
|
|
||||||
#[allow(clippy::doc_markdown)]
|
|
||||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
|
||||||
pub(crate) async fn register_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
|
||||||
body: Ruma<register::v3::Request>,
|
|
||||||
) -> Result<register::v3::Response> {
|
|
||||||
let is_guest = body.kind == RegistrationKind::Guest;
|
|
||||||
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
|
||||||
|
|
||||||
// Allow registration if it's enabled in the config file or if this is the first
|
|
||||||
// run (so the first user account can be created)
|
|
||||||
let allow_registration =
|
|
||||||
services.config.allow_registration || services.firstrun.is_first_run();
|
|
||||||
|
|
||||||
if !allow_registration && body.appservice_info.is_none() {
|
|
||||||
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
|
||||||
| (Some(username), Some(device_display_name)) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
user = %username,
|
|
||||||
device_name = %device_display_name,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (Some(username), _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
user = %username,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (_, Some(device_display_name)) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
device_name = %device_display_name,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| (None, _) => {
|
|
||||||
info!(
|
|
||||||
%is_guest,
|
|
||||||
"Rejecting registration attempt as registration is disabled"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"This server is not accepting registrations at this time."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if is_guest && !services.config.allow_guest_registration {
|
|
||||||
info!(
|
|
||||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
|
||||||
name: \"{}\"",
|
|
||||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
|
||||||
);
|
|
||||||
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// forbid guests from registering if there is not a real admin user yet. give
|
|
||||||
// generic user error.
|
|
||||||
if is_guest && services.users.count().await < 2 {
|
|
||||||
warn!(
|
|
||||||
"Guest account attempted to register before a real admin user has been registered, \
|
|
||||||
rejecting registration. Guest's initial device name: \"{}\"",
|
|
||||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
|
||||||
);
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"This server is not accepting registrations at this time."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let user_id = match (body.username.as_ref(), is_guest) {
|
|
||||||
| (Some(username), false) => {
|
|
||||||
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
|
|
||||||
let is_matrix_appservice_irc =
|
|
||||||
body.appservice_info.as_ref().is_some_and(|appservice| {
|
|
||||||
appservice.registration.id == "irc"
|
|
||||||
|| appservice.registration.id.contains("matrix-appservice-irc")
|
|
||||||
|| appservice.registration.id.contains("matrix_appservice_irc")
|
|
||||||
});
|
|
||||||
|
|
||||||
if services.globals.forbidden_usernames().is_match(username)
|
|
||||||
&& !emergency_mode_enabled
|
|
||||||
{
|
|
||||||
return Err!(Request(Forbidden("Username is forbidden")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// don't force the username lowercase if it's from matrix-appservice-irc
|
|
||||||
let body_username = if is_matrix_appservice_irc {
|
|
||||||
username.clone()
|
|
||||||
} else {
|
|
||||||
username.to_lowercase()
|
|
||||||
};
|
|
||||||
|
|
||||||
let proposed_user_id = match UserId::parse_with_server_name(
|
|
||||||
&body_username,
|
|
||||||
services.globals.server_name(),
|
|
||||||
) {
|
|
||||||
| Ok(user_id) => {
|
|
||||||
if let Err(e) = user_id.validate_strict() {
|
|
||||||
// unless the username is from the broken matrix appservice IRC bridge, or
|
|
||||||
// we are in emergency mode, we should follow synapse's behaviour on
|
|
||||||
// not allowing things like spaces and UTF-8 characters in usernames
|
|
||||||
if !is_matrix_appservice_irc && !emergency_mode_enabled {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {body_username} contains disallowed characters or \
|
|
||||||
spaces: {e}"
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't allow registration with user IDs that aren't local
|
|
||||||
if !services.globals.user_is_local(&user_id) {
|
|
||||||
return Err!(Request(InvalidUsername(
|
|
||||||
"Username {body_username} is not local to this server"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
user_id
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(Request(InvalidUsername(debug_warn!(
|
|
||||||
"Username {body_username} is not valid: {e}"
|
|
||||||
))));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if services.users.exists(&proposed_user_id).await {
|
|
||||||
return Err!(Request(UserInUse("User ID is not available.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
proposed_user_id
|
|
||||||
},
|
|
||||||
| _ => loop {
|
|
||||||
let proposed_user_id = UserId::parse_with_server_name(
|
|
||||||
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
|
||||||
services.globals.server_name(),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
if !services.users.exists(&proposed_user_id).await {
|
|
||||||
break proposed_user_id;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
|
||||||
match body.appservice_info {
|
|
||||||
| Some(ref info) =>
|
|
||||||
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
|
||||||
return Err!(Request(Exclusive(
|
|
||||||
"Username is not in an appservice namespace."
|
|
||||||
)));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(MissingToken("Missing appservice token.")));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
|
||||||
{
|
|
||||||
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIAA
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
|
||||||
flows: Vec::new(),
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
|
||||||
|
|
||||||
// Populate required UIAA flows
|
|
||||||
|
|
||||||
if services.firstrun.is_first_run() {
|
|
||||||
// Registration token forced while in first-run mode
|
|
||||||
uiaainfo.flows.push(AuthFlow {
|
|
||||||
stages: vec![AuthType::RegistrationToken],
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if services
|
|
||||||
.registration_tokens
|
|
||||||
.iterate_tokens()
|
|
||||||
.next()
|
|
||||||
.await
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
// Registration token required
|
|
||||||
uiaainfo.flows.push(AuthFlow {
|
|
||||||
stages: vec![AuthType::RegistrationToken],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if services.config.recaptcha_private_site_key.is_some() {
|
|
||||||
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
|
||||||
// ReCaptcha required
|
|
||||||
uiaainfo
|
|
||||||
.flows
|
|
||||||
.push(AuthFlow { stages: vec![AuthType::ReCaptcha] });
|
|
||||||
uiaainfo.params = serde_json::value::to_raw_value(&serde_json::json!({
|
|
||||||
"m.login.recaptcha": {
|
|
||||||
"public_key": pubkey,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
.expect("Failed to serialize recaptcha params");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if uiaainfo.flows.is_empty() && !skip_auth {
|
|
||||||
// Registration isn't _disabled_, but there's no captcha configured and no
|
|
||||||
// registration tokens currently set. Bail out by default unless open
|
|
||||||
// registration was explicitly enabled.
|
|
||||||
if !services
|
|
||||||
.config
|
|
||||||
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
|
||||||
{
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"This server is not accepting registrations at this time."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// We have open registration enabled (😧), provide a dummy stage
|
|
||||||
uiaainfo = UiaaInfo {
|
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Dummy] }],
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !skip_auth {
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(
|
|
||||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
|
||||||
.unwrap(),
|
|
||||||
"".into(),
|
|
||||||
auth,
|
|
||||||
&uiaainfo,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body {
|
|
||||||
| Some(ref json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services.uiaa.create(
|
|
||||||
&UserId::parse_with_server_name("", services.globals.server_name())
|
|
||||||
.unwrap(),
|
|
||||||
"".into(),
|
|
||||||
&uiaainfo,
|
|
||||||
json,
|
|
||||||
);
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(NotJson("JSON body is not valid")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let password = if is_guest { None } else { body.password.as_deref() };
|
|
||||||
|
|
||||||
// Create user
|
|
||||||
services.users.create(&user_id, password, None).await?;
|
|
||||||
|
|
||||||
// Default to pretty displayname
|
|
||||||
let mut displayname = user_id.localpart().to_owned();
|
|
||||||
|
|
||||||
// If `new_user_displayname_suffix` is set, registration will push whatever
|
|
||||||
// content is set to the user's display name with a space before it
|
|
||||||
if !services.globals.new_user_displayname_suffix().is_empty()
|
|
||||||
&& body.appservice_info.is_none()
|
|
||||||
{
|
|
||||||
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.set_displayname(&user_id, Some(displayname.clone()));
|
|
||||||
|
|
||||||
// Initial account data
|
|
||||||
services
|
|
||||||
.account_data
|
|
||||||
.update(
|
|
||||||
None,
|
|
||||||
&user_id,
|
|
||||||
GlobalAccountDataEventType::PushRules.to_string().into(),
|
|
||||||
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
|
||||||
content: ruma::events::push_rules::PushRulesEventContent {
|
|
||||||
global: push::Ruleset::server_default(&user_id),
|
|
||||||
},
|
|
||||||
})?,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Generate new device id if the user didn't specify one
|
|
||||||
let no_device = body.inhibit_login
|
|
||||||
|| body
|
|
||||||
.appservice_info
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|aps| aps.registration.device_management);
|
|
||||||
let (token, device) = if !no_device {
|
|
||||||
// Don't create a device for inhibited logins
|
|
||||||
let device_id = if is_guest { None } else { body.device_id.clone() }
|
|
||||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
|
||||||
|
|
||||||
// Generate new token for the device
|
|
||||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
|
||||||
|
|
||||||
// Create device for this account
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.create_device(
|
|
||||||
&user_id,
|
|
||||||
&device_id,
|
|
||||||
&new_token,
|
|
||||||
body.initial_device_display_name.clone(),
|
|
||||||
Some(client.to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
debug_info!(%user_id, %device_id, "User account was created");
|
|
||||||
(Some(new_token), Some(device_id))
|
|
||||||
} else {
|
|
||||||
(None, None)
|
|
||||||
};
|
|
||||||
|
|
||||||
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
|
||||||
|
|
||||||
// log in conduit admin channel if a non-guest user registered
|
|
||||||
if body.appservice_info.is_none() && !is_guest {
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
let notice = format!(
|
|
||||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
|
||||||
display name \"{device_display_name}\""
|
|
||||||
);
|
|
||||||
|
|
||||||
info!("{notice}");
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let notice = format!("New user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
info!("{notice}");
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services.admin.notice(¬ice).await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// log in conduit admin channel if a guest registered
|
|
||||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
|
||||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
|
||||||
|
|
||||||
if !device_display_name.is_empty() {
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with device display name \
|
|
||||||
\"{device_display_name}\" registered on this server from IP {client}"
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
#[allow(clippy::collapsible_else_if)]
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!(
|
|
||||||
"Guest user \"{user_id}\" with no device display name registered on \
|
|
||||||
this server from IP {client}",
|
|
||||||
))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !is_guest {
|
|
||||||
// Make the first user to register an administrator and disable first-run mode.
|
|
||||||
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
|
||||||
|
|
||||||
// If the registering user was not the first and we're suspending users on
|
|
||||||
// register, suspend them.
|
|
||||||
if !was_first_user && services.config.suspend_on_register {
|
|
||||||
// Note that we can still do auto joins for suspended users
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.suspend_account(&user_id, &services.globals.server_user)
|
|
||||||
.await;
|
|
||||||
// And send an @room notice to the admin room, to prompt admins to review the
|
|
||||||
// new user and ideally unsuspend them if deemed appropriate.
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
|
||||||
"User {user_id} has been suspended as they are not the first user on \
|
|
||||||
this server. Please review and unsuspend them if appropriate."
|
|
||||||
)))
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.appservice_info.is_none()
|
|
||||||
&& !services.server.config.auto_join_rooms.is_empty()
|
|
||||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
|
||||||
{
|
|
||||||
for room in &services.server.config.auto_join_rooms {
|
|
||||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
|
||||||
error!(
|
|
||||||
"Failed to resolve room alias to room ID when attempting to auto join \
|
|
||||||
{room}, skipping"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.server_in_room(services.globals.server_name(), &room_id)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
warn!(
|
|
||||||
"Skipping room {room} to automatically join as we have never joined before."
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(room_server_name) = room.server_name() {
|
|
||||||
match join_room_by_id_helper(
|
|
||||||
&services,
|
|
||||||
&user_id,
|
|
||||||
&room_id,
|
|
||||||
Some("Automatically joining this room upon registration".to_owned()),
|
|
||||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
|
||||||
&body.appservice_info,
|
|
||||||
)
|
|
||||||
.boxed()
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Err(e) => {
|
|
||||||
// don't return this error so we don't fail registrations
|
|
||||||
error!(
|
|
||||||
"Failed to automatically join room {room} for user {user_id}: {e}"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
info!("Automatically joined room {room} for user {user_id}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(register::v3::Response {
|
|
||||||
access_token: token,
|
|
||||||
user_id,
|
|
||||||
device_id: device,
|
|
||||||
refresh_token: None,
|
|
||||||
expires_in: None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/account/password`
|
|
||||||
///
|
|
||||||
/// Changes the password of this account.
|
|
||||||
///
|
|
||||||
/// - Requires UIAA to verify user password
|
|
||||||
/// - Changes the password of the sender user
|
|
||||||
/// - The password hash is calculated using argon2 with 32 character salt, the
|
|
||||||
/// plain password is
|
|
||||||
/// not saved
|
|
||||||
///
|
|
||||||
/// If logout_devices is true it does the following for each device except the
|
|
||||||
/// sender device:
|
|
||||||
/// - Invalidates access token
|
|
||||||
/// - Deletes device metadata (device id, device display name, last seen ip,
|
|
||||||
/// last seen ts)
|
|
||||||
/// - Forgets to-device events
|
|
||||||
/// - Triggers device list updates
|
|
||||||
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
|
||||||
pub(crate) async fn change_password_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
|
||||||
body: Ruma<change_password::v3::Request>,
|
|
||||||
) -> Result<change_password::v3::Response> {
|
|
||||||
// Authentication for this endpoint was made optional, but we need
|
|
||||||
// authentication currently
|
|
||||||
let sender_user = body
|
|
||||||
.sender_user
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
|
||||||
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body {
|
|
||||||
| Some(ref json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(NotJson("JSON body is not valid")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.set_password(sender_user, Some(&body.new_password))
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if body.logout_devices {
|
|
||||||
// Logout all devices except the current one
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.all_device_ids(sender_user)
|
|
||||||
.ready_filter(|id| *id != body.sender_device())
|
|
||||||
.for_each(|id| services.users.remove_device(sender_user, id))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// Remove all pushers except the ones associated with this session
|
|
||||||
services
|
|
||||||
.pusher
|
|
||||||
.get_pushkeys(sender_user)
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.broad_filter_map(async |pushkey| {
|
|
||||||
services
|
|
||||||
.pusher
|
|
||||||
.get_pusher_device(&pushkey)
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
.filter(|pusher_device| pusher_device != body.sender_device())
|
|
||||||
.is_some()
|
|
||||||
.then_some(pushkey)
|
|
||||||
})
|
|
||||||
.for_each(async |pushkey| {
|
|
||||||
services.pusher.delete_pusher(sender_user, &pushkey).await;
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("User {sender_user} changed their password.");
|
|
||||||
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!("User {sender_user} changed their password."))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(change_password::v3::Response {})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/account/whoami`
|
|
||||||
///
|
|
||||||
/// Get `user_id` of the sender user.
|
|
||||||
///
|
|
||||||
/// Note: Also works for Application Services
|
|
||||||
pub(crate) async fn whoami_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<whoami::v3::Request>,
|
|
||||||
) -> Result<whoami::v3::Response> {
|
|
||||||
let is_guest = services
|
|
||||||
.users
|
|
||||||
.is_deactivated(body.sender_user())
|
|
||||||
.await
|
|
||||||
.map_err(|_| {
|
|
||||||
err!(Request(Forbidden("Application service has not registered this user.")))
|
|
||||||
})? && body.appservice_info.is_none();
|
|
||||||
Ok(whoami::v3::Response {
|
|
||||||
user_id: body.sender_user().to_owned(),
|
|
||||||
device_id: body.sender_device.clone(),
|
|
||||||
is_guest,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/account/deactivate`
|
|
||||||
///
|
|
||||||
/// Deactivate sender user account.
|
|
||||||
///
|
|
||||||
/// - Leaves all rooms and rejects all invitations
|
|
||||||
/// - Invalidates all access tokens
|
|
||||||
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
|
||||||
/// last seen ts)
|
|
||||||
/// - Forgets all to-device events
|
|
||||||
/// - Triggers device list updates
|
|
||||||
/// - Removes ability to log in again
|
|
||||||
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
|
||||||
pub(crate) async fn deactivate_route(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
InsecureClientIp(client): InsecureClientIp,
|
|
||||||
body: Ruma<deactivate::v3::Request>,
|
|
||||||
) -> Result<deactivate::v3::Response> {
|
|
||||||
// Authentication for this endpoint was made optional, but we need
|
|
||||||
// authentication currently
|
|
||||||
let sender_user = body
|
|
||||||
.sender_user
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
|
||||||
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(sender_user, body.sender_device(), auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body {
|
|
||||||
| Some(ref json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, body.sender_device(), &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(NotJson("JSON body is not valid")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove profile pictures and display name
|
|
||||||
let all_joined_rooms: Vec<OwnedRoomId> = services
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.rooms_joined(sender_user)
|
|
||||||
.map(Into::into)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
|
||||||
.boxed()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
info!("User {sender_user} deactivated their account.");
|
|
||||||
|
|
||||||
if services.server.config.admin_room_notices {
|
|
||||||
services
|
|
||||||
.admin
|
|
||||||
.notice(&format!("User {sender_user} deactivated their account."))
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(deactivate::v3::Response {
|
|
||||||
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `GET _matrix/client/v3/account/3pid`
|
|
||||||
///
|
|
||||||
/// Get a list of third party identifiers associated with this account.
|
|
||||||
///
|
|
||||||
/// - Currently always returns empty list
|
|
||||||
pub(crate) async fn third_party_route(
|
|
||||||
body: Ruma<get_3pids::v3::Request>,
|
|
||||||
) -> Result<get_3pids::v3::Response> {
|
|
||||||
let _sender_user = body.sender_user.as_ref().expect("user is authenticated");
|
|
||||||
|
|
||||||
Ok(get_3pids::v3::Response::new(Vec::new()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
|
||||||
///
|
|
||||||
/// "This API should be used to request validation tokens when adding an email
|
|
||||||
/// address to an account"
|
|
||||||
///
|
|
||||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
|
||||||
/// as a contact option.
|
|
||||||
pub(crate) async fn request_3pid_management_token_via_email_route(
|
|
||||||
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
|
||||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
|
||||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
|
||||||
///
|
|
||||||
/// "This API should be used to request validation tokens when adding an phone
|
|
||||||
/// number to an account"
|
|
||||||
///
|
|
||||||
/// - 403 signals that The homeserver does not allow the third party identifier
|
|
||||||
/// as a contact option.
|
|
||||||
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
|
||||||
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
|
||||||
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
|
||||||
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
|
||||||
///
|
|
||||||
/// Checks if the provided registration token is valid at the time of checking.
|
|
||||||
pub(crate) async fn check_registration_token_validity(
|
|
||||||
State(services): State<crate::State>,
|
|
||||||
body: Ruma<check_registration_token_validity::v1::Request>,
|
|
||||||
) -> Result<check_registration_token_validity::v1::Response> {
|
|
||||||
// TODO: ratelimit this pretty heavily
|
|
||||||
|
|
||||||
let valid = services
|
|
||||||
.registration_tokens
|
|
||||||
.validate_token(body.token.clone())
|
|
||||||
.await
|
|
||||||
.is_some();
|
|
||||||
|
|
||||||
Ok(check_registration_token_validity::v1::Response { valid })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Runs through all the deactivation steps:
|
|
||||||
///
|
|
||||||
/// - Mark as deactivated
|
|
||||||
/// - Removing display name
|
|
||||||
/// - Removing avatar URL and blurhash
|
|
||||||
/// - Removing all profile data
|
|
||||||
/// - Leaving all rooms (and forgets all of them)
|
|
||||||
pub async fn full_user_deactivate(
|
|
||||||
services: &Services,
|
|
||||||
user_id: &UserId,
|
|
||||||
all_joined_rooms: &[OwnedRoomId],
|
|
||||||
) -> Result<()> {
|
|
||||||
services.users.deactivate_account(user_id).await.ok();
|
|
||||||
|
|
||||||
services
|
|
||||||
.users
|
|
||||||
.all_profile_keys(user_id)
|
|
||||||
.ready_for_each(|(profile_key, _)| {
|
|
||||||
services.users.set_profile_key(user_id, &profile_key, None);
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// TODO: Rescind all user invites
|
|
||||||
|
|
||||||
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
|
||||||
|
|
||||||
for room_id in all_joined_rooms {
|
|
||||||
let room_power_levels = services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
|
||||||
room_id,
|
|
||||||
&StateEventType::RoomPowerLevels,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
|
|
||||||
let user_can_demote_self =
|
|
||||||
room_power_levels
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|power_levels_content| {
|
|
||||||
RoomPowerLevels::from(power_levels_content.clone())
|
|
||||||
.user_can_change_user_power_level(user_id, user_id)
|
|
||||||
}) || services
|
|
||||||
.rooms
|
|
||||||
.state_accessor
|
|
||||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
|
||||||
.await
|
|
||||||
.is_ok_and(|event| event.sender() == user_id);
|
|
||||||
|
|
||||||
if user_can_demote_self {
|
|
||||||
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
|
||||||
power_levels_content.users.remove(user_id);
|
|
||||||
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
|
||||||
pdu_queue.push((pl_evt, room_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Leave the room
|
|
||||||
pdu_queue.push((
|
|
||||||
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
|
||||||
avatar_url: None,
|
|
||||||
blurhash: None,
|
|
||||||
membership: MembershipState::Leave,
|
|
||||||
displayname: None,
|
|
||||||
join_authorized_via_users_server: None,
|
|
||||||
reason: None,
|
|
||||||
is_direct: None,
|
|
||||||
third_party_invite: None,
|
|
||||||
redact_events: None,
|
|
||||||
}),
|
|
||||||
room_id,
|
|
||||||
));
|
|
||||||
|
|
||||||
// TODO: Redact all messages sent by the user in the room
|
|
||||||
}
|
|
||||||
|
|
||||||
super::update_all_rooms(services, pdu_queue, user_id).await;
|
|
||||||
for room_id in all_joined_rooms {
|
|
||||||
services.rooms.state_cache.forget(room_id, user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
use axum::extract::State;
|
||||||
|
use axum_client_ip::InsecureClientIp;
|
||||||
|
use conduwuit::{
|
||||||
|
Err, Event, Result, err, info,
|
||||||
|
pdu::PduBuilder,
|
||||||
|
utils::{ReadyExt, stream::BroadbandExt},
|
||||||
|
};
|
||||||
|
use conduwuit_service::Services;
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use lettre::{Address, message::Mailbox};
|
||||||
|
use ruma::{
|
||||||
|
OwnedRoomId, OwnedUserId, UserId,
|
||||||
|
api::client::{
|
||||||
|
account::{
|
||||||
|
ThirdPartyIdRemovalStatus, change_password, check_registration_token_validity,
|
||||||
|
deactivate, get_username_availability, request_password_change_token_via_email,
|
||||||
|
whoami,
|
||||||
|
},
|
||||||
|
uiaa::{AuthFlow, AuthType},
|
||||||
|
},
|
||||||
|
events::{
|
||||||
|
StateEventType,
|
||||||
|
room::{
|
||||||
|
member::{MembershipState, RoomMemberEventContent},
|
||||||
|
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use service::{mailer::messages, uiaa::Identity};
|
||||||
|
|
||||||
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
pub(crate) mod register;
|
||||||
|
pub(crate) mod threepid;
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v3/register/available`
|
||||||
|
///
|
||||||
|
/// Checks if a username is valid and available on this server.
|
||||||
|
///
|
||||||
|
/// Conditions for returning true:
|
||||||
|
/// - The user id is not historical
|
||||||
|
/// - The server name of the user id matches this server
|
||||||
|
/// - No user or appservice on this server already claimed this username
|
||||||
|
///
|
||||||
|
/// Note: This will not reserve the username, so the username might become
|
||||||
|
/// invalid when trying to register
|
||||||
|
#[tracing::instrument(skip_all, fields(%client), name = "register_available", level = "info")]
|
||||||
|
pub(crate) async fn get_register_available_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<get_username_availability::v3::Request>,
|
||||||
|
) -> Result<get_username_availability::v3::Response> {
|
||||||
|
// Validate user id
|
||||||
|
let user_id =
|
||||||
|
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
|
||||||
|
| Ok(user_id) => {
|
||||||
|
if let Err(e) = user_id.validate_strict() {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {} contains disallowed characters or spaces: {e}",
|
||||||
|
body.username
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {} is not valid: {e}",
|
||||||
|
body.username
|
||||||
|
))));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if username is creative enough
|
||||||
|
if services.users.exists(&user_id).await {
|
||||||
|
return Err!(Request(UserInUse("User ID is not available.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(ref info) = body.appservice_info {
|
||||||
|
if !info.is_user_match(&user_id) {
|
||||||
|
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if services.appservice.is_exclusive_user_id(&user_id).await {
|
||||||
|
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(get_username_availability::v3::Response { available: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/r0/account/password`
|
||||||
|
///
|
||||||
|
/// Changes the password of this account.
|
||||||
|
///
|
||||||
|
/// - Requires UIAA to verify user password
|
||||||
|
/// - Changes the password of the sender user
|
||||||
|
/// - The password hash is calculated using argon2 with 32 character salt, the
|
||||||
|
/// plain password is
|
||||||
|
/// not saved
|
||||||
|
///
|
||||||
|
/// If logout_devices is true it does the following for each device except the
|
||||||
|
/// sender device:
|
||||||
|
/// - Invalidates access token
|
||||||
|
/// - Deletes device metadata (device id, device display name, last seen ip,
|
||||||
|
/// last seen ts)
|
||||||
|
/// - Forgets to-device events
|
||||||
|
/// - Triggers device list updates
|
||||||
|
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
|
||||||
|
pub(crate) async fn change_password_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<change_password::v3::Request>,
|
||||||
|
) -> Result<change_password::v3::Response> {
|
||||||
|
let identity = if let Some(ref user_id) = body.sender_user {
|
||||||
|
// A signed-in user is trying to change their password, prompt them for their
|
||||||
|
// existing one
|
||||||
|
|
||||||
|
services
|
||||||
|
.uiaa
|
||||||
|
.authenticate(
|
||||||
|
&body.auth,
|
||||||
|
vec![AuthFlow::new(vec![AuthType::Password])],
|
||||||
|
Box::default(),
|
||||||
|
Some(Identity::from_user_id(user_id)),
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
// A signed-out user is trying to reset their password, prompt them for email
|
||||||
|
// confirmation. Note that we do not _send_ an email here, their client should
|
||||||
|
// have already hit `/account/password/requestToken` to send the email. We
|
||||||
|
// just validate it.
|
||||||
|
|
||||||
|
services
|
||||||
|
.uiaa
|
||||||
|
.authenticate(
|
||||||
|
&body.auth,
|
||||||
|
vec![AuthFlow::new(vec![AuthType::EmailIdentity])],
|
||||||
|
Box::default(),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let sender_user = OwnedUserId::parse(format!(
|
||||||
|
"@{}:{}",
|
||||||
|
identity.localpart.expect("localpart should be known"),
|
||||||
|
services.globals.server_name()
|
||||||
|
))
|
||||||
|
.expect("user ID should be valid");
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.set_password(&sender_user, Some(&body.new_password))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if body.logout_devices {
|
||||||
|
// Logout all devices except the current one
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.all_device_ids(&sender_user)
|
||||||
|
.ready_filter(|id| *id != body.sender_device())
|
||||||
|
.for_each(|id| services.users.remove_device(&sender_user, id))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Remove all pushers except the ones associated with this session
|
||||||
|
services
|
||||||
|
.pusher
|
||||||
|
.get_pushkeys(&sender_user)
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
.broad_filter_map(async |pushkey| {
|
||||||
|
services
|
||||||
|
.pusher
|
||||||
|
.get_pusher_device(&pushkey)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.filter(|pusher_device| pusher_device != body.sender_device())
|
||||||
|
.is_some()
|
||||||
|
.then_some(pushkey)
|
||||||
|
})
|
||||||
|
.for_each(async |pushkey| {
|
||||||
|
services.pusher.delete_pusher(&sender_user, &pushkey).await;
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("User {} changed their password.", &sender_user);
|
||||||
|
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!("User {} changed their password.", &sender_user))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(change_password::v3::Response {})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/password/email/requestToken`
|
||||||
|
///
|
||||||
|
/// Requests a validation email for the purpose of resetting a user's password.
|
||||||
|
pub(crate) async fn request_password_change_token_via_email_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<request_password_change_token_via_email::v3::Request>,
|
||||||
|
) -> Result<request_password_change_token_via_email::v3::Response> {
|
||||||
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(localpart) = services.threepid.get_localpart_for_email(&email).await else {
|
||||||
|
return Err!(Request(ThreepidNotFound(
|
||||||
|
"No account is associated with this email address"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let user_id =
|
||||||
|
OwnedUserId::parse(format!("@{localpart}:{}", services.globals.server_name())).unwrap();
|
||||||
|
let display_name = services.users.displayname(&user_id).await.ok();
|
||||||
|
|
||||||
|
let session = services
|
||||||
|
.threepid
|
||||||
|
.send_validation_email(
|
||||||
|
Mailbox::new(display_name.clone(), email),
|
||||||
|
|verification_link| messages::PasswordReset {
|
||||||
|
display_name: display_name.as_deref(),
|
||||||
|
user_id: &user_id,
|
||||||
|
verification_link,
|
||||||
|
},
|
||||||
|
&body.client_secret,
|
||||||
|
body.send_attempt.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(request_password_change_token_via_email::v3::Response::new(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v3/account/whoami`
|
||||||
|
///
|
||||||
|
/// Get `user_id` of the sender user.
|
||||||
|
///
|
||||||
|
/// Note: Also works for Application Services
|
||||||
|
pub(crate) async fn whoami_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<whoami::v3::Request>,
|
||||||
|
) -> Result<whoami::v3::Response> {
|
||||||
|
let is_guest = services
|
||||||
|
.users
|
||||||
|
.is_deactivated(body.sender_user())
|
||||||
|
.await
|
||||||
|
.map_err(|_| {
|
||||||
|
err!(Request(Forbidden("Application service has not registered this user.")))
|
||||||
|
})? && body.appservice_info.is_none();
|
||||||
|
Ok(whoami::v3::Response {
|
||||||
|
user_id: body.sender_user().to_owned(),
|
||||||
|
device_id: body.sender_device.clone(),
|
||||||
|
is_guest,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/r0/account/deactivate`
|
||||||
|
///
|
||||||
|
/// Deactivate sender user account.
|
||||||
|
///
|
||||||
|
/// - Leaves all rooms and rejects all invitations
|
||||||
|
/// - Invalidates all access tokens
|
||||||
|
/// - Deletes all device metadata (device id, device display name, last seen ip,
|
||||||
|
/// last seen ts)
|
||||||
|
/// - Forgets all to-device events
|
||||||
|
/// - Triggers device list updates
|
||||||
|
/// - Removes ability to log in again
|
||||||
|
#[tracing::instrument(skip_all, fields(%client), name = "deactivate", level = "info")]
|
||||||
|
pub(crate) async fn deactivate_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<deactivate::v3::Request>,
|
||||||
|
) -> Result<deactivate::v3::Response> {
|
||||||
|
// Authentication for this endpoint is technically optional,
|
||||||
|
// but we require the user to be logged in
|
||||||
|
let sender_user = body
|
||||||
|
.sender_user
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
|
||||||
|
|
||||||
|
// Prompt the user to confirm with their password using UIAA
|
||||||
|
let _ = services
|
||||||
|
.uiaa
|
||||||
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Remove profile pictures and display name
|
||||||
|
let all_joined_rooms: Vec<OwnedRoomId> = services
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.rooms_joined(sender_user)
|
||||||
|
.map(Into::into)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
full_user_deactivate(&services, sender_user, &all_joined_rooms)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
info!("User {sender_user} deactivated their account.");
|
||||||
|
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!("User {sender_user} deactivated their account."))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(deactivate::v3::Response {
|
||||||
|
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
|
||||||
|
///
|
||||||
|
/// Checks if the provided registration token is valid at the time of checking.
|
||||||
|
pub(crate) async fn check_registration_token_validity(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<check_registration_token_validity::v1::Request>,
|
||||||
|
) -> Result<check_registration_token_validity::v1::Response> {
|
||||||
|
// TODO: ratelimit this pretty heavily
|
||||||
|
|
||||||
|
let valid = services
|
||||||
|
.registration_tokens
|
||||||
|
.validate_token(body.token.clone())
|
||||||
|
.await
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
Ok(check_registration_token_validity::v1::Response { valid })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs through all the deactivation steps:
|
||||||
|
///
|
||||||
|
/// - Mark as deactivated
|
||||||
|
/// - Removing display name
|
||||||
|
/// - Removing avatar URL and blurhash
|
||||||
|
/// - Removing all profile data
|
||||||
|
/// - Leaving all rooms (and forgets all of them)
|
||||||
|
pub async fn full_user_deactivate(
|
||||||
|
services: &Services,
|
||||||
|
user_id: &UserId,
|
||||||
|
all_joined_rooms: &[OwnedRoomId],
|
||||||
|
) -> Result<()> {
|
||||||
|
services.users.deactivate_account(user_id).await.ok();
|
||||||
|
|
||||||
|
if services.globals.user_is_local(user_id) {
|
||||||
|
let _ = services
|
||||||
|
.threepid
|
||||||
|
.disassociate_localpart_email(user_id.localpart())
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.all_profile_keys(user_id)
|
||||||
|
.ready_for_each(|(profile_key, _)| {
|
||||||
|
services.users.set_profile_key(user_id, &profile_key, None);
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// TODO: Rescind all user invites
|
||||||
|
|
||||||
|
let mut pdu_queue: Vec<(PduBuilder, &OwnedRoomId)> = Vec::new();
|
||||||
|
|
||||||
|
for room_id in all_joined_rooms {
|
||||||
|
let room_power_levels = services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||||
|
room_id,
|
||||||
|
&StateEventType::RoomPowerLevels,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
|
||||||
|
let user_can_demote_self =
|
||||||
|
room_power_levels
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|power_levels_content| {
|
||||||
|
RoomPowerLevels::from(power_levels_content.clone())
|
||||||
|
.user_can_change_user_power_level(user_id, user_id)
|
||||||
|
}) || services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||||
|
.await
|
||||||
|
.is_ok_and(|event| event.sender() == user_id);
|
||||||
|
|
||||||
|
if user_can_demote_self {
|
||||||
|
let mut power_levels_content = room_power_levels.unwrap_or_default();
|
||||||
|
power_levels_content.users.remove(user_id);
|
||||||
|
let pl_evt = PduBuilder::state(String::new(), &power_levels_content);
|
||||||
|
pdu_queue.push((pl_evt, room_id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Leave the room
|
||||||
|
pdu_queue.push((
|
||||||
|
PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
|
||||||
|
avatar_url: None,
|
||||||
|
blurhash: None,
|
||||||
|
membership: MembershipState::Leave,
|
||||||
|
displayname: None,
|
||||||
|
join_authorized_via_users_server: None,
|
||||||
|
reason: None,
|
||||||
|
is_direct: None,
|
||||||
|
third_party_invite: None,
|
||||||
|
redact_events: None,
|
||||||
|
}),
|
||||||
|
room_id,
|
||||||
|
));
|
||||||
|
|
||||||
|
// TODO: Redact all messages sent by the user in the room
|
||||||
|
}
|
||||||
|
|
||||||
|
super::update_all_rooms(services, pdu_queue, user_id).await;
|
||||||
|
for room_id in all_joined_rooms {
|
||||||
|
services.rooms.state_cache.forget(room_id, user_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,601 @@
|
|||||||
|
use std::{collections::HashMap, fmt::Write};
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use axum_client_ip::InsecureClientIp;
|
||||||
|
use conduwuit::{
|
||||||
|
Err, Result, debug_info, error, info,
|
||||||
|
utils::{self},
|
||||||
|
warn,
|
||||||
|
};
|
||||||
|
use conduwuit_service::Services;
|
||||||
|
use futures::{FutureExt, StreamExt};
|
||||||
|
use lettre::{Address, message::Mailbox};
|
||||||
|
use register::RegistrationKind;
|
||||||
|
use ruma::{
|
||||||
|
OwnedUserId, UserId,
|
||||||
|
api::client::{
|
||||||
|
account::{
|
||||||
|
register::{self, LoginType},
|
||||||
|
request_registration_token_via_email,
|
||||||
|
},
|
||||||
|
uiaa::{AuthFlow, AuthType},
|
||||||
|
},
|
||||||
|
events::{GlobalAccountDataEventType, room::message::RoomMessageEventContent},
|
||||||
|
push,
|
||||||
|
};
|
||||||
|
use serde_json::value::RawValue;
|
||||||
|
use service::mailer::messages;
|
||||||
|
|
||||||
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
const RANDOM_USER_ID_LENGTH: usize = 10;
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/register`
|
||||||
|
///
|
||||||
|
/// Register an account on this homeserver.
|
||||||
|
///
|
||||||
|
/// You can use [`GET
|
||||||
|
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||||
|
/// html) to check if the user id is valid and available.
|
||||||
|
///
|
||||||
|
/// - Only works if registration is enabled
|
||||||
|
/// - If type is guest: ignores all parameters except
|
||||||
|
/// initial_device_display_name
|
||||||
|
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
|
||||||
|
/// - If type is not guest and no username is given: Always fails after UIAA
|
||||||
|
/// check
|
||||||
|
/// - Creates a new account and populates it with default account data
|
||||||
|
/// - If `inhibit_login` is false: Creates a device and returns device id and
|
||||||
|
/// access_token
|
||||||
|
#[allow(clippy::doc_markdown)]
|
||||||
|
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||||
|
pub(crate) async fn register_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<register::v3::Request>,
|
||||||
|
) -> Result<register::v3::Response> {
|
||||||
|
let is_guest = body.kind == RegistrationKind::Guest;
|
||||||
|
let emergency_mode_enabled = services.config.emergency_password.is_some();
|
||||||
|
|
||||||
|
// Allow registration if it's enabled in the config file or if this is the first
|
||||||
|
// run (so the first user account can be created)
|
||||||
|
let allow_registration =
|
||||||
|
services.config.allow_registration || services.firstrun.is_first_run();
|
||||||
|
|
||||||
|
if !allow_registration && body.appservice_info.is_none() {
|
||||||
|
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
|
||||||
|
| (Some(username), Some(device_display_name)) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
user = %username,
|
||||||
|
device_name = %device_display_name,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (Some(username), _) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
user = %username,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (_, Some(device_display_name)) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
device_name = %device_display_name,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| (None, _) => {
|
||||||
|
info!(
|
||||||
|
%is_guest,
|
||||||
|
"Rejecting registration attempt as registration is disabled"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_guest && !services.config.allow_guest_registration {
|
||||||
|
info!(
|
||||||
|
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||||
|
name: \"{}\"",
|
||||||
|
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
|
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// forbid guests from registering if there is not a real admin user yet. give
|
||||||
|
// generic user error.
|
||||||
|
if is_guest && services.firstrun.is_first_run() {
|
||||||
|
warn!(
|
||||||
|
"Guest account attempted to register before a real admin user has been registered, \
|
||||||
|
rejecting registration. Guest's initial device name: \"{}\"",
|
||||||
|
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||||
|
);
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Appeservices and guests get to skip auth
|
||||||
|
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||||
|
|
||||||
|
let identity = if skip_auth {
|
||||||
|
// Appservices and guests have no identity
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Perform UIAA to determine the user's identity
|
||||||
|
let (flows, params) = create_registration_uiaa_session(&services).await?;
|
||||||
|
|
||||||
|
Some(
|
||||||
|
services
|
||||||
|
.uiaa
|
||||||
|
.authenticate(&body.auth, flows, params, None)
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the user didn't supply a username but did supply an email, use
|
||||||
|
// the email's user as their initial localpart to avoid falling back to
|
||||||
|
// a randomly generated localpart
|
||||||
|
let supplied_username = body.username.clone().or_else(|| {
|
||||||
|
if let Some(identity) = &identity
|
||||||
|
&& let Some(email) = &identity.email
|
||||||
|
{
|
||||||
|
Some(email.user().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let user_id = determine_registration_user_id(
|
||||||
|
&services,
|
||||||
|
supplied_username,
|
||||||
|
is_guest,
|
||||||
|
emergency_mode_enabled,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||||
|
// For appservice logins, make sure that the user ID is in the appservice's
|
||||||
|
// namespace
|
||||||
|
|
||||||
|
match body.appservice_info {
|
||||||
|
| Some(ref info) =>
|
||||||
|
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
|
||||||
|
return Err!(Request(Exclusive(
|
||||||
|
"Username is not in an appservice namespace."
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
| _ => {
|
||||||
|
return Err!(Request(MissingToken("Missing appservice token.")));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
|
||||||
|
{
|
||||||
|
// For non-appservice logins, ban user IDs which are in an appservice's
|
||||||
|
// namespace (unless emergency mode is enabled)
|
||||||
|
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let password = if is_guest { None } else { body.password.as_deref() };
|
||||||
|
|
||||||
|
// Create user
|
||||||
|
services.users.create(&user_id, password, None).await?;
|
||||||
|
|
||||||
|
// Set an initial display name
|
||||||
|
let mut displayname = user_id.localpart().to_owned();
|
||||||
|
|
||||||
|
// Apply the new user displayname suffix, if it's set
|
||||||
|
if !services.globals.new_user_displayname_suffix().is_empty()
|
||||||
|
&& body.appservice_info.is_none()
|
||||||
|
{
|
||||||
|
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.set_displayname(&user_id, Some(displayname.clone()));
|
||||||
|
|
||||||
|
// Initial account data
|
||||||
|
services
|
||||||
|
.account_data
|
||||||
|
.update(
|
||||||
|
None,
|
||||||
|
&user_id,
|
||||||
|
GlobalAccountDataEventType::PushRules.to_string().into(),
|
||||||
|
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent {
|
||||||
|
content: ruma::events::push_rules::PushRulesEventContent {
|
||||||
|
global: push::Ruleset::server_default(&user_id),
|
||||||
|
},
|
||||||
|
})?,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Generate new device id if the user didn't specify one
|
||||||
|
let no_device = body.inhibit_login
|
||||||
|
|| body
|
||||||
|
.appservice_info
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|aps| aps.registration.device_management);
|
||||||
|
|
||||||
|
let (token, device) = if !no_device {
|
||||||
|
// Don't create a device for inhibited logins
|
||||||
|
let device_id = if is_guest { None } else { body.device_id.clone() }
|
||||||
|
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||||
|
|
||||||
|
// Generate new token for the device
|
||||||
|
let new_token = utils::random_string(TOKEN_LENGTH);
|
||||||
|
|
||||||
|
// Create device for this account
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.create_device(
|
||||||
|
&user_id,
|
||||||
|
&device_id,
|
||||||
|
&new_token,
|
||||||
|
body.initial_device_display_name.clone(),
|
||||||
|
Some(client.to_string()),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
debug_info!(%user_id, %device_id, "User account was created");
|
||||||
|
(Some(new_token), Some(device_id))
|
||||||
|
} else {
|
||||||
|
(None, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
// If the user registered with an email, associate it with their account.
|
||||||
|
if let Some(identity) = identity
|
||||||
|
&& let Some(email) = identity.email
|
||||||
|
{
|
||||||
|
// This may fail if the email is already in use, but we already check for that
|
||||||
|
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
|
||||||
|
// that an email is sniped by another user between the `/requestToken` request
|
||||||
|
// and the `/register` request.
|
||||||
|
let _ = services
|
||||||
|
.threepid
|
||||||
|
.associate_localpart_email(user_id.localpart(), &email)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
|
||||||
|
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
|
||||||
|
|
||||||
|
// log in conduit admin channel if a non-guest user registered
|
||||||
|
if body.appservice_info.is_none() && !is_guest {
|
||||||
|
if !device_display_name.is_empty() {
|
||||||
|
let notice = format!(
|
||||||
|
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||||
|
display name \"{device_display_name}\""
|
||||||
|
);
|
||||||
|
|
||||||
|
info!("{notice}");
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services.admin.notice(¬ice).await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let notice = format!("New user \"{user_id}\" registered on this server.");
|
||||||
|
|
||||||
|
info!("{notice}");
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services.admin.notice(¬ice).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// log in conduit admin channel if a guest registered
|
||||||
|
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||||
|
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||||
|
|
||||||
|
if !device_display_name.is_empty() {
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!(
|
||||||
|
"Guest user \"{user_id}\" with device display name \
|
||||||
|
\"{device_display_name}\" registered on this server from IP {client}"
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
#[allow(clippy::collapsible_else_if)]
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.notice(&format!(
|
||||||
|
"Guest user \"{user_id}\" with no device display name registered on \
|
||||||
|
this server from IP {client}",
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_guest {
|
||||||
|
// Make the first user to register an administrator and disable first-run mode.
|
||||||
|
let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
|
||||||
|
|
||||||
|
// If the registering user was not the first and we're suspending users on
|
||||||
|
// register, suspend them.
|
||||||
|
if !was_first_user && services.config.suspend_on_register {
|
||||||
|
// Note that we can still do auto joins for suspended users
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.suspend_account(&user_id, &services.globals.server_user)
|
||||||
|
.await;
|
||||||
|
// And send an @room notice to the admin room, to prompt admins to review the
|
||||||
|
// new user and ideally unsuspend them if deemed appropriate.
|
||||||
|
if services.server.config.admin_room_notices {
|
||||||
|
services
|
||||||
|
.admin
|
||||||
|
.send_loud_message(RoomMessageEventContent::text_plain(format!(
|
||||||
|
"User {user_id} has been suspended as they are not the first user on \
|
||||||
|
this server. Please review and unsuspend them if appropriate."
|
||||||
|
)))
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.appservice_info.is_none()
|
||||||
|
&& !services.server.config.auto_join_rooms.is_empty()
|
||||||
|
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||||
|
{
|
||||||
|
for room in &services.server.config.auto_join_rooms {
|
||||||
|
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||||
|
error!(
|
||||||
|
"Failed to resolve room alias to room ID when attempting to auto join \
|
||||||
|
{room}, skipping"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
if !services
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.server_in_room(services.globals.server_name(), &room_id)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
warn!(
|
||||||
|
"Skipping room {room} to automatically join as we have never joined before."
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(room_server_name) = room.server_name() {
|
||||||
|
match join_room_by_id_helper(
|
||||||
|
&services,
|
||||||
|
&user_id,
|
||||||
|
&room_id,
|
||||||
|
Some("Automatically joining this room upon registration".to_owned()),
|
||||||
|
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||||
|
&body.appservice_info,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Err(e) => {
|
||||||
|
// don't return this error so we don't fail registrations
|
||||||
|
error!(
|
||||||
|
"Failed to automatically join room {room} for user {user_id}: {e}"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
| _ => {
|
||||||
|
info!("Automatically joined room {room} for user {user_id}");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(register::v3::Response {
|
||||||
|
access_token: token,
|
||||||
|
user_id,
|
||||||
|
device_id: device,
|
||||||
|
refresh_token: None,
|
||||||
|
expires_in: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine which flows and parameters should be presented when
|
||||||
|
/// registering a new account.
|
||||||
|
async fn create_registration_uiaa_session(
|
||||||
|
services: &Services,
|
||||||
|
) -> Result<(Vec<AuthFlow>, Box<RawValue>)> {
|
||||||
|
let mut params = HashMap::<String, serde_json::Value>::new();
|
||||||
|
|
||||||
|
let flows = if services.firstrun.is_first_run() {
|
||||||
|
// Registration token forced while in first-run mode
|
||||||
|
vec![AuthFlow::new(vec![AuthType::RegistrationToken])]
|
||||||
|
} else {
|
||||||
|
let mut flows = vec![];
|
||||||
|
|
||||||
|
if services
|
||||||
|
.registration_tokens
|
||||||
|
.iterate_tokens()
|
||||||
|
.next()
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
// Trusted registration flow with a token is available
|
||||||
|
let mut token_flow = AuthFlow::new(vec![AuthType::RegistrationToken]);
|
||||||
|
|
||||||
|
if let Some(smtp) = &services.config.smtp
|
||||||
|
&& smtp.require_email_for_token_registration
|
||||||
|
{
|
||||||
|
// Email is required for token registrations
|
||||||
|
token_flow.stages.push(AuthType::EmailIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
flows.push(token_flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut untrusted_flow = AuthFlow::default();
|
||||||
|
|
||||||
|
if services.config.recaptcha_private_site_key.is_some() {
|
||||||
|
if let Some(pubkey) = &services.config.recaptcha_site_key {
|
||||||
|
// ReCaptcha is configured for untrusted registrations
|
||||||
|
untrusted_flow.stages.push(AuthType::ReCaptcha);
|
||||||
|
|
||||||
|
params.insert(
|
||||||
|
AuthType::ReCaptcha.as_str().to_owned(),
|
||||||
|
serde_json::json!({
|
||||||
|
"public_key": pubkey,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(smtp) = &services.config.smtp
|
||||||
|
&& smtp.require_email_for_registration
|
||||||
|
{
|
||||||
|
// Email is required for untrusted registrations
|
||||||
|
untrusted_flow.stages.push(AuthType::EmailIdentity);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !untrusted_flow.stages.is_empty() {
|
||||||
|
flows.push(untrusted_flow);
|
||||||
|
}
|
||||||
|
|
||||||
|
if flows.is_empty() {
|
||||||
|
// No flows are configured. Bail out by default
|
||||||
|
// unless open registration was explicitly enabled.
|
||||||
|
if !services
|
||||||
|
.config
|
||||||
|
.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"This server is not accepting registrations at this time."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// We have open registration enabled (😧), provide a dummy flow
|
||||||
|
flows.push(AuthFlow::new(vec![AuthType::Dummy]));
|
||||||
|
}
|
||||||
|
|
||||||
|
flows
|
||||||
|
};
|
||||||
|
|
||||||
|
let params = serde_json::value::to_raw_value(¶ms).expect("params should be valid JSON");
|
||||||
|
|
||||||
|
Ok((flows, params))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn determine_registration_user_id(
|
||||||
|
services: &Services,
|
||||||
|
supplied_username: Option<String>,
|
||||||
|
is_guest: bool,
|
||||||
|
emergency_mode_enabled: bool,
|
||||||
|
) -> Result<OwnedUserId> {
|
||||||
|
if let Some(supplied_username) = supplied_username
|
||||||
|
&& !is_guest
|
||||||
|
{
|
||||||
|
// The user gets to pick their username. Do some validation to make sure it's
|
||||||
|
// acceptable.
|
||||||
|
|
||||||
|
// Don't allow registration with forbidden usernames.
|
||||||
|
if services
|
||||||
|
.globals
|
||||||
|
.forbidden_usernames()
|
||||||
|
.is_match(&supplied_username)
|
||||||
|
&& !emergency_mode_enabled
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden("Username is forbidden")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and validate the user ID
|
||||||
|
let user_id = match UserId::parse_with_server_name(
|
||||||
|
&supplied_username,
|
||||||
|
services.globals.server_name(),
|
||||||
|
) {
|
||||||
|
| Ok(user_id) => {
|
||||||
|
if let Err(e) = user_id.validate_strict() {
|
||||||
|
// Unless we are in emergency mode, we should follow synapse's behaviour on
|
||||||
|
// not allowing things like spaces and UTF-8 characters in usernames
|
||||||
|
if !emergency_mode_enabled {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {supplied_username} contains disallowed characters or \
|
||||||
|
spaces: {e}"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't allow registration with user IDs that aren't local
|
||||||
|
if !services.globals.user_is_local(&user_id) {
|
||||||
|
return Err!(Request(InvalidUsername(
|
||||||
|
"Username {supplied_username} is not local to this server"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
user_id
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
return Err!(Request(InvalidUsername(debug_warn!(
|
||||||
|
"Username {supplied_username} is not valid: {e}"
|
||||||
|
))));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if services.users.exists(&user_id).await {
|
||||||
|
return Err!(Request(UserInUse("User ID is not available.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(user_id)
|
||||||
|
} else {
|
||||||
|
// The user is a guest or didn't specify a username. Generate a username for
|
||||||
|
// them.
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let user_id = UserId::parse_with_server_name(
|
||||||
|
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
|
||||||
|
services.globals.server_name(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if !services.users.exists(&user_id).await {
|
||||||
|
break Ok(user_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/register/email/requestToken`
|
||||||
|
///
|
||||||
|
/// Requests a validation email for the purpose of registering a new account.
|
||||||
|
pub(crate) async fn request_registration_token_via_email_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<request_registration_token_via_email::v3::Request>,
|
||||||
|
) -> Result<request_registration_token_via_email::v3::Response> {
|
||||||
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if services
|
||||||
|
.threepid
|
||||||
|
.get_localpart_for_email(&email)
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = services
|
||||||
|
.threepid
|
||||||
|
.send_validation_email(
|
||||||
|
Mailbox::new(None, email),
|
||||||
|
|verification_link| messages::NewAccount {
|
||||||
|
server_name: services.config.server_name.as_ref(),
|
||||||
|
verification_link,
|
||||||
|
},
|
||||||
|
&body.client_secret,
|
||||||
|
body.send_attempt.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(request_registration_token_via_email::v3::Response::new(session))
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
use std::time::SystemTime;
|
||||||
|
|
||||||
|
use axum::extract::State;
|
||||||
|
use conduwuit::{Err, Result, err};
|
||||||
|
use lettre::{Address, message::Mailbox};
|
||||||
|
use ruma::{
|
||||||
|
MilliSecondsSinceUnixEpoch,
|
||||||
|
api::client::account::{
|
||||||
|
ThirdPartyIdRemovalStatus, add_3pid, delete_3pid, get_3pids,
|
||||||
|
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
|
||||||
|
},
|
||||||
|
thirdparty::{Medium, ThirdPartyIdentifierInit},
|
||||||
|
};
|
||||||
|
use service::{mailer::messages, uiaa::Identity};
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
/// # `GET _matrix/client/v3/account/3pid`
|
||||||
|
///
|
||||||
|
/// Get a list of third party identifiers associated with this account.
|
||||||
|
pub(crate) async fn third_party_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<get_3pids::v3::Request>,
|
||||||
|
) -> Result<get_3pids::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
let mut threepids = vec![];
|
||||||
|
|
||||||
|
if let Some(email) = services
|
||||||
|
.threepid
|
||||||
|
.get_email_for_localpart(sender_user.localpart())
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
threepids.push(
|
||||||
|
ThirdPartyIdentifierInit {
|
||||||
|
address: email.to_string(),
|
||||||
|
medium: Medium::Email,
|
||||||
|
// We don't currently track these, and they aren't used for much
|
||||||
|
validated_at: MilliSecondsSinceUnixEpoch::now(),
|
||||||
|
added_at: MilliSecondsSinceUnixEpoch::from_system_time(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(get_3pids::v3::Response::new(threepids))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/email/requestToken`
|
||||||
|
///
|
||||||
|
/// Requests a validation email for the purpose of changing an account's email.
|
||||||
|
pub(crate) async fn request_3pid_management_token_via_email_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||||
|
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||||
|
let Ok(email) = Address::try_from(body.email.clone()) else {
|
||||||
|
return Err!(Request(InvalidParam("Invalid email address.")));
|
||||||
|
};
|
||||||
|
|
||||||
|
if services
|
||||||
|
.threepid
|
||||||
|
.get_localpart_for_email(&email)
|
||||||
|
.await
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
|
return Err!(Request(ThreepidInUse("This email address is already in use.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = services
|
||||||
|
.threepid
|
||||||
|
.send_validation_email(
|
||||||
|
Mailbox::new(None, email),
|
||||||
|
|verification_link| messages::ChangeEmail {
|
||||||
|
server_name: services.config.server_name.as_str(),
|
||||||
|
user_id: body.sender_user.as_deref(),
|
||||||
|
verification_link,
|
||||||
|
},
|
||||||
|
&body.client_secret,
|
||||||
|
body.send_attempt.try_into().unwrap(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(request_3pid_management_token_via_email::v3::Response::new(session))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
|
||||||
|
///
|
||||||
|
/// "This API should be used to request validation tokens when adding an email
|
||||||
|
/// address to an account"
|
||||||
|
///
|
||||||
|
/// - 403 signals that The homeserver does not allow the third party identifier
|
||||||
|
/// as a contact option.
|
||||||
|
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
|
||||||
|
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
|
||||||
|
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
|
||||||
|
Err!(Request(ThreepidMediumNotSupported(
|
||||||
|
"MSISDN third-party identifiers are not supported."
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/add`
|
||||||
|
pub(crate) async fn add_3pid_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<add_3pid::v3::Request>,
|
||||||
|
) -> Result<add_3pid::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
// Require password auth to add an email
|
||||||
|
let _ = services
|
||||||
|
.uiaa
|
||||||
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let email = services
|
||||||
|
.threepid
|
||||||
|
.consume_valid_session(&body.sid, &body.client_secret)
|
||||||
|
.await
|
||||||
|
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
|
||||||
|
|
||||||
|
services
|
||||||
|
.threepid
|
||||||
|
.associate_localpart_email(sender_user.localpart(), &email)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(add_3pid::v3::Response::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `POST /_matrix/client/v3/account/3pid/delete`
|
||||||
|
pub(crate) async fn delete_3pid_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
body: Ruma<delete_3pid::v3::Request>,
|
||||||
|
) -> Result<delete_3pid::v3::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
if body.medium != Medium::Email {
|
||||||
|
return Ok(delete_3pid::v3::Response {
|
||||||
|
id_server_unbind_result: ThirdPartyIdRemovalStatus::NoSupport,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if services
|
||||||
|
.threepid
|
||||||
|
.disassociate_localpart_email(sender_user.localpart())
|
||||||
|
.await
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
return Err!(Request(ThreepidNotFound("Your account has no associated email.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(delete_3pid::v3::Response {
|
||||||
|
id_server_unbind_result: ThirdPartyIdRemovalStatus::Success,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -30,8 +30,10 @@ pub(crate) async fn get_capabilities_route(
|
|||||||
default: services.server.config.default_room_version.clone(),
|
default: services.server.config.default_room_version.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// we do not implement 3PID stuff
|
// Only allow 3pid changes if SMTP is configured
|
||||||
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability { enabled: false };
|
capabilities.thirdparty_id_changes = ThirdPartyIdChangesCapability {
|
||||||
|
enabled: services.mailer.mailer().is_some(),
|
||||||
|
};
|
||||||
|
|
||||||
capabilities.get_login_token = GetLoginTokenCapability {
|
capabilities.get_login_token = GetLoginTokenCapability {
|
||||||
enabled: services.server.config.login_via_existing_session,
|
enabled: services.server.config.login_via_existing_session,
|
||||||
@@ -51,7 +53,7 @@ pub(crate) async fn get_capabilities_route(
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
// Advertise suspension API
|
// Advertise suspension API
|
||||||
capabilities.set("uk.timedout.msc4323", json!({"suspend":true, "lock": false}))?;
|
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(get_capabilities::v3::Response { capabilities })
|
Ok(get_capabilities::v3::Response { capabilities })
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
use axum::extract::State;
|
||||||
|
use axum_client_ip::InsecureClientIp;
|
||||||
|
use conduwuit::{Err, Result, at};
|
||||||
|
use futures::StreamExt;
|
||||||
|
use ruma::api::client::dehydrated_device::{
|
||||||
|
delete_dehydrated_device::unstable as delete_dehydrated_device,
|
||||||
|
get_dehydrated_device::unstable as get_dehydrated_device, get_events::unstable as get_events,
|
||||||
|
put_dehydrated_device::unstable as put_dehydrated_device,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::Ruma;
|
||||||
|
|
||||||
|
const MAX_BATCH_EVENTS: usize = 50;
|
||||||
|
|
||||||
|
/// # `PUT /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Creates or overwrites the user's dehydrated device.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn put_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<put_dehydrated_device::Request>,
|
||||||
|
) -> Result<put_dehydrated_device::Response> {
|
||||||
|
let sender_user = body
|
||||||
|
.sender_user
|
||||||
|
.as_deref()
|
||||||
|
.expect("AccessToken authentication required");
|
||||||
|
|
||||||
|
let device_id = body.body.device_id.clone();
|
||||||
|
|
||||||
|
services
|
||||||
|
.users
|
||||||
|
.set_dehydrated_device(sender_user, body.body)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(put_dehydrated_device::Response { device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `DELETE /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Deletes the user's dehydrated device without replacement.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn delete_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<delete_dehydrated_device::Request>,
|
||||||
|
) -> Result<delete_dehydrated_device::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
|
||||||
|
|
||||||
|
services.users.remove_device(sender_user, &device_id).await;
|
||||||
|
|
||||||
|
Ok(delete_dehydrated_device::Response { device_id })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/../dehydrated_device`
|
||||||
|
///
|
||||||
|
/// Gets the user's dehydrated device
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn get_dehydrated_device_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<get_dehydrated_device::Request>,
|
||||||
|
) -> Result<get_dehydrated_device::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device = services.users.get_dehydrated_device(sender_user).await?;
|
||||||
|
|
||||||
|
Ok(get_dehydrated_device::Response {
|
||||||
|
device_id: device.device_id,
|
||||||
|
device_data: device.device_data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
|
||||||
|
///
|
||||||
|
/// Paginates the events of the dehydrated device.
|
||||||
|
#[tracing::instrument(skip_all, fields(%client))]
|
||||||
|
pub(crate) async fn get_dehydrated_events_route(
|
||||||
|
State(services): State<crate::State>,
|
||||||
|
InsecureClientIp(client): InsecureClientIp,
|
||||||
|
body: Ruma<get_events::Request>,
|
||||||
|
) -> Result<get_events::Response> {
|
||||||
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
|
let device_id = &body.body.device_id;
|
||||||
|
let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
|
||||||
|
|
||||||
|
if existing_id.as_ref().is_err()
|
||||||
|
|| existing_id
|
||||||
|
.as_ref()
|
||||||
|
.is_ok_and(|existing_id| existing_id != device_id)
|
||||||
|
{
|
||||||
|
return Err!(Request(Forbidden("Not the dehydrated device_id.")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let since: Option<u64> = body
|
||||||
|
.body
|
||||||
|
.next_batch
|
||||||
|
.as_deref()
|
||||||
|
.map(str::parse)
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
let mut next_batch: Option<u64> = None;
|
||||||
|
let events = services
|
||||||
|
.users
|
||||||
|
.get_to_device_events(sender_user, device_id, since, None)
|
||||||
|
.take(MAX_BATCH_EVENTS)
|
||||||
|
.inspect(|&(count, _)| {
|
||||||
|
next_batch.replace(count);
|
||||||
|
})
|
||||||
|
.map(at!(1))
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
|
||||||
|
Ok(get_events::Response {
|
||||||
|
events,
|
||||||
|
next_batch: next_batch.as_ref().map(ToString::to_string),
|
||||||
|
})
|
||||||
|
}
|
||||||
+16
-78
@@ -1,17 +1,15 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{Err, Error, Result, debug, err, utils};
|
use conduwuit::{Err, Result, debug, err, utils};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
|
||||||
api::client::{
|
api::client::device::{
|
||||||
device::{self, delete_device, delete_devices, get_device, get_devices, update_device},
|
self, delete_device, delete_devices, get_device, get_devices, update_device,
|
||||||
error::ErrorKind,
|
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use super::SESSION_ID_LENGTH;
|
|
||||||
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
use crate::{Ruma, client::DEVICE_ID_LENGTH};
|
||||||
|
|
||||||
/// # `GET /_matrix/client/r0/devices`
|
/// # `GET /_matrix/client/r0/devices`
|
||||||
@@ -123,7 +121,7 @@ pub(crate) async fn delete_device_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<delete_device::v3::Request>,
|
body: Ruma<delete_device::v3::Request>,
|
||||||
) -> Result<delete_device::v3::Response> {
|
) -> Result<delete_device::v3::Response> {
|
||||||
let (sender_user, sender_device) = body.sender();
|
let sender_user = body.sender_user();
|
||||||
let appservice = body.appservice_info.as_ref();
|
let appservice = body.appservice_info.as_ref();
|
||||||
|
|
||||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||||
@@ -139,41 +137,11 @@ pub(crate) async fn delete_device_route(
|
|||||||
return Ok(delete_device::v3::Response {});
|
return Ok(delete_device::v3::Response {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let _ = services
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
.uiaa
|
||||||
completed: Vec::new(),
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
params: Box::default(),
|
.await?;
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err!(Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body {
|
|
||||||
| Some(ref json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, sender_device, &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err!(Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(NotJson("Not json.")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
@@ -200,7 +168,7 @@ pub(crate) async fn delete_devices_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<delete_devices::v3::Request>,
|
body: Ruma<delete_devices::v3::Request>,
|
||||||
) -> Result<delete_devices::v3::Response> {
|
) -> Result<delete_devices::v3::Response> {
|
||||||
let (sender_user, sender_device) = body.sender();
|
let sender_user = body.sender_user();
|
||||||
let appservice = body.appservice_info.as_ref();
|
let appservice = body.appservice_info.as_ref();
|
||||||
|
|
||||||
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
if appservice.is_some_and(|appservice| appservice.registration.device_management) {
|
||||||
@@ -215,41 +183,11 @@ pub(crate) async fn delete_devices_route(
|
|||||||
return Ok(delete_devices::v3::Response {});
|
return Ok(delete_devices::v3::Response {});
|
||||||
}
|
}
|
||||||
|
|
||||||
// UIAA
|
// Prompt the user to confirm with their password using UIAA
|
||||||
let mut uiaainfo = UiaaInfo {
|
let _ = services
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
.uiaa
|
||||||
completed: Vec::new(),
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
params: Box::default(),
|
.await?;
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body {
|
|
||||||
| Some(ref json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, sender_device, &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for device_id in &body.devices {
|
for device_id in &body.devices {
|
||||||
services.users.remove_device(sender_user, device_id).await;
|
services.users.remove_device(sender_user, device_id).await;
|
||||||
|
|||||||
+6
-39
@@ -7,7 +7,6 @@ use axum::extract::State;
|
|||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, Result, debug, debug_warn, err,
|
Err, Error, Result, debug, debug_warn, err,
|
||||||
result::NotFound,
|
result::NotFound,
|
||||||
utils,
|
|
||||||
utils::{IterStream, stream::WidebandExt},
|
utils::{IterStream, stream::WidebandExt},
|
||||||
};
|
};
|
||||||
use conduwuit_service::{Services, users::parse_master_key};
|
use conduwuit_service::{Services, users::parse_master_key};
|
||||||
@@ -22,7 +21,6 @@ use ruma::{
|
|||||||
upload_signatures::{self},
|
upload_signatures::{self},
|
||||||
upload_signing_keys,
|
upload_signing_keys,
|
||||||
},
|
},
|
||||||
uiaa::{AuthFlow, AuthType, UiaaInfo},
|
|
||||||
},
|
},
|
||||||
federation,
|
federation,
|
||||||
},
|
},
|
||||||
@@ -30,8 +28,8 @@ use ruma::{
|
|||||||
serde::Raw,
|
serde::Raw,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use super::SESSION_ID_LENGTH;
|
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/r0/keys/upload`
|
/// # `POST /_matrix/client/r0/keys/upload`
|
||||||
@@ -174,16 +172,7 @@ pub(crate) async fn upload_signing_keys_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<upload_signing_keys::v3::Request>,
|
body: Ruma<upload_signing_keys::v3::Request>,
|
||||||
) -> Result<upload_signing_keys::v3::Response> {
|
) -> Result<upload_signing_keys::v3::Response> {
|
||||||
let (sender_user, sender_device) = body.sender();
|
let sender_user = body.sender_user();
|
||||||
|
|
||||||
// UIAA
|
|
||||||
let mut uiaainfo = UiaaInfo {
|
|
||||||
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
|
|
||||||
completed: Vec::new(),
|
|
||||||
params: Box::default(),
|
|
||||||
session: None,
|
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match check_for_new_keys(
|
match check_for_new_keys(
|
||||||
services,
|
services,
|
||||||
@@ -207,32 +196,10 @@ pub(crate) async fn upload_signing_keys_route(
|
|||||||
// Some of the keys weren't found, so we let them upload
|
// Some of the keys weren't found, so we let them upload
|
||||||
},
|
},
|
||||||
| _ => {
|
| _ => {
|
||||||
match &body.auth {
|
let _ = services
|
||||||
| Some(auth) => {
|
.uiaa
|
||||||
let (worked, uiaainfo) = services
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
.uiaa
|
.await?;
|
||||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body.as_ref() {
|
|
||||||
| Some(json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, sender_device, &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub(super) mod appservice;
|
|||||||
pub(super) mod backup;
|
pub(super) mod backup;
|
||||||
pub(super) mod capabilities;
|
pub(super) mod capabilities;
|
||||||
pub(super) mod context;
|
pub(super) mod context;
|
||||||
|
pub(super) mod dehydrated_device;
|
||||||
pub(super) mod device;
|
pub(super) mod device;
|
||||||
pub(super) mod directory;
|
pub(super) mod directory;
|
||||||
pub(super) mod filter;
|
pub(super) mod filter;
|
||||||
@@ -49,6 +50,7 @@ pub(super) use appservice::*;
|
|||||||
pub(super) use backup::*;
|
pub(super) use backup::*;
|
||||||
pub(super) use capabilities::*;
|
pub(super) use capabilities::*;
|
||||||
pub(super) use context::*;
|
pub(super) use context::*;
|
||||||
|
pub(super) use dehydrated_device::*;
|
||||||
pub(super) use device::*;
|
pub(super) use device::*;
|
||||||
pub(super) use directory::*;
|
pub(super) use directory::*;
|
||||||
pub(super) use filter::*;
|
pub(super) use filter::*;
|
||||||
@@ -90,6 +92,3 @@ const DEVICE_ID_LENGTH: usize = 10;
|
|||||||
|
|
||||||
/// generated user access token length
|
/// generated user access token length
|
||||||
const TOKEN_LENGTH: usize = 32;
|
const TOKEN_LENGTH: usize = 32;
|
||||||
|
|
||||||
/// generated user session ID length
|
|
||||||
const SESSION_ID_LENGTH: usize = service::uiaa::SESSION_ID_LENGTH;
|
|
||||||
|
|||||||
+38
-58
@@ -8,8 +8,9 @@ use conduwuit::{
|
|||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use conduwuit_core::{debug_error, debug_warn};
|
use conduwuit_core::{debug_error, debug_warn};
|
||||||
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
|
use conduwuit_service::Services;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
|
use lettre::Address;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedUserId, UserId,
|
OwnedUserId, UserId,
|
||||||
api::client::{
|
api::client::{
|
||||||
@@ -26,9 +27,10 @@ use ruma::{
|
|||||||
},
|
},
|
||||||
logout, logout_all,
|
logout, logout_all,
|
||||||
},
|
},
|
||||||
uiaa,
|
uiaa::UserIdentifier,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use service::uiaa::Identity;
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
@@ -80,7 +82,7 @@ pub(crate) async fn password_login(
|
|||||||
.password_hash(lowercased_user_id)
|
.password_hash(lowercased_user_id)
|
||||||
.await
|
.await
|
||||||
.map(|hash| (hash, lowercased_user_id))
|
.map(|hash| (hash, lowercased_user_id))
|
||||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?,
|
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?,
|
||||||
};
|
};
|
||||||
|
|
||||||
if hash.is_empty() {
|
if hash.is_empty() {
|
||||||
@@ -89,7 +91,7 @@ pub(crate) async fn password_login(
|
|||||||
|
|
||||||
hash::verify_password(password, &hash)
|
hash::verify_password(password, &hash)
|
||||||
.inspect_err(|e| debug_error!("{e}"))
|
.inspect_err(|e| debug_error!("{e}"))
|
||||||
.map_err(|_| err!(Request(Forbidden("Wrong username or password."))))?;
|
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
|
||||||
|
|
||||||
Ok(user_id.to_owned())
|
Ok(user_id.to_owned())
|
||||||
}
|
}
|
||||||
@@ -161,28 +163,38 @@ pub(super) async fn ldap_login(
|
|||||||
|
|
||||||
pub(crate) async fn handle_login(
|
pub(crate) async fn handle_login(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
body: &Ruma<login::v3::Request>,
|
identifier: Option<&UserIdentifier>,
|
||||||
identifier: Option<&uiaa::UserIdentifier>,
|
|
||||||
password: &str,
|
password: &str,
|
||||||
user: Option<&String>,
|
user: Option<&String>,
|
||||||
) -> Result<OwnedUserId> {
|
) -> Result<OwnedUserId> {
|
||||||
debug!("Got password login type");
|
debug!("Got password login type");
|
||||||
|
let user_id_or_localpart = match (identifier, user) {
|
||||||
|
| (Some(UserIdentifier::UserIdOrLocalpart(localpart)), _) => localpart,
|
||||||
|
| (Some(UserIdentifier::Email { address }), _) => {
|
||||||
|
let email = Address::try_from(address.to_owned())
|
||||||
|
.map_err(|_| err!(Request(InvalidParam("Email is malformed"))))?;
|
||||||
|
|
||||||
|
&services
|
||||||
|
.threepid
|
||||||
|
.get_localpart_for_email(&email)
|
||||||
|
.await
|
||||||
|
.ok_or_else(|| err!(Request(Forbidden("Invalid identifier or password"))))?
|
||||||
|
},
|
||||||
|
| (None, Some(user)) => user,
|
||||||
|
| _ => {
|
||||||
|
return Err!(Request(InvalidParam("Identifier type not recognized")));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
|
||||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||||
} 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(
|
let lowercased_user_id = UserId::parse_with_server_name(
|
||||||
user_id.localpart().to_lowercase(),
|
user_id.localpart().to_lowercase(),
|
||||||
&services.config.server_name,
|
&services.config.server_name,
|
||||||
)?;
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
if !services.globals.user_is_local(&user_id)
|
if !services.globals.user_is_local(&user_id)
|
||||||
|| !services.globals.user_is_local(&lowercased_user_id)
|
|| !services.globals.user_is_local(&lowercased_user_id)
|
||||||
@@ -244,7 +256,7 @@ pub(crate) async fn login_route(
|
|||||||
password,
|
password,
|
||||||
user,
|
user,
|
||||||
..
|
..
|
||||||
}) => handle_login(&services, &body, identifier.as_ref(), password, user.as_ref()).await?,
|
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
|
||||||
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
| login::v3::LoginInfo::Token(login::v3::Token { token }) => {
|
||||||
debug!("Got token login type");
|
debug!("Got token login type");
|
||||||
if !services.server.config.login_via_existing_session {
|
if !services.server.config.login_via_existing_session {
|
||||||
@@ -264,7 +276,7 @@ pub(crate) async fn login_route(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let user_id =
|
let user_id =
|
||||||
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
if let Some(UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
|
||||||
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
UserId::parse_with_server_name(user_id, &services.config.server_name)
|
||||||
} else if let Some(user) = user {
|
} else if let Some(user) = user {
|
||||||
UserId::parse_with_server_name(user, &services.config.server_name)
|
UserId::parse_with_server_name(user, &services.config.server_name)
|
||||||
@@ -273,7 +285,7 @@ pub(crate) async fn login_route(
|
|||||||
debug_warn!(?body.login_info, "Valid identifier or username was not provided (invalid or unsupported login type?)")
|
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}")))))?;
|
.map_err(|_| err!(Request(InvalidUsername(warn!("User ID is malformed")))))?;
|
||||||
|
|
||||||
if !services.globals.user_is_local(&user_id) {
|
if !services.globals.user_is_local(&user_id) {
|
||||||
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
|
||||||
@@ -370,45 +382,13 @@ pub(crate) async fn login_token_route(
|
|||||||
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// This route SHOULD have UIA
|
let sender_user = body.sender_user();
|
||||||
// TODO: How do we make only UIA sessions that have not been used before valid?
|
|
||||||
let (sender_user, sender_device) = body.sender();
|
|
||||||
|
|
||||||
let mut uiaainfo = uiaa::UiaaInfo {
|
// Prompt the user to confirm with their password using UIAA
|
||||||
flows: vec![uiaa::AuthFlow { stages: vec![uiaa::AuthType::Password] }],
|
let _ = services
|
||||||
completed: Vec::new(),
|
.uiaa
|
||||||
params: Box::default(),
|
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
|
||||||
session: None,
|
.await?;
|
||||||
auth_error: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match &body.auth {
|
|
||||||
| Some(auth) => {
|
|
||||||
let (worked, uiaainfo) = services
|
|
||||||
.uiaa
|
|
||||||
.try_auth(sender_user, sender_device, auth, &uiaainfo)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !worked {
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Success!
|
|
||||||
},
|
|
||||||
| _ => match body.json_body.as_ref() {
|
|
||||||
| Some(json) => {
|
|
||||||
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.create(sender_user, sender_device, &uiaainfo, json);
|
|
||||||
|
|
||||||
return Err(Error::Uiaa(uiaainfo));
|
|
||||||
},
|
|
||||||
| _ => {
|
|
||||||
return Err!(Request(NotJson("No JSON body was sent when required.")));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
let login_token = utils::random_string(TOKEN_LENGTH);
|
||||||
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ async fn build_state_and_timeline(
|
|||||||
// joined since the last sync, that being the syncing user's join event. if
|
// joined since the last sync, that being the syncing user's join event. if
|
||||||
// it's empty something is wrong.
|
// it's empty something is wrong.
|
||||||
if joined_since_last_sync && timeline.pdus.is_empty() {
|
if joined_since_last_sync && timeline.pdus.is_empty() {
|
||||||
warn!("timeline for newly joined room is empty");
|
debug_warn!("timeline for newly joined room is empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
let (summary, device_list_updates) = try_join(
|
let (summary, device_list_updates) = try_join(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Event, PduCount, PduEvent, Result, at, debug_warn,
|
Event, PduEvent, Result, at, debug_warn,
|
||||||
pdu::EventHash,
|
pdu::EventHash,
|
||||||
trace,
|
trace,
|
||||||
utils::{self, IterStream, future::ReadyEqExt, stream::WidebandExt as _},
|
utils::{self, IterStream, future::ReadyEqExt, stream::WidebandExt as _},
|
||||||
@@ -68,9 +68,13 @@ pub(super) async fn load_left_room(
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// return early if this is an incremental sync, and we've already synced this
|
// return early if:
|
||||||
// leave to the user, and `include_leave` isn't set on the filter.
|
// - this is an initial sync and the room filter doesn't include leaves, or
|
||||||
if !filter.room.include_leave && last_sync_end_count >= Some(left_count) {
|
// - this is an incremental sync, and we've already synced the leave, and the
|
||||||
|
// room filter doesn't include leaves
|
||||||
|
if last_sync_end_count.is_none_or(|last_sync_end_count| last_sync_end_count >= left_count)
|
||||||
|
&& !filter.room.include_leave
|
||||||
|
{
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,27 +199,13 @@ async fn build_left_state_and_timeline(
|
|||||||
leave_shortstatehash: ShortStateHash,
|
leave_shortstatehash: ShortStateHash,
|
||||||
prev_membership_event: PduEvent,
|
prev_membership_event: PduEvent,
|
||||||
) -> Result<(TimelinePdus, Vec<PduEvent>)> {
|
) -> Result<(TimelinePdus, Vec<PduEvent>)> {
|
||||||
let SyncContext {
|
let SyncContext { syncing_user, filter, .. } = sync_context;
|
||||||
syncing_user,
|
|
||||||
last_sync_end_count,
|
|
||||||
filter,
|
|
||||||
..
|
|
||||||
} = sync_context;
|
|
||||||
|
|
||||||
let timeline_start_count = if let Some(last_sync_end_count) = last_sync_end_count {
|
let timeline_start_count = services
|
||||||
// for incremental syncs, start the timeline after `since`
|
.rooms
|
||||||
PduCount::Normal(last_sync_end_count)
|
.timeline
|
||||||
} else {
|
.get_pdu_count(&prev_membership_event.event_id)
|
||||||
// for initial syncs, start the timeline after the previous membership
|
.await?;
|
||||||
// event. we don't want to include the membership event itself
|
|
||||||
// because clients get confused when they see a `join`
|
|
||||||
// membership event in a `leave` room.
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.timeline
|
|
||||||
.get_pdu_count(&prev_membership_event.event_id)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
|
|
||||||
// end the timeline at the user's leave event
|
// end the timeline at the user's leave event
|
||||||
let timeline_end_count = services
|
let timeline_end_count = services
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use std::{
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::InsecureClientIp;
|
use axum_client_ip::InsecureClientIp;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Result, extract_variant,
|
Result, at, extract_variant,
|
||||||
utils::{
|
utils::{
|
||||||
ReadyExt, TryFutureExtExt,
|
ReadyExt, TryFutureExtExt,
|
||||||
stream::{BroadbandExt, Tools, WidebandExt},
|
stream::{BroadbandExt, Tools, WidebandExt},
|
||||||
@@ -297,12 +297,18 @@ pub(crate) async fn build_sync_events(
|
|||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
.rooms_left(syncing_user)
|
.rooms_left(syncing_user)
|
||||||
.broad_filter_map(|(room_id, leave_pdu)| {
|
.broad_filter_map(|(room_id, leave_pdu)| async {
|
||||||
load_left_room(services, context, room_id.clone(), leave_pdu)
|
let left_room = load_left_room(services, context, room_id.clone(), leave_pdu).await;
|
||||||
.map_ok(move |left_room| (room_id, left_room))
|
|
||||||
.ok()
|
match left_room {
|
||||||
|
| Ok(Some(left_room)) => Some((room_id, left_room)),
|
||||||
|
| Ok(None) => None,
|
||||||
|
| Err(err) => {
|
||||||
|
warn!(?err, %room_id, "error loading joined room");
|
||||||
|
None
|
||||||
|
},
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.ready_filter_map(|(room_id, left_room)| left_room.map(|left_room| (room_id, left_room)))
|
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let invited_rooms = services
|
let invited_rooms = services
|
||||||
@@ -385,6 +391,7 @@ pub(crate) async fn build_sync_events(
|
|||||||
last_sync_end_count,
|
last_sync_end_count,
|
||||||
Some(current_count),
|
Some(current_count),
|
||||||
)
|
)
|
||||||
|
.map(at!(1))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let device_one_time_keys_count = services
|
let device_one_time_keys_count = services
|
||||||
|
|||||||
@@ -1029,6 +1029,7 @@ async fn collect_to_device(
|
|||||||
events: services
|
events: services
|
||||||
.users
|
.users
|
||||||
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
|
.get_to_device_events(sender_user, sender_device, None, Some(next_batch))
|
||||||
|
.map(at!(1))
|
||||||
.collect()
|
.collect()
|
||||||
.await,
|
.await,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ pub(crate) async fn get_supported_versions_route(
|
|||||||
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
("org.matrix.msc2836".to_owned(), true), /* threading/threads (https://github.com/matrix-org/matrix-spec-proposals/pull/2836) */
|
||||||
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
("org.matrix.msc2946".to_owned(), true), /* spaces/hierarchy summaries (https://github.com/matrix-org/matrix-spec-proposals/pull/2946) */
|
||||||
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
("org.matrix.msc3026.busy_presence".to_owned(), true), /* busy presence status (https://github.com/matrix-org/matrix-spec-proposals/pull/3026) */
|
||||||
|
("org.matrix.msc3814".to_owned(), true), /* dehydrated devices */
|
||||||
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
("org.matrix.msc3827".to_owned(), true), /* filtering of /publicRooms by room type (https://github.com/matrix-org/matrix-spec-proposals/pull/3827) */
|
||||||
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
("org.matrix.msc3952_intentional_mentions".to_owned(), true), /* intentional mentions (https://github.com/matrix-org/matrix-spec-proposals/pull/3952) */
|
||||||
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
("org.matrix.msc3916.stable".to_owned(), true), /* authenticated media (https://github.com/matrix-org/matrix-spec-proposals/pull/3916) */
|
||||||
|
|||||||
+12
-4
@@ -28,7 +28,8 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::appservice_ping)
|
.ruma_route(&client::appservice_ping)
|
||||||
.ruma_route(&client::get_supported_versions_route)
|
.ruma_route(&client::get_supported_versions_route)
|
||||||
.ruma_route(&client::get_register_available_route)
|
.ruma_route(&client::get_register_available_route)
|
||||||
.ruma_route(&client::register_route)
|
.ruma_route(&client::register::register_route)
|
||||||
|
.ruma_route(&client::register::request_registration_token_via_email_route)
|
||||||
.ruma_route(&client::get_login_types_route)
|
.ruma_route(&client::get_login_types_route)
|
||||||
.ruma_route(&client::login_route)
|
.ruma_route(&client::login_route)
|
||||||
.ruma_route(&client::login_token_route)
|
.ruma_route(&client::login_token_route)
|
||||||
@@ -36,10 +37,13 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::logout_route)
|
.ruma_route(&client::logout_route)
|
||||||
.ruma_route(&client::logout_all_route)
|
.ruma_route(&client::logout_all_route)
|
||||||
.ruma_route(&client::change_password_route)
|
.ruma_route(&client::change_password_route)
|
||||||
|
.ruma_route(&client::request_password_change_token_via_email_route)
|
||||||
.ruma_route(&client::deactivate_route)
|
.ruma_route(&client::deactivate_route)
|
||||||
.ruma_route(&client::third_party_route)
|
.ruma_route(&client::threepid::third_party_route)
|
||||||
.ruma_route(&client::request_3pid_management_token_via_email_route)
|
.ruma_route(&client::threepid::request_3pid_management_token_via_email_route)
|
||||||
.ruma_route(&client::request_3pid_management_token_via_msisdn_route)
|
.ruma_route(&client::threepid::request_3pid_management_token_via_msisdn_route)
|
||||||
|
.ruma_route(&client::threepid::add_3pid_route)
|
||||||
|
.ruma_route(&client::threepid::delete_3pid_route)
|
||||||
.ruma_route(&client::check_registration_token_validity)
|
.ruma_route(&client::check_registration_token_validity)
|
||||||
.ruma_route(&client::get_capabilities_route)
|
.ruma_route(&client::get_capabilities_route)
|
||||||
.ruma_route(&client::get_pushrules_all_route)
|
.ruma_route(&client::get_pushrules_all_route)
|
||||||
@@ -160,6 +164,10 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
|||||||
.ruma_route(&client::update_device_route)
|
.ruma_route(&client::update_device_route)
|
||||||
.ruma_route(&client::delete_device_route)
|
.ruma_route(&client::delete_device_route)
|
||||||
.ruma_route(&client::delete_devices_route)
|
.ruma_route(&client::delete_devices_route)
|
||||||
|
.ruma_route(&client::put_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::delete_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::get_dehydrated_device_route)
|
||||||
|
.ruma_route(&client::get_dehydrated_events_route)
|
||||||
.ruma_route(&client::get_tags_route)
|
.ruma_route(&client::get_tags_route)
|
||||||
.ruma_route(&client::update_tag_route)
|
.ruma_route(&client::update_tag_route)
|
||||||
.ruma_route(&client::delete_tag_route)
|
.ruma_route(&client::delete_tag_route)
|
||||||
|
|||||||
+6
-39
@@ -2,14 +2,13 @@ use std::{mem, ops::Deref};
|
|||||||
|
|
||||||
use axum::{body::Body, extract::FromRequest};
|
use axum::{body::Body, extract::FromRequest};
|
||||||
use bytes::{BufMut, Bytes, BytesMut};
|
use bytes::{BufMut, Bytes, BytesMut};
|
||||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
|
use conduwuit::{Error, Result, debug, debug_warn, err, trace};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
CanonicalJsonObject, CanonicalJsonValue, DeviceId, OwnedDeviceId, OwnedServerName,
|
||||||
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
OwnedUserId, ServerName, UserId, api::IncomingRequest,
|
||||||
};
|
};
|
||||||
use service::Services;
|
|
||||||
|
|
||||||
use super::{auth, auth::Auth, request, request::Request};
|
use super::{auth, request, request::Request};
|
||||||
use crate::{State, service::appservice::RegistrationInfo};
|
use crate::{State, service::appservice::RegistrationInfo};
|
||||||
|
|
||||||
/// Extractor for Ruma request structs
|
/// Extractor for Ruma request structs
|
||||||
@@ -108,7 +107,7 @@ where
|
|||||||
}
|
}
|
||||||
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
|
let auth = auth::auth(services, &mut request, json_body.as_ref(), &T::METADATA).await?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
body: make_body::<T>(services, &mut request, json_body.as_mut(), &auth)?,
|
body: make_body::<T>(&mut request, json_body.as_mut())?,
|
||||||
origin: auth.origin,
|
origin: auth.origin,
|
||||||
sender_user: auth.sender_user,
|
sender_user: auth.sender_user,
|
||||||
sender_device: auth.sender_device,
|
sender_device: auth.sender_device,
|
||||||
@@ -118,16 +117,11 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_body<T>(
|
fn make_body<T>(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Result<T>
|
||||||
services: &Services,
|
|
||||||
request: &mut Request,
|
|
||||||
json_body: Option<&mut CanonicalJsonValue>,
|
|
||||||
auth: &Auth,
|
|
||||||
) -> Result<T>
|
|
||||||
where
|
where
|
||||||
T: IncomingRequest,
|
T: IncomingRequest,
|
||||||
{
|
{
|
||||||
let body = take_body(services, request, json_body, auth);
|
let body = take_body(request, json_body);
|
||||||
let http_request = into_http_request(request, body);
|
let http_request = into_http_request(request, body);
|
||||||
T::try_from_http_request(http_request, &request.path)
|
T::try_from_http_request(http_request, &request.path)
|
||||||
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))
|
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))
|
||||||
@@ -151,38 +145,11 @@ fn into_http_request(request: &Request, body: Bytes) -> hyper::Request<Bytes> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::needless_pass_by_value)]
|
#[allow(clippy::needless_pass_by_value)]
|
||||||
fn take_body(
|
fn take_body(request: &mut Request, json_body: Option<&mut CanonicalJsonValue>) -> Bytes {
|
||||||
services: &Services,
|
|
||||||
request: &mut Request,
|
|
||||||
json_body: Option<&mut CanonicalJsonValue>,
|
|
||||||
auth: &Auth,
|
|
||||||
) -> Bytes {
|
|
||||||
let Some(CanonicalJsonValue::Object(json_body)) = json_body else {
|
let Some(CanonicalJsonValue::Object(json_body)) = json_body else {
|
||||||
return mem::take(&mut request.body);
|
return mem::take(&mut request.body);
|
||||||
};
|
};
|
||||||
|
|
||||||
let user_id = auth.sender_user.clone().unwrap_or_else(|| {
|
|
||||||
let server_name = services.globals.server_name();
|
|
||||||
UserId::parse_with_server_name(EMPTY, server_name).expect("valid user_id")
|
|
||||||
});
|
|
||||||
|
|
||||||
let uiaa_request = json_body
|
|
||||||
.get("auth")
|
|
||||||
.and_then(CanonicalJsonValue::as_object)
|
|
||||||
.and_then(|auth| auth.get("session"))
|
|
||||||
.and_then(CanonicalJsonValue::as_str)
|
|
||||||
.and_then(|session| {
|
|
||||||
services
|
|
||||||
.uiaa
|
|
||||||
.get_uiaa_request(&user_id, auth.sender_device.as_deref(), session)
|
|
||||||
});
|
|
||||||
|
|
||||||
if let Some(CanonicalJsonValue::Object(initial_request)) = uiaa_request {
|
|
||||||
for (key, value) in initial_request {
|
|
||||||
json_body.entry(key).or_insert(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut buf = BytesMut::new().writer();
|
let mut buf = BytesMut::new().writer();
|
||||||
serde_json::to_writer(&mut buf, &json_body).expect("value serialization can't fail");
|
serde_json::to_writer(&mut buf, &json_body).expect("value serialization can't fail");
|
||||||
buf.into_inner().freeze()
|
buf.into_inner().freeze()
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ libc.workspace = true
|
|||||||
libloading.workspace = true
|
libloading.workspace = true
|
||||||
libloading.optional = true
|
libloading.optional = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
lettre.workspace = true
|
||||||
num-traits.workspace = true
|
num-traits.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
rand_core = { version = "0.6.4", features = ["getrandom"] }
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ pub fn check(config: &Config) -> Result {
|
|||||||
if config.allow_registration
|
if config.allow_registration
|
||||||
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
&& config.yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse
|
||||||
&& config.registration_token.is_none()
|
&& config.registration_token.is_none()
|
||||||
|
&& config.registration_token_file.is_none()
|
||||||
{
|
{
|
||||||
warn!(
|
warn!(
|
||||||
"Open registration is enabled via setting \
|
"Open registration is enabled via setting \
|
||||||
|
|||||||
+79
-7
@@ -16,6 +16,7 @@ use either::{
|
|||||||
};
|
};
|
||||||
use figment::providers::{Env, Format, Toml};
|
use figment::providers::{Env, Format, Toml};
|
||||||
pub use figment::{Figment, value::Value as FigmentValue};
|
pub use figment::{Figment, value::Value as FigmentValue};
|
||||||
|
use lettre::message::Mailbox;
|
||||||
use regex::RegexSet;
|
use regex::RegexSet;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||||
@@ -68,6 +69,10 @@ pub struct Config {
|
|||||||
///
|
///
|
||||||
/// Also see the `[global.well_known]` config section at the very bottom.
|
/// Also see the `[global.well_known]` config section at the very bottom.
|
||||||
///
|
///
|
||||||
|
/// If `client` is not set under `[global.well_known]`, the server name will
|
||||||
|
/// be used as the base domain for user-facing links (such as password
|
||||||
|
/// reset links) created by Continuwuity.
|
||||||
|
///
|
||||||
/// Examples of delegation:
|
/// Examples of delegation:
|
||||||
/// - https://continuwuity.org/.well-known/matrix/server
|
/// - https://continuwuity.org/.well-known/matrix/server
|
||||||
/// - https://continuwuity.org/.well-known/matrix/client
|
/// - https://continuwuity.org/.well-known/matrix/client
|
||||||
@@ -609,19 +614,25 @@ pub struct Config {
|
|||||||
pub yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool,
|
pub yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse: bool,
|
||||||
|
|
||||||
/// A static registration token that new users will have to provide when
|
/// A static registration token that new users will have to provide when
|
||||||
/// creating an account. If unset and `allow_registration` is true,
|
/// creating an account. This token does not supersede tokens from other
|
||||||
/// you must set
|
/// sources, such as the `!admin token` command or the
|
||||||
/// `yes_i_am_very_very_sure_i_want_an_open_registration_server_prone_to_abuse`
|
/// `registration_token_file` configuration option.
|
||||||
/// to true to allow open registration without any conditions.
|
|
||||||
///
|
|
||||||
/// If you do not want to set a static token, the `!admin token` commands
|
|
||||||
/// may also be used to manage registration tokens.
|
|
||||||
///
|
///
|
||||||
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
/// example: "o&^uCtes4HPf0Vu@F20jQeeWE7"
|
||||||
///
|
///
|
||||||
/// display: sensitive
|
/// display: sensitive
|
||||||
pub registration_token: Option<String>,
|
pub registration_token: Option<String>,
|
||||||
|
|
||||||
|
/// A path to a file containing static registration tokens, one per line.
|
||||||
|
/// Tokens in this file do not supersede tokens from other sources, such as
|
||||||
|
/// the `!admin token` command or the `registration_token` configuration
|
||||||
|
/// option.
|
||||||
|
///
|
||||||
|
/// The file will be read once, when Continuwuity starts. It is not
|
||||||
|
/// currently reread when the server configuration is reloaded. If the file
|
||||||
|
/// cannot be read, Continuwuity will fail to start.
|
||||||
|
pub registration_token_file: Option<PathBuf>,
|
||||||
|
|
||||||
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
/// The public site key for reCaptcha. If this is provided, reCaptcha
|
||||||
/// becomes required during registration. If both captcha *and*
|
/// becomes required during registration. If both captcha *and*
|
||||||
/// registration token are enabled, both will be required during
|
/// registration token are enabled, both will be required during
|
||||||
@@ -746,6 +757,9 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub well_known: WellKnownConfig,
|
pub well_known: WellKnownConfig,
|
||||||
|
|
||||||
|
/// display: nested
|
||||||
|
pub smtp: Option<SmtpConfig>,
|
||||||
|
|
||||||
/// Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
|
/// Enable OpenTelemetry OTLP tracing export. This replaces the deprecated
|
||||||
/// Jaeger exporter. Traces will be sent via OTLP to a collector (such as
|
/// Jaeger exporter. Traces will be sent via OTLP to a collector (such as
|
||||||
/// Jaeger) that supports the OpenTelemetry Protocol.
|
/// Jaeger) that supports the OpenTelemetry Protocol.
|
||||||
@@ -1729,6 +1743,11 @@ pub struct Config {
|
|||||||
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
|
/// default: "continuwuity/<version> (bot; +https://continuwuity.org)"
|
||||||
pub url_preview_user_agent: Option<String>,
|
pub url_preview_user_agent: Option<String>,
|
||||||
|
|
||||||
|
/// Determines whether audio and video files will be downloaded for URL
|
||||||
|
/// previews.
|
||||||
|
#[serde(default)]
|
||||||
|
pub url_preview_allow_audio_video: bool,
|
||||||
|
|
||||||
/// List of forbidden room aliases and room IDs as strings of regex
|
/// List of forbidden room aliases and room IDs as strings of regex
|
||||||
/// patterns.
|
/// patterns.
|
||||||
///
|
///
|
||||||
@@ -2078,6 +2097,13 @@ pub struct Config {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub force_disable_first_run_mode: bool,
|
pub force_disable_first_run_mode: bool,
|
||||||
|
|
||||||
|
/// Allow search engines and crawlers to index Continuwuity's built-in
|
||||||
|
/// webpages served under the `/_continuwuity/` prefix.
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub allow_web_indexing: bool,
|
||||||
|
|
||||||
/// display: nested
|
/// display: nested
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub ldap: LdapConfig,
|
pub ldap: LdapConfig,
|
||||||
@@ -2418,6 +2444,52 @@ pub struct DraupnirConfig {
|
|||||||
pub secret: String,
|
pub secret: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize)]
|
||||||
|
#[config_example_generator(
|
||||||
|
filename = "conduwuit-example.toml",
|
||||||
|
section = "global.smtp",
|
||||||
|
optional = "true"
|
||||||
|
)]
|
||||||
|
pub struct SmtpConfig {
|
||||||
|
/// A `smtp://`` URI which will be used to connect to a mail server.
|
||||||
|
/// Uncommenting the [global.smtp] group and setting this option enables
|
||||||
|
/// features which depend on the ability to send email,
|
||||||
|
/// such as self-service password resets.
|
||||||
|
///
|
||||||
|
/// For most modern mail servers, format the URI like this:
|
||||||
|
/// `smtps://username:password@hostname:port`
|
||||||
|
/// Note that you will need to URL-encode the username and password. If your
|
||||||
|
/// username _is_ your email address, you will need to replace the `@` with
|
||||||
|
/// `%40`.
|
||||||
|
///
|
||||||
|
/// For a guide on the accepted URI syntax, consult Lettre's documentation:
|
||||||
|
/// https://docs.rs/lettre/latest/lettre/transport/smtp/struct.AsyncSmtpTransport.html#method.from_url
|
||||||
|
pub connection_uri: String,
|
||||||
|
|
||||||
|
/// The outgoing address which will be used for sending emails.
|
||||||
|
///
|
||||||
|
/// For a syntax guide, see https://datatracker.ietf.org/doc/html/rfc2822#section-3.4
|
||||||
|
///
|
||||||
|
/// ...or if you don't want to read the RFC, for some reason:
|
||||||
|
/// - `Name <address@domain.org>` to specify a sender name
|
||||||
|
/// - `address@domain.org` to not use a name
|
||||||
|
pub sender: Mailbox,
|
||||||
|
|
||||||
|
/// Whether to require that users provide an email address when they
|
||||||
|
/// register.
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub require_email_for_registration: bool,
|
||||||
|
|
||||||
|
/// Whether to require that users who register with a registration token
|
||||||
|
/// provide an email address.
|
||||||
|
///
|
||||||
|
/// default: false
|
||||||
|
#[serde(default)]
|
||||||
|
pub require_email_for_token_registration: bool,
|
||||||
|
}
|
||||||
|
|
||||||
const DEPRECATED_KEYS: &[&str] = &[
|
const DEPRECATED_KEYS: &[&str] = &[
|
||||||
"cache_capacity",
|
"cache_capacity",
|
||||||
"conduit_cache_capacity_modifier",
|
"conduit_cache_capacity_modifier",
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ impl Error {
|
|||||||
| Self::Reqwest(error) => error.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
| Self::Reqwest(error) => error.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR),
|
||||||
| Self::Conflict(_) => StatusCode::CONFLICT,
|
| Self::Conflict(_) => StatusCode::CONFLICT,
|
||||||
| Self::Io(error) => response::io_error_code(error.kind()),
|
| Self::Io(error) => response::io_error_code(error.kind()),
|
||||||
|
| Self::Uiaa(_) => StatusCode::UNAUTHORIZED,
|
||||||
| _ => StatusCode::INTERNAL_SERVER_ERROR,
|
| _ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1224,6 +1224,7 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Confirm that the event sender has the required power levels.
|
/// Confirm that the event sender has the required power levels.
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
fn check_power_levels(
|
fn check_power_levels(
|
||||||
room_version: &RoomVersion,
|
room_version: &RoomVersion,
|
||||||
power_event: &impl Event,
|
power_event: &impl Event,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ type Result<T, E = Error> = crate::Result<T, E>;
|
|||||||
/// event is part of the same room.
|
/// event is part of the same room.
|
||||||
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
|
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
|
||||||
//#[tracing::instrument(level event_fetch))]
|
//#[tracing::instrument(level event_fetch))]
|
||||||
|
#[allow(clippy::cognitive_complexity)]
|
||||||
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
|
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
|
||||||
room_version: &RoomVersionId,
|
room_version: &RoomVersionId,
|
||||||
state_sets: Sets,
|
state_sets: Sets,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ pub mod json;
|
|||||||
pub mod math;
|
pub mod math;
|
||||||
pub mod mutex_map;
|
pub mod mutex_map;
|
||||||
pub mod rand;
|
pub mod rand;
|
||||||
|
pub mod response;
|
||||||
pub mod result;
|
pub mod result;
|
||||||
pub mod set;
|
pub mod set;
|
||||||
pub mod stream;
|
pub mod stream;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
use futures::StreamExt;
|
||||||
|
use num_traits::ToPrimitive;
|
||||||
|
|
||||||
|
use crate::Err;
|
||||||
|
|
||||||
|
/// Reads the response body while enforcing a maximum size limit to prevent
|
||||||
|
/// memory exhaustion.
|
||||||
|
pub async fn limit_read(response: reqwest::Response, max_size: u64) -> crate::Result<Vec<u8>> {
|
||||||
|
if response.content_length().is_some_and(|len| len > max_size) {
|
||||||
|
return Err!(BadServerResponse("Response too large"));
|
||||||
|
}
|
||||||
|
let mut data = Vec::new();
|
||||||
|
let mut reader = response.bytes_stream();
|
||||||
|
|
||||||
|
while let Some(chunk) = reader.next().await {
|
||||||
|
let chunk = chunk?;
|
||||||
|
data.extend_from_slice(&chunk);
|
||||||
|
|
||||||
|
if data.len() > max_size.to_usize().expect("max_size must fit in usize") {
|
||||||
|
return Err!(BadServerResponse("Response too large"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the response body as text while enforcing a maximum size limit to
|
||||||
|
/// prevent memory exhaustion.
|
||||||
|
pub async fn limit_read_text(
|
||||||
|
response: reqwest::Response,
|
||||||
|
max_size: u64,
|
||||||
|
) -> crate::Result<String> {
|
||||||
|
let text = String::from_utf8(limit_read(response, max_size).await?)?;
|
||||||
|
Ok(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(async_fn_in_trait)]
|
||||||
|
pub trait LimitReadExt {
|
||||||
|
async fn limit_read(self, max_size: u64) -> crate::Result<Vec<u8>>;
|
||||||
|
async fn limit_read_text(self, max_size: u64) -> crate::Result<String>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LimitReadExt for reqwest::Response {
|
||||||
|
async fn limit_read(self, max_size: u64) -> crate::Result<Vec<u8>> {
|
||||||
|
limit_read(self, max_size).await
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn limit_read_text(self, max_size: u64) -> crate::Result<String> {
|
||||||
|
limit_read_text(self, max_size).await
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -415,13 +415,6 @@ impl<'a, 'de: 'a> de::Deserializer<'de> for &'a mut Deserializer<'de> {
|
|||||||
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
|
tracing::instrument(level = "trace", skip_all, fields(?self.buf))
|
||||||
)]
|
)]
|
||||||
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||||
debug_assert_eq!(
|
|
||||||
conduwuit::debug::type_name::<V>(),
|
|
||||||
"serde_json::value::de::<impl serde_core::de::Deserialize for \
|
|
||||||
serde_json::value::Value>::deserialize::ValueVisitor",
|
|
||||||
"deserialize_any: type not expected"
|
|
||||||
);
|
|
||||||
|
|
||||||
match self.record_peek_byte() {
|
match self.record_peek_byte() {
|
||||||
| Some(b'{') => self.deserialize_map(visitor),
|
| Some(b'{') => self.deserialize_map(visitor),
|
||||||
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
|
| Some(b'[') => serde_json::Deserializer::from_slice(self.record_next())
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user