Compare commits

...
Author SHA1 Message Date
timedout 68ee16962a fix: Don't panic when m.room.create event isn't a PDU 2026-07-29 18:32:16 +01:00
timedout d2f28a076d fix: validate_legacy_invite_state_event isn't async 2026-07-29 18:16:19 +01:00
timedout e327912a39 feat: Support accepting invites consisting of solely stripped state 2026-07-29 18:12:06 +01:00
timedout cab2f53c22 feat: Implement sketchy backwards compatibility for illegal invites 2026-07-29 17:18:46 +01:00
timedout 1ca30e4268 chore: Fix missing whitespace in previous commit's changelog
My IDE has beef with pre-commit for some reason
2026-07-29 16:45:50 +01:00
timedoutandErwan Leboucher 71016a0d7f fix: SEC20
Reviewed-By: Ginger <ginger@gingershaped.computer>
Co-Authored-By: Erwan Leboucher <erwanleboucher@gmail.com>
2026-07-29 16:41:59 +01:00
Renovate Bot c7602a570c chore(deps): update github-actions-digest 2026-07-28 05:01:29 +00:00
Ginger 83cf15bccb chore: Release 2026-07-27 16:37:23 -04:00
Ginger e8a6a9ac9d fix: Restore missing arm64 build logic 2026-07-27 16:36:50 -04:00
Ginger 4e4c6028ed chore: Release 2026-07-27 15:59:39 -04:00
Ginger 9bfd41000e chore: Update changelog 2026-07-27 15:56:32 -04:00
Ginger 8d10613607 chore: Update generated documentation 2026-07-27 15:53:14 -04:00
Murph MurphyandGinger ca100e093b Fix for jammy previous image w/ different sources format 2026-07-27 15:48:48 -04:00
Murph MurphyandGinger 71bd471bb7 Update after trying to run as much as I could locally via docker 2026-07-27 15:48:48 -04:00
skeet70andGinger ec8815aa23 feat(workflows): Add arm64 .deb 2026-07-27 15:48:48 -04:00
GingerandEllis Git 44d7e6cf4f chore: News fragment 2026-07-27 19:28:15 +00:00
GingerandEllis Git 4e001abe92 feat: Add stable mutual rooms endpoint 2026-07-27 19:28:15 +00:00
Ginger 4a40290514 chore: News fragment 2026-07-27 14:36:42 -04:00
Ginger 226a3917af feat: Show dehydrated devices in account panel 2026-07-27 14:36:01 -04:00
timedoutandEllis Git 7920127391 fix: Explicit type error 2026-07-27 16:01:33 +00:00
timedoutandEllis Git 65ff23bd48 fix: Don't treat policy server signing keys as required for signature verification 2026-07-27 16:01:33 +00:00
Renovate BotandEllis Git 3e8bf4a3c7 chore(deps): update rust crate syn to v3 2026-07-27 16:01:11 +00:00
timedout 399005abc6 chore: Move registration notice logic 2026-07-27 16:46:36 +01:00
timedoutandEllis Git 7fdc7f9216 fix: Ensure client IP is logged in all registration alert paths 2026-07-27 15:20:07 +00:00
timedoutandEllis Git c677847e29 chore: Add newsfrag 2026-07-27 15:20:07 +00:00
timedoutandEllis Git 795cdd3740 fix: Re-introduce registration alerts 2026-07-27 15:20:07 +00:00
timedoutandEllis Git 9894e2a6d1 fix: Rephrase newsfrag 2026-07-27 14:21:38 +00:00
GingerandEllis Git c8a9eb41c5 refactor: Remove redundant bail_restricted calls 2026-07-27 14:21:38 +00:00
GingerandEllis Git e431a13a1a chore: News fragment 2026-07-27 14:21:38 +00:00
GingerandEllis Git 9a3496ae70 feat: Add admin command to issue access tokens 2026-07-27 14:21:38 +00:00
Renovate BotandEllis Git 4cf743883a chore(deps): update rust-zerover-patch-updates 2026-07-27 01:50:47 +00:00
Renovate Bot 2d719e45fb chore(deps): update ghcr.io/renovatebot/renovate docker tag to v43.281.1 2026-07-27 00:29:00 +00:00
Renovate BotandEllis Git 0b456e3492 chore(deps): update rust-non-major 2026-07-27 00:24:50 +00:00
Renovate Bot eee7a22e26 chore(deps): update ruma digest to 3ad0471 2026-07-26 22:57:51 +00:00
Renovate BotandEllis Git bc3168ef31 chore(deps): update node-patch-updates to v2.0.18 2026-07-26 22:55:24 +00:00
Renovate BotandEllis Git a6fd464f32 chore(deps): update dependency cargo-bins/cargo-binstall to v1.21.1 2026-07-26 22:55:18 +00:00
8addb41b5a chore(deps): Update ruma digest to 04d5d68
Co-authored-by: Ginger <ginger@gingershaped.computer>
2026-07-26 22:54:42 +00:00
Renovate BotandEllis Git 3e10875f86 chore(deps): update https://github.com/actions/setup-node action to v7 2026-07-26 19:26:49 +00:00
Erwan LeboucherandEllis Git e161b5f01e perf: Fetch joined member count once per event when notifying 2026-07-26 14:40:34 +00:00
Erwan LeboucherandEllis Git a0fa4cf0fe fix: Deliver simplified sliding sync account data without hanging 2026-07-26 14:17:21 +00:00
Erwan LeboucherandEllis Git 08c22b5fcc fix(sync): Wake local target user after membership changes are committed 2026-07-26 14:12:37 +00:00
timedout a7892b296f chore: Resolve clippy lints post-rebase 2026-07-26 15:09:52 +01:00
new-years-eveandtimedout 7f8c2aedc8 refactor: Use a shared helper function for submitting state events at room creation
On room creation, A list of initial state events may be provided that
should be submitted to the room after creation. These events
should be treated as any other state event and submitted to
the same checks.

With this change, state events submitted on room creation will be
submitted through the same helper as those through the usual endpoint.

The submission helper is moved to the timeline service to make it
available everywhere. This will be useful for implementing MSC4140
(issue #903).
2026-07-26 15:06:38 +01:00
Ginger 8cde2ad0cc fix: Fix missing migration logic 2026-07-25 15:18:13 -04:00
GingerandEllis Git 64b14cfad2 chore: News fragment 2026-07-25 14:16:34 +00:00
GingerandEllis Git 7cc5e44093 fix: Exclude deactivated users and AS puppets from user count 2026-07-25 14:16:34 +00:00
reasterandEllis Git a838d59cfc make clippy happy again 2026-07-25 13:55:30 +00:00
reasterandEllis Git fb7a378d63 deduplicate rooms in hierarchy traversal 2026-07-25 13:55:30 +00:00
Erwan LeboucherandEllis Git 4727f07e26 fix: Correct roomuserid_lastnotificationread table alias
roomuserid_lastnotificationread was mapped to userroomid_highlightcount
instead of its own table, so last_notification_read() read the highlight
count and reset_notification_counts clobbered the highlight table when
setting a read marker. Add the missing table definition and fix the alias.
2026-07-24 01:13:42 +00:00
lvenerisandEllis Git aa612baef3 fix(nix): use correct toolchain for rustfmt 2026-07-22 23:53:35 +00:00
Erwan Leboucher b144b18aef fix(sync): Verify state for newly-left rooms is in incremental sync 2026-07-21 18:32:34 +00:00
gingerandEllis Git 4f1af01bdc chore: Formatting 2026-07-21 18:26:02 +00:00
gingerandEllis Git 1ff0d8d5a4 fix: Fix oauth auth code flow grant page being broken 2026-07-21 18:23:39 +00:00
Omar PakkerandEllis Git bee92b58e7 chore: Add towncrier news fragment 2026-07-21 14:35:55 +00:00
Omar PakkerandEllis Git b9c6d5956f feat: Introduce accepted_ip_sources as a multiple options variant of request_ip_source
The current `request_ip_source` setting only allows a single option.
While it does fall back to the peer IP if the header is missing as of !2003,
which likely covers a lot of regular use, only allowing a single option limits
the deployment options available to more advanced deployments.
Setups where internal and external traffic use different reverse proxies will
end up with the wrong IP and the implicitness of the fallback allows for
situations where the used IP is not the IP expected.

By introducing a setting that allows multiple options to be set,
this limitation is resolved and it becomes possible to have client IP resolution
behind different reverse proxies and also making it possible to decide if and/or
what the fallback should be.

If set, options are evaluated in order. If all fail, the request fails.
2026-07-21 14:35:55 +00:00
Logan DevineandEllis Git 480e975877 chore: clean up unused imports of ClientIp 2026-07-21 14:31:16 +00:00
timedoutandEllis Git 6ce0d63733 meta: Update expectations section and call out attribution 2026-07-21 14:19:42 +00:00
timedoutandEllis Git e54252651b meta: Update security policy 2026-07-21 14:19:42 +00:00
GingerandEllis Git 8f60648009 fix: Use correct error code for invalid client ID 2026-07-21 13:42:50 +00:00
GingerandEllis Git 4c9426a13f feat: Add support for OAuth2 device auth flow 2026-07-21 13:42:50 +00:00
Deniel9204 25264308ab fix: Use the requested device ID when an appservice creates a device
The appservice branch of update_device_route (MSC4190 device creation)
generated a random device ID instead of using the one from the request
path, and dropped the requested display name. The PUT returned 200, but
the device the appservice asked for never existed, so every subsequent
request masquerading as that device failed with M_FORBIDDEN and one
orphaned random-ID device was left behind per attempt.

This made encrypted mautrix (bridgev2) bridges unable to start on
OIDC-enabled servers, where MSC4190 is the only available device
creation mechanism: /keys/upload failed on first start and /keys/query
on every restart. Combined with the pre-1f5e178c3f behaviour (400
"Token conflicts with an existing appservice token"), MSC4190 device
creation has never worked end-to-end in any release.

Create the device under the requested ID and forward the requested
display name.
2026-07-21 08:21:09 +02:00
Renovate Bot bcd57b9598 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v43.272.6 2026-07-21 05:10:42 +00:00
Renovate Bot b2dee05ccf chore(deps): update github-actions-digest 2026-07-21 05:02:36 +00:00
timedout 8b3be67860 feat: Increase access token length
Reviewed-By: ginger <ginger@gingershaped.computer>
2026-07-19 15:06:23 +01:00
Jade Ellis 153ef3a173 ci: Attempt to delay major rust toolchain updates 2026-07-17 19:18:38 +01:00
renovateandEllis Git 2441dfe436 chore(Nix): Updated flake hashes 2026-07-17 17:58:23 +00:00
Renovate BotandEllis Git d149833b7a chore(deps): update rust to v1.97.1 2026-07-17 17:58:23 +00:00
Erwan LeboucherandEllis Git b1632ee60b fix(register): Resolve alias service by correct name for auto-join 2026-07-17 17:57:51 +00:00
timedoutandEllis Git 789589ef37 chore: Remove redundant client IP extractions on individual routes 2026-07-17 12:57:48 +00:00
timedoutandEllis Git 1e47090f5d feat: Log client IP in router layer 2026-07-17 12:57:48 +00:00
GingerandEllis Git 3e55d08488 fix: Only return create prompt if registration is enabled 2026-07-17 12:49:15 +00:00
Erwan LeboucherandEllis Git 3a74ce6151 fix(client-ip): Fall back to peer IP instead of returning 500 2026-07-17 12:46:11 +00:00
Erwan LeboucherandEllis Git eff454218c fix(sync): Send full state for newly-joined rooms in incremental sync 2026-07-17 02:48:28 +00:00
Erwan Leboucher e2eb980b5e fix(sync): Stop waking sync loops when removing to-device events 2026-07-16 22:15:27 +02:00
Erwan Leboucher 8945cc9e10 fix(federation): Sign restricted join events before verification 2026-07-16 19:46:42 +00:00
Julian Anderson 78adeccda1 docs(deploying/rpm): Update out-of-date statement about Oracle Linux
version availability
2026-07-15 23:23:10 -04:00
Renovate Bot 8195373d81 chore(deps): update rust-non-major 2026-07-15 05:05:03 +00:00
Erwan Leboucher 1ac05838dd docs: Add back binstall with a link to the deps 2026-07-14 19:44:28 +02:00
Erwan Leboucher 7ecd2aa5e2 fix(pdu): Exempt m.room.create from auth_events check 2026-07-14 19:27:46 +02:00
Renovate BotandEllis Git 69a3145983 chore(deps): update rust-zerover-patch-updates 2026-07-14 13:10:32 +00:00
N00byKing 73b7553b1e chore: Add changelog entry for #1984 2026-07-14 13:40:28 +02:00
N00byKing 232ec3f620 fix: Return 201 instead of 200 on oauth registration
See RFC 7591 at 3.2.1: "The server responds with an HTTP 201 Created status code [...]"

Fixes a failure in matrix-dart-sdk
2026-07-14 11:00:34 +02:00
N00byKing e52bbeaf94 fix: Correct link for code style guide in CONTRIBUTING.md 2026-07-14 08:59:08 +00:00
Renovate Bot ad1c194fb0 chore(deps): update github-actions-digest 2026-07-14 05:01:52 +00:00
GingerandEllis Git 5e2dcd8e79 chore: News fragment 2026-07-13 20:38:10 +00:00
GingerandEllis Git 24c6474bd1 fix: Allow unstable and stable device id query together 2026-07-13 20:38:10 +00:00
gingerandEllis Git 642d05048b chore: announce 2026-07-13 18:54:04 +00:00
Koen OostveenandEllis Git 446ae93ad7 fix(web): Change incorrect deeplink for deleting a device 2026-07-13 13:45:46 +00:00
Renovate BotandEllis Git 9ca0774b2d chore(deps): lock file maintenance 2026-07-13 13:44:47 +00:00
Ginger ce494eb0c0 fix: Properly sync newly created rooms 2026-07-13 08:50:53 -04:00
Ginger 1f5e178c3f fix: Properly handle appservice device creation 2026-07-13 08:50:48 -04:00
Renovate Bot ddd050d402 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v43.260.2 2026-07-13 05:03:24 +00:00
122 changed files with 2870 additions and 1680 deletions
@@ -44,7 +44,7 @@ runs:
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
@@ -79,7 +79,7 @@ runs:
- name: Login to builtin registry
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4
with:
registry: ${{ env.BUILTIN_REGISTRY }}
username: ${{ inputs.registry_user }}
+1 -1
View File
@@ -71,7 +71,7 @@ runs:
- name: Install timelord-cli and git-warp-time
if: steps.check-binaries.outputs.need-install == 'true'
uses: https://github.com/taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2
uses: https://github.com/taiki-e/install-action@41049aa56687c35e0afa74eed4f09cec4f9afabf # v2
with:
tool: git-warp-time,timelord-cli@3.0.1
+58 -8
View File
@@ -18,6 +18,7 @@ jobs:
strategy:
matrix:
container: [ "ubuntu-latest", "ubuntu-previous", "debian-latest", "debian-oldstable" ]
arch: [ "amd64", "arm64" ]
container:
image: "ghcr.io/tcpipuk/act-runner:${{ matrix.container }}"
@@ -43,7 +44,7 @@ jobs:
# fi
- name: Checkout repository with full history
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
@@ -54,9 +55,9 @@ jobs:
path: |
~/.cargo/registry
~/.cargo/git
key: cargo-debian-${{ steps.debian-version.outputs.distribution }}-${{ hashFiles('**/Cargo.lock') }}
key: cargo-debian-${{ steps.debian-version.outputs.distribution }}-${{ matrix.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
cargo-debian-${{ steps.debian-version.outputs.distribution }}-
cargo-debian-${{ steps.debian-version.outputs.distribution }}-${{ matrix.arch }}-
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
@@ -70,11 +71,55 @@ jobs:
# Aggressive GC since cache restores don't increment counter
echo "CARGO_INCREMENTAL_GC_TRIGGER=5" >> $GITHUB_ENV
- name: Install cross-compilation tools for arm64
if: matrix.arch == 'arm64'
run: |
dpkg --add-architecture arm64
if ! apt-get update; then
# Older Ubuntu releases (e.g. noble) advertise arm64 in their
# Release files but only serve amd64/i386 from archive.ubuntu.com,
# so apt update 404s. Restrict the main sources to amd64 and
# fetch arm64 from ports.ubuntu.com instead.
CODENAME=$(lsb_release -sc)
# deb822 sources (noble and newer)
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sed -i '/^Components:/a Architectures: amd64' /etc/apt/sources.list.d/ubuntu.sources
fi
# one-line sources (jammy and older)
if [ -f /etc/apt/sources.list ]; then
sed -i 's/^deb /deb [arch=amd64] /' /etc/apt/sources.list
fi
printf 'deb [arch=arm64] http://ports.ubuntu.com/ubuntu-ports %s main restricted universe multiverse\n' \
"$CODENAME" "$CODENAME-updates" "$CODENAME-security" \
> /etc/apt/sources.list.d/arm64-ports.list
apt-get update
fi
apt-get install -y \
gcc-aarch64-linux-gnu \
g++-aarch64-linux-gnu \
libc6-dev-arm64-cross
- name: Setup Rust
uses: ./.forgejo/actions/setup-rust
with:
github-token: ${{ secrets.GH_PUBLIC_RO }}
- name: Add Rust target
run: |
TARGET=${{ matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu' }}
rustup target add $TARGET
- name: Configure cross-compilation for arm64
if: matrix.arch == 'arm64'
run: |
echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
echo "CC_aarch64_unknown_linux_gnu=aarch64-linux-gnu-gcc" >> $GITHUB_ENV
echo "CXX_aarch64_unknown_linux_gnu=aarch64-linux-gnu-g++" >> $GITHUB_ENV
# rust-rocksdb's build script probes liburing via pkg-config, which
# refuses to run when host != target unless explicitly allowed
echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
echo "PKG_CONFIG_PATH_aarch64_unknown_linux_gnu=/usr/lib/aarch64-linux-gnu/pkgconfig" >> $GITHUB_ENV
- name: Get package version and component
id: package-meta
run: |
@@ -120,21 +165,26 @@ jobs:
apt-get update -y
# Build dependencies for rocksdb
apt-get install -y liburing-dev clang
# For arm64 builds, install cross-compiled dependencies.
# libstdc++6:arm64 is needed by dpkg-shlibdeps to resolve the
# dynamically-linked libstdc++ when cargo-deb computes $auto depends.
if [ "${{ matrix.arch }}" = "arm64" ]; then
apt-get install -y liburing-dev:arm64 libstdc++6:arm64
fi
- name: Run cargo-deb
id: cargo-deb
run: |
DEB_PATH=$(cargo deb --deb-version ${{ steps.package-meta.outputs.version }})
TARGET=${{ matrix.arch == 'arm64' && 'aarch64-unknown-linux-gnu' || 'x86_64-unknown-linux-gnu' }}
DEB_PATH=$(cargo deb --target $TARGET --deb-version ${{ steps.package-meta.outputs.version }})
echo "path=$DEB_PATH" >> $GITHUB_OUTPUT
- name: Test deb installation
if: matrix.arch == 'amd64'
run: |
echo "Installing: ${{ steps.cargo-deb.outputs.path }}"
apt-get install -y ${{ steps.cargo-deb.outputs.path }}
dpkg -s continuwuity
[ -f /usr/bin/conduwuit ] && echo "✅ Binary installed successfully"
[ -f /usr/lib/systemd/system/conduwuit.service ] && echo "✅ Systemd service installed"
[ -f /etc/conduwuit/conduwuit.toml ] && echo "✅ Config file installed"
@@ -142,7 +192,7 @@ jobs:
- name: Upload deb artifact
uses: forgejo/upload-artifact@v4
with:
name: continuwuity-${{ steps.debian-version.outputs.distribution }}
name: continuwuity-${{ steps.debian-version.outputs.distribution }}-${{ matrix.arch }}
path: ${{ steps.cargo-deb.outputs.path }}
- name: Publish to Forgejo package registry
+1 -1
View File
@@ -65,7 +65,7 @@ jobs:
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
uses: https://github.com/softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
draft: true
files: binaries/*
+2 -2
View File
@@ -21,7 +21,7 @@ jobs:
steps:
- name: Sync repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
fetch-depth: 0
@@ -32,7 +32,7 @@ jobs:
- name: Setup Node.js
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: https://github.com/actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 22
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
steps:
- name: 📦 Setup Node.js
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
uses: https://github.com/actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: "22"
+2 -2
View File
@@ -41,7 +41,7 @@ jobs:
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
@@ -55,7 +55,7 @@ jobs:
# repositories: continuwuity
- name: Install regsync
uses: https://github.com/regclient/actions/regsync-installer@39bf30ced4be9d147faaabbde2682e9725fb8bd1 # main
uses: https://github.com/regclient/actions/regsync-installer@c48159175b58fb90cccd0f98410927a81e04a166 # main
- name: Check what images need mirroring
run: |
+3 -3
View File
@@ -17,7 +17,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
@@ -48,7 +48,7 @@ jobs:
rust: ${{ steps.filter.outputs.rust }}
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
@@ -70,7 +70,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
+5 -5
View File
@@ -46,7 +46,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Prepare Docker build environment
@@ -101,7 +101,7 @@ jobs:
needs: build-release
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Create multi-platform manifest
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Prepare max-perf Docker build environment
@@ -189,7 +189,7 @@ jobs:
needs: build-maxperf
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false
- name: Create max-perf manifest
@@ -218,7 +218,7 @@ jobs:
path: binaries
merge-multiple: true
- name: Create Release and Upload
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
uses: https://github.com/softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
with:
draft: true
files: binaries/*
+2 -2
View File
@@ -43,11 +43,11 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
image: ghcr.io/renovatebot/renovate:43.281.1@sha256:34c2dd58f58e8976be2024a24fec23bbee805f0bf887837d9aaee7daeb09ccfc
options: --tmpfs /tmp:exec
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
show-progress: false
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
update-flake-hashes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: true
token: ${{ secrets.FORGEJO_TOKEN }}
+40
View File
@@ -1,3 +1,43 @@
# Continuwuity 26.7.0 (2026-07-27)
## Features
- Build and publish arm64 .deb packages alongside amd64 for all supported Debian and Ubuntu releases. (#1235)
- Dehydrated devices are now visible in the account panel. Contributed by @ginger. (#1970)
- Introduce `accepted_ip_sources` as a multiple options variant of `request_ip_source`, allowing for more advanced deployments and making fallbacks an explicit choice. Contributed by @Omar007 (#1985)
- Added an admin command to issue an access token for a bot account, to allow legacy bots to function while legacy authentication is disabled. Contributed by @ginger (#2044)
- Added support for the OAuth2 device authorization flow. Contributed by @ginger
- Added support for the stable mutual rooms query endpoint. Contributed by @ginger
- Fetch the joined member count once per event instead of once per notified user.
## Bugfixes
- Fix joining restricted rooms over federation failing with signature verification error. (fix-federation-signature)
- Fixed the client space hierarchy endpoint returning a 500 "Space hierarchy is unreasonably large" error for cyclic space graphs (e.g. a space containing itself). Rooms are now deduplicated during traversal as required by the spec, and the traversal depth is bounded even when the client does not specify `max_depth`. (space-hierarchy-cycle)
- Fixed simplified sliding sync holding account data for up to 30 seconds, which made encryption setup and cross-signing resets appear to hang. (sss-account-data-longpoll)
- Fixed local invites and invite acceptances not being reflected in sync promptly. Contributed by @eleboucher (wake-local-member-sync)
- Fixed the deeplink redirect for deleting devices. Contributed by @koen (#1965)
- Fix status code for oauth registration. Contributed by @n00byking (#1984)
- Exempt m.room.create from auth_events check. Contributed by @eleboucher (#1987)
- Fixed `create` being returned as a supported prompt value regardless of if registration is enabled or not. Contributed by @ginger (#1994)
- Fixed high CPU usage when multiple clients from the same account were connected at once. Each sync woke the account's other sync loops, causing them to wake each other in a loop. (#2006)
- Fixed MSC4190 appservice device creation registering a random device ID instead of the requested one (and dropping the requested display name), which prevented encrypted mautrix bridges from starting on OIDC-enabled servers and leaked an orphan device on the bridge bot per startup attempt. (#2015)
- Deactivated users and appservice puppets are no longer counted by `/_continuwuity/local_user_count`. Contributed by @ginger. (#2040)
- Re-introduced admin room registration alerts that were accidentally removed in the OAuth2 update. (#2057)
- Appservices are now properly able to create devices for E2EE.
- Appservices may now specify both the unstable and stable `device_id` query parameters in a request. The stable parameter will take priority. Contributed by @ginger.
- Fixed `roomuserid_lastnotificationread` being aliased to the highlight count table, which clobbered highlight counts when setting a read marker. Contributed by @eleboucher
- Fixed freshly left room failing to sync.
- Fixed newly created rooms failing to sync properly in clients using legacy sync.
- Fixed newly joined rooms failing to sync their full state (including the room name) to clients using legacy sync.
- Fixed requests returning `500 Internal Server Error` when the header selected by `request_ip_source` is absent, duplicated, or malformed (for example Envoy omitting `X-Envoy-External-Address` on internal requests). The client IP now falls back to the connection peer address instead of failing the request. Contributed by @eleboucher
- Resolve alias service by correct name for auto-join. Contributed by @eleboucher
## Improved Documentation
- Updated an out-of-date statement about Oracle Linux release cadences. (#1999)
# Continuwuity 26.6.2 (2026-07-12)
## Bugfixes
+2 -2
View File
@@ -26,7 +26,7 @@ ### Pre-commit Checks
```bash
# Install prek using cargo-binstall
# Install prek using cargo-binstall https://github.com/cargo-bins/cargo-binstall
cargo binstall prek
# Install git hooks to run checks automatically
@@ -155,7 +155,7 @@ ### Creating pull requests
Before submitting a pull request, please ensure:
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
2. Your code follows the [code style guide](/development/code_style.md)
2. Your code follows the [code style guide](docs/development/code_style.mdx)
3. Your commit messages follow the conventional commits format
4. Tests are added for new functionality
5. Documentation is updated if needed
Generated
+306 -404
View File
File diff suppressed because it is too large Load Diff
+5 -7
View File
@@ -12,7 +12,7 @@ license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
version = "26.6.2"
version = "26.7.1"
[workspace.metadata.crane]
name = "conduwuit"
@@ -115,8 +115,8 @@ git = "https://github.com/vinchona/axum-server-dual-protocol.git"
rev = "ca6db055254255b74238673ce4135698e347d71c" # feat!: bump axum_server to 0.8.0
default-features = false
[workspace.dependencies.axum-client-ip]
version = "1.3"
[workspace.dependencies.client-ip]
version = "0.2"
[workspace.dependencies.tower]
version = "0.5.2"
@@ -342,9 +342,8 @@ version = "1.1.1"
# Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma]
# version = "0.14.1"
git = "https://github.com/ruma/ruma.git"
rev = "9b6a2e7323649af926e4b5363d87239ee4247f4a"
rev = "e7284c31da289f0a3b885191f5b2b6b307fa059f"
features = [
"appservice-api-c",
"client-api",
@@ -357,7 +356,6 @@ features = [
"compat-upload-signatures",
"compat-optional-txn-pdus",
"compat-get-3pids",
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
"unstable-msc3061",
@@ -503,7 +501,7 @@ default-features = false
version = "0.1"
[workspace.dependencies.syn]
version = "2.0"
version = "3.0"
default-features = false
features = ["full", "extra-traits"]
+68 -24
View File
@@ -1,47 +1,90 @@
# Security Policy for Continuwuity
This document outlines the security policy for Continuwuity. Our goal is to maintain a secure platform for all users, and we take security matters seriously.
This document outlines the security policy for Continuwuity. Our goal is to maintain a secure platform for all users,
and we take security matters seriously.
## Supported Versions
We provide security updates for the following versions of Continuwuity:
| Version | Supported |
| -------------- |:----------------:|
| Latest release | |
| Main branch | |
| Older releases | |
| Version | Supported |
|----------------|:---------:|
| Latest release | |
| Main branch | |
| Older releases | |
We may backport fixes to the previous release at our discretion, but we don't guarantee this.
We may backport fixes to the previous release at our discretion, but we don't guarantee this; our versioning is designed
to encourage users to stay up-to-date with the latest release, and we have no concept of "long term support".
## Reporting a Vulnerability
### Responsible Disclosure
We appreciate the efforts of security researchers and the community in identifying and reporting vulnerabilities. To ensure that potential vulnerabilities are addressed properly, please follow these guidelines:
We appreciate the efforts of security researchers and the community in identifying and reporting vulnerabilities. To
ensure that potential vulnerabilities are addressed properly, please follow these guidelines:
1. **Contact members of the team directly** over E2EE private message.
- [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk)
- [@ginger:gingershaped.computer](https://matrix.to/#/@ginger:gingershaped.computer)
2. **Email the security team** at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so don't include sensitive details.
3. **Do not disclose the vulnerability publicly** until it has been addressed
- [@jade:ellis.link](https://matrix.to/#/@jade:ellis.link)
- [@nex:nexy7574.co.uk](https://matrix.to/#/@nex:nexy7574.co.uk)
- [@ginger:gingershaped.computer](https://matrix.to/#/@ginger:gingershaped.computer)
2. **Email the security team** at [security@continuwuity.org](mailto:security@continuwuity.org). This is not E2EE, so
don't include sensitive details.
3. **Do not disclose the vulnerability publicly** until a fix has been pushed to the main branch.
4. **Provide detailed information** about the vulnerability, including:
- A clear description of the issue
- Steps to reproduce
- Potential impact
- Any possible mitigations
- Version(s) affected, including specific commits if possible
- A clear description of the issue
- Steps to reproduce
- Potential impact
- Any possible mitigations
- Version(s) affected, including specific commits if possible
- How you want to be attributed if your report is accepted (website, social media handle, Matrix user ID, etc).
**Please state explicitly if you wish to remain anonymous.**
If you have any doubts about a potential security vulnerability, contact us via private channels first! We'd prefer that you bother us, instead of having a vulnerability disclosed without a fix.
If you have any doubts about a potential security vulnerability, contact us via private channels first! We'd prefer that
you bother us, instead of having a vulnerability disclosed without a fix.
### What to Expect
### Terms for credit
Before reporting a vulnerability, please remember that we are a small team maintaining a large codebase depended upon by
a vast unknown number of users in our free time. While we always investigate *all* security reports, we may not
acknowledge or credit your report under certain circumstances.
#### Following the security policy
If you do not report the vulnerability following this security policy, your report may be ignored and/or may not be
credited if fixed. This includes filing for GitHub security advisories without contacting us directly first (we do not
get notified about these!).
#### Automation-assisted reports
Reports assisted by automatic tooling such as LLMs MUST disclose such in the report.
Assisted reports must also produce a working proof-of-concept (PoC) demonstrating the vulnerability against the latest
release and/or main commit with no modifications to the codebase. This is to demonstrate that not only does
the author understand the vulnerability that they are reporting, but also that the vulnerability is reproducible
and not a false positive.
#### Reports for known issues
Reports for issues we are already aware of will only be credited if subsequent reporters provide new information.
#### Audits and CVE farming
Sweeping audits (vulnerability hunting) **must** be coordinated with the team first - receiving a rapsheet of new
vulnerabilities is explicitly not helpful to us and only harms the project. If you are interested in performing a
security audit, please contact us first to discuss the scope and methodology.
Likewise, CVE farming (reporting vulnerabilities with the primary intent to get a CVE number) is actively harmful to the
project, and will not be credited. Severe violations of this policy will result in a permanent ban from collaborating
with the project in any capacity. We are attempting to build high quality free software, not flesh out your CV/resume.
### What to expect
When you report a security vulnerability:
1. **Acknowledgment**: We will acknowledge receipt of your report.
2. **Assessment**: We will assess the vulnerability and determine its impact on our users
3. **Updates**: We will provide updates on our progress in addressing the vulnerability, and may request you help test mitigations
1. **Acknowledgment**: We will acknowledge receipt of your report. We may ask for further information.
2. **Triage**: The report will be triaged into our internal tracker, and you will be provided with a reference number (
in case you end up with multiple reports). An ETA for a fix will be provided if feasible.
3. **Updates**: We will provide updates on our progress in addressing the vulnerability, including a heads-up for when
we plan to release a fix.
4. **Resolution**: Once resolved, we will notify you and discuss coordinated disclosure
5. **Credit**: We will recognize your contribution (unless you prefer to remain anonymous)
@@ -51,7 +94,8 @@ ## Security Update Process
1. We will develop and test fixes in a private fork
2. Security updates will be released as soon as possible
3. Release notes will include information about the vulnerabilities, avoiding details that could facilitate exploitation where possible
3. Release notes will include information about the vulnerabilities, avoiding details that could facilitate exploitation
where possible
4. Critical security updates may be backported to the previous stable release
## Additional Resources
+1
View File
@@ -0,0 +1 @@
**TODO - embargoed until next release** (maintainers see security issue 10 when writing rls notes). Contributed by @eleboucher.
+42 -2
View File
@@ -290,7 +290,7 @@
#
#ip_lookup_strategy = 5
# The source to use for discovering the real connecting client IP.
# The preferred source to use for getting the real client IP.
#
# Takes any of the following options:
#
@@ -301,16 +301,47 @@
# "true_client_ip" - `True-Client-Ip` header
# "x_envoy_external_address" - `X-Envoy-External-Address` header
# "x_real_ip" - `X-Real-Ip` header
# "direct" - the directly connected IP address (fallback for other
# options)
#
# Only set this if you are certain only your reverse proxy
# will send the expected header. There is no "is the connecting IP allowed
# to set this header" check; if the header selected is present, it is
# used.
#
# Defaults to the IP address actually making the connection.
# Ignored if `accepted_ip_sources` is set.
#
# If not set set, `direct` is used. Additionally, `direct` is used
# as a fallback if the chosen header is not found.
#
#request_ip_source = false
# The accepted source(s) for the real client IP.
#
# Takes any combination of the following options:
#
# "cf_connecting_ip" - `Cf-Connecting-Ip` header
# "cloudfront_viewer_address" - `CloudFront-Viewer-Address` header
# "fly_client_ip" - `Fly-Client-IP` header
# "x_forwarded_for" - rightmost value of the `X-Forwarded-For` header
# "true_client_ip" - `True-Client-Ip` header
# "x_envoy_external_address" - `X-Envoy-External-Address` header
# "x_real_ip" - `X-Real-Ip` header
# "direct" - the directly connected IP address
#
# Only set this if you are certain only your reverse proxy
# will send the expected header. There is no "is the connecting IP allowed
# to set this header" check; if the header selected is present, it is
# used.
#
# One of the configured sources must be available or the request is
# rejected. If you want a fallback (including `direct`!), you have to
# configure it. Priority is based on configuration order.
#
# If not set, `request_ip_source` configuration is used.
#
#accepted_ip_sources = []
# Max request size for file uploads in bytes. Defaults to 20MB.
# Also limits incoming federated media.
#
@@ -1455,6 +1486,15 @@
#
#send_messages_from_ignored_users_to_client = false
# Disables strict validation on incoming federated invites. This weakens
# your server's security - only enable it if you encounter issues
# receiving invites from outdated servers.
#
# Validation will still be performed, but it will log a warning instead of
# causing an error.
#
#enable_legacy_invite_support = false
# Send "org.matrix.dummy_event" events to the client. This is a debugging
# option.
#
+1 -1
View File
@@ -50,7 +50,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.20.1
ENV BINSTALL_VERSION=1.21.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.20.1
ENV BINSTALL_VERSION=1.21.1
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+2 -2
View File
@@ -15,8 +15,8 @@ ## Overview
:::warning Oracle Linux support
Due to upstream limitations, Terra is only usable on Oracle Linux if you use [upstream EPEL](https://docs.fedoraproject.org/en-US/epel/getting-started/)
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, as of the time of writing, 10.2 has been
available for over a month through other ELs, but Oracle has not yet released corresponding updates—so you may encounter further compatibility issues with EPEL.
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, Oracle's release of 10.2 lagged
approximately 1.5 months behind other ELs—so you may encounter further compatibility issues with EPEL.
**For this reason, it is recommended that you use another EL distribution if at all possible.**
:::
@@ -6,10 +6,10 @@
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
},
{
"id": 15,
"id": 16,
"mention_room": true,
"date": "2026-07-10",
"message": "[Continuwuity 26.6.1](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.1) has just been released! This release contains a bunch of bugfixes for bugs found initially after 26.6.0's release - please read the changelog before updating!"
"date": "2026-07-13",
"message": "[Continuwuity 26.6.2](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.1) has just been released! This release fixes a severe bug with OIDC that could cause users' accounts to be flagged as deactivated. If you use OIDC, please update as soon as possible."
}
]
}
+4
View File
@@ -8,6 +8,10 @@ ## `!admin users create-user`
Create a new user
## `!admin users issue-token`
Issue an access token for a user. This command will not work on shadow users, such as appservice puppets or accounts imported from an identity provider
## `!admin users reset-password`
Reset user password
Generated
+21 -21
View File
@@ -3,11 +3,11 @@
"advisory-db": {
"flake": false,
"locked": {
"lastModified": 1781566179,
"narHash": "sha256-Tqv8I586fYzWpEW/Smq/JqESFa3DVVzVWsnAMtvhy/I=",
"lastModified": 1783840254,
"narHash": "sha256-XjyvZk0f3YiinVHmkGOotmLBAzvK+LwEJYj2QqJ5pn8=",
"owner": "rustsec",
"repo": "advisory-db",
"rev": "74e084413d979d52d2f93b1d93b1ab7b9ee648f5",
"rev": "6e3286f4efa8c142fb33e5ea4342c8db6693cf34",
"type": "github"
},
"original": {
@@ -18,11 +18,11 @@
},
"crane": {
"locked": {
"lastModified": 1780532242,
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
"lastModified": 1783203018,
"narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
"rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2",
"type": "github"
},
"original": {
@@ -39,11 +39,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1781527054,
"narHash": "sha256-1fX9ev2Fh5QoKQ41G9dYutjo5j/jywu6tZse5Eb1Ck4=",
"lastModified": 1783844668,
"narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=",
"owner": "nix-community",
"repo": "fenix",
"rev": "8c2e51dffefc040a21975da7abf6f252c8c9b783",
"rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7",
"type": "github"
},
"original": {
@@ -74,11 +74,11 @@
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"lastModified": 1782949081,
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
"type": "github"
},
"original": {
@@ -89,11 +89,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1781074563,
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
"lastModified": 1783776592,
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
"type": "github"
},
"original": {
@@ -105,11 +105,11 @@
},
"nixpkgs-lib": {
"locked": {
"lastModified": 1777168982,
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
"lastModified": 1782614948,
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
"owner": "nix-community",
"repo": "nixpkgs.lib",
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
"type": "github"
},
"original": {
@@ -132,11 +132,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1781453968,
"narHash": "sha256-+V3nK4pCngbmgyVGXY6Kkrlevp4ocPkJJLf2aqwkDNA=",
"lastModified": 1783779020,
"narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "cc272809a173c2c11d0e479d639c811c1eacf049",
"rev": "5be5e89cf0145d73a85c805726821b4adfc3af48",
"type": "github"
},
"original": {
+1 -1
View File
@@ -9,7 +9,7 @@
{
# basic nix shell containing all things necessary to build continuwuity in all flavors manually (on x86_64-linux)
devShells.default =
((inputs.crane.mkLib pkgs).overrideToolchain (pkgs: self'.packages.stable-toolchain)).devShell
((inputs.crane.mkLib pkgs).overrideToolchain (pkgs: self'.packages.dev-toolchain)).devShell
{
packages = [
self'.packages.rocksdb
+2 -2
View File
@@ -12,7 +12,7 @@
target:
target.fromToolchainName {
name = (lib.importTOML "${inputs.self}/rust-toolchain.toml").toolchain.channel;
sha256 = "sha256-OATSZm98Es5kIFuqaba+UvkQtFsVgJEBMmS+t6od5/U=";
sha256 = "sha256-A1abGIbOtcBSdrUMhDGrER3pRM1hQP4fp9gh3Y4PKc8=";
};
in
{
@@ -27,9 +27,9 @@
inherit stable-toolchain;
dev-toolchain = fnx.combine [
stable-toolchain
# use the nightly rustfmt because we use nightly features
fnx.complete.rustfmt
stable-toolchain
];
};
};
+61 -61
View File
@@ -419,9 +419,9 @@
}
},
"node_modules/@rspress/core": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.17.tgz",
"integrity": "sha512-oynYHE2aBj7BFzD/UgqKnrj/PyYxfWGrmRPlTmjLL22UPcocdUdFYLb/6nijyseAST59VMr5Gj4wOfFUeJT5vw==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.18.tgz",
"integrity": "sha512-DBpsr/6XAItQkZPL5FSjthLGuzdG9ks/7EOxkqaaXp93uAT54LO0PD5Dkoy6ydup1PnvkpHyiHpUyDcnHHTVhg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -429,7 +429,7 @@
"@mdx-js/react": "^3.1.1",
"@rsbuild/core": "^2.1.5",
"@rsbuild/plugin-react": "~2.1.0",
"@rspress/shared": "2.0.17",
"@rspress/shared": "2.0.18",
"@shikijs/rehype": "^4.2.0",
"@types/unist": "^3.0.3",
"@unhead/react": "^2.1.15",
@@ -472,9 +472,9 @@
}
},
"node_modules/@rspress/plugin-client-redirects": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.17.tgz",
"integrity": "sha512-HJMlW5xsQe2cAFYNHw3LhMy8WcuqRb4l0ey3E3OIu121ymbSLqIQJVPS/IF/NuoyPoxCAmV58LYIaCxGEriUww==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.18.tgz",
"integrity": "sha512-q70ufN0S125kl0QlucO7JuiZ0WJaxFOtPPoGu7eWwam9fb+i4YsWnOWtAKSKGH4JP5+897mgpDPP+dmfl9DPCw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -485,9 +485,9 @@
}
},
"node_modules/@rspress/plugin-sitemap": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.17.tgz",
"integrity": "sha512-MpNxF+hFK7zsk8t+Ta7KYI/Py6Csjyw0AMGrWf3XG/kr0+fisVxbT/jUanXRLSoj2m9VX9CVsi180rPnezUpbQ==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.18.tgz",
"integrity": "sha512-XfaXzZx+ASN9OMTc0fOvW8QmbmyCyDWkO8xJj4XF4E0YJtsu7j/65+v8/KXXdwpywOfUIQ6M94/meRTy2urNmQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -498,9 +498,9 @@
}
},
"node_modules/@rspress/shared": {
"version": "2.0.17",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.17.tgz",
"integrity": "sha512-ZzpZ4hm5svgwU0w5CpTLZy4vFD3uPo8+gXtWMOREYMzwfzJeHqwOyXwxHf6byGpx85mVK5DQqMDi61meAS7ZUw==",
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.18.tgz",
"integrity": "sha512-GJswqJQCPSxvBt5r+gJzz8Em8EEK/sOmCQjTpemTvldvxr1Lva85BGVnSQZOgofnM0nrjt18kn4mmnuuSstbpA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -510,14 +510,14 @@
}
},
"node_modules/@shikijs/core": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz",
"integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz",
"integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/primitive": "4.2.0",
"@shikijs/types": "4.2.0",
"@shikijs/primitive": "4.3.1",
"@shikijs/types": "4.3.1",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4",
"hast-util-to-html": "^9.0.5"
@@ -527,13 +527,13 @@
}
},
"node_modules/@shikijs/engine-javascript": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz",
"integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz",
"integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/types": "4.3.1",
"@shikijs/vscode-textmate": "^10.0.2",
"oniguruma-to-es": "^4.3.6"
},
@@ -542,13 +542,13 @@
}
},
"node_modules/@shikijs/engine-oniguruma": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz",
"integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz",
"integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/types": "4.3.1",
"@shikijs/vscode-textmate": "^10.0.2"
},
"engines": {
@@ -556,26 +556,26 @@
}
},
"node_modules/@shikijs/langs": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz",
"integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz",
"integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0"
"@shikijs/types": "4.3.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/primitive": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz",
"integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz",
"integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/types": "4.3.1",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
},
@@ -584,16 +584,16 @@
}
},
"node_modules/@shikijs/rehype": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.2.0.tgz",
"integrity": "sha512-ST3EWye/dwF1gWskczJNBnwFtDzEQ9ceytXZtyc/GfwR5V0qJrkoSGZO55O3SAKDDsXkTDcsfwd9pVe7ROlAHg==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.3.1.tgz",
"integrity": "sha512-oshrlfUF3VPUJfnp5K1lLwsS/SRBKrIxONpdWebSKZXdBE3UsZnxgqpvRUA8UsofS7vmjFOCAHIT71ECbmOxTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0",
"@shikijs/types": "4.3.1",
"@types/hast": "^3.0.4",
"hast-util-to-string": "^3.0.1",
"shiki": "4.2.0",
"shiki": "4.3.1",
"unified": "^11.0.5",
"unist-util-visit": "^5.1.0"
},
@@ -602,22 +602,22 @@
}
},
"node_modules/@shikijs/themes": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz",
"integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz",
"integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/types": "4.2.0"
"@shikijs/types": "4.3.1"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@shikijs/types": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz",
"integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz",
"integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -684,9 +684,9 @@
}
},
"node_modules/@types/hast": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1076,9 +1076,9 @@
}
},
"node_modules/@ungap/structured-clone": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
"integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
"dev": true,
"license": "ISC"
},
@@ -3589,18 +3589,18 @@
"license": "MIT"
},
"node_modules/shiki": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz",
"integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz",
"integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@shikijs/core": "4.2.0",
"@shikijs/engine-javascript": "4.2.0",
"@shikijs/engine-oniguruma": "4.2.0",
"@shikijs/langs": "4.2.0",
"@shikijs/themes": "4.2.0",
"@shikijs/types": "4.2.0",
"@shikijs/core": "4.3.1",
"@shikijs/engine-javascript": "4.3.1",
"@shikijs/engine-oniguruma": "4.3.1",
"@shikijs/langs": "4.3.1",
"@shikijs/themes": "4.3.1",
"@shikijs/types": "4.3.1",
"@shikijs/vscode-textmate": "^10.0.2",
"@types/hast": "^3.0.4"
},
+7
View File
@@ -60,6 +60,13 @@
"matchPackageNames": ["rust", "rustc", "cargo"],
"groupName": "rust-toolchain"
},
{
"description": "Delay major rust toolchain updates",
"matchManagers": ["custom.regex"],
"matchPackageNames": ["rust", "rustc", "cargo"],
"matchUpdateTypes": ["major"],
"minimumReleaseAge": "30 days"
},
{
"description": "Batch minor and patch GitHub Actions updates",
"matchManagers": ["github-actions"],
+1 -1
View File
@@ -10,7 +10,7 @@
[toolchain]
profile = "minimal"
channel = "1.97.0"
channel = "1.97.1"
components = [
# For rust-analyzer
"rust-src",
+1 -1
View File
@@ -3,7 +3,7 @@
use service::registration_tokens::TokenExpires;
impl crate::Context<'_> {
pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
pub(super) async fn issue_registration_token(&self, expires: super::TokenExpires) -> Result {
let expires = {
if expires.immortal {
None
+1 -1
View File
@@ -10,7 +10,7 @@
pub enum TokenCommand {
/// Issue a new registration token
#[clap(name = "issue")]
IssueToken {
IssueRegistrationToken {
/// When this token will expire.
#[command(flatten)]
expires: TokenExpires,
+31 -14
View File
@@ -20,7 +20,7 @@
tag::{TagEvent, TagEventContent, TagInfo},
},
};
use service::users::{AccountStatus, HashedPassword};
use service::users::{AccountStatus, DeviceToken, HashedPassword};
use crate::{
get_room_info,
@@ -59,13 +59,42 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services
.users
.create_local_account(&user_id, Some(HashedPassword::new(password)?), None)
.create_local_account(
&user_id,
Some(HashedPassword::new(password)?),
None,
None,
None,
)
.await?;
self.write_str(&format!("Created user {user_id} with password `{password}`"))
.await
}
pub(super) async fn issue_access_token(&self, username: String, password: String) -> Result {
let user_id = parse_active_local_user_id(self.services, &username).await?;
let user_id = self
.services
.users
.check_password(&user_id, &password)
.await?;
let token = DeviceToken::new_random();
let device_id = self
.services
.users
.create_device(&user_id, None, Some(token.clone()), None, None)
.await?;
self.write_str(&format!(
"Created device `{device_id}` with access token `{}` for {user_id}",
token.into_token()
))
.await
}
pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result {
// Validate user id
let user_id = parse_local_user_id(self.services, &user_id)?;
@@ -102,7 +131,6 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
}
pub(super) async fn suspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
@@ -123,7 +151,6 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
}
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
@@ -935,7 +962,6 @@ pub(super) async fn force_leave_remote_room(
}
pub(super) async fn lock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
@@ -956,7 +982,6 @@ pub(super) async fn lock(&self, user_id: String) -> Result {
}
pub(super) async fn unlock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
self.services.users.unlock_account(&user_id).await;
@@ -966,7 +991,6 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
}
pub(super) async fn logout(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
@@ -992,7 +1016,6 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
}
pub(super) async fn disable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
if user_id == self.services.globals.server_user {
@@ -1011,7 +1034,6 @@ pub(super) async fn disable_login(&self, user_id: String) -> Result {
}
pub(super) async fn enable_login(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
self.services.users.enable_login(&user_id);
@@ -1020,7 +1042,6 @@ pub(super) async fn enable_login(&self, user_id: String) -> Result {
}
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
@@ -1039,8 +1060,6 @@ pub(super) async fn get_email(&self, user_id: String) -> Result {
}
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.");
};
@@ -1063,8 +1082,6 @@ pub(super) async fn get_user_by_email(&self, email: String) -> Result {
}
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.");
+9
View File
@@ -18,6 +18,15 @@ pub enum UserCommand {
password: Option<String>,
},
/// Issue an access token for a user. This command will not work on
/// shadow users, such as appservice puppets or accounts imported from
/// an identity provider.
#[clap(name = "issue-token")]
IssueAccessToken {
username: String,
password: String,
},
/// Reset user password
ResetPassword {
/// Log out existing sessions
+1 -1
View File
@@ -64,11 +64,11 @@ zstd_compression = [
[dependencies]
async-trait.workspace = true
axum-client-ip.workspace = true
axum-extra.workspace = true
axum.workspace = true
base64.workspace = true
bytes.workspace = true
client-ip.workspace = true
conduwuit-core.workspace = true
conduwuit-macros.workspace = true
conduwuit-service.workspace = true
+3 -8
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err, info,
pdu::PartialPdu,
@@ -26,7 +25,6 @@
};
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{Ruma, router::ClientIdentity};
pub(crate) mod register;
@@ -43,10 +41,9 @@
///
/// 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")]
#[tracing::instrument(skip_all, name = "register_available", level = "info")]
pub(crate) async fn get_register_available_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
let _ = services
@@ -80,10 +77,9 @@ pub(crate) async fn get_register_available_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "change_password", level = "info")]
#[tracing::instrument(skip_all, name = "change_password", level = "info")]
pub(crate) async fn change_password_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
let identity = if let Some(identity) = body.identity.as_ref() {
@@ -248,10 +244,9 @@ pub(crate) async fn whoami_route(
/// - 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")]
#[tracing::instrument(skip_all, name = "deactivate", level = "info")]
pub(crate) async fn deactivate_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<deactivate::v3::Request>,
) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint is technically optional,
+21 -23
View File
@@ -1,11 +1,7 @@
use std::collections::HashMap;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_info, info,
utils::{self},
};
use conduwuit::{Err, Result, debug_info, info};
use conduwuit_service::Services;
use futures::StreamExt;
use lettre::{Address, message::Mailbox};
@@ -20,10 +16,12 @@
assign,
};
use serde_json::value::RawValue;
use service::{mailer::messages, users::HashedPassword};
use service::{
mailer::messages,
users::{DeviceToken, HashedPassword},
};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `POST /_matrix/client/v3/register`
///
@@ -33,10 +31,10 @@
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available.
#[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
#[tracing::instrument(skip_all, name = "register", level = "info")]
pub(crate) async fn register_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
ClientIp(client): ClientIp, // NOTE: Required for metadata.
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
if body.kind != RegistrationKind::User {
@@ -97,7 +95,13 @@ pub(crate) async fn register_route(
services
.users
.create_local_account(&user_id, Some(password), identity.email)
.create_local_account(
&user_id,
Some(password),
identity.email,
Some(&client),
body.initial_device_display_name.as_deref(),
)
.await?;
user_id
@@ -112,27 +116,21 @@ pub(crate) async fn register_route(
)));
}
// Generate new device id if the user didn't specify one
let device_id = 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);
let new_token = DeviceToken::new_random();
// Create device for this account
services
let device_id = services
.users
.create_device(
&user_id,
&device_id,
&new_token,
None,
body.device_id.clone(),
Some(new_token.clone()),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
(Some(new_token), Some(device_id))
} else {
// Don't create a device for inhibited logins
@@ -142,7 +140,7 @@ pub(crate) async fn register_route(
debug_info!(%user_id, ?device, "New account created via legacy registration");
Ok(assign!(register::v3::Response::new(user_id), {
access_token: token,
access_token: token.map(DeviceToken::into_token),
device_id: device,
refresh_token: None,
expires_in: None,
+4 -9
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, at};
use futures::StreamExt;
use ruma::{
@@ -19,10 +18,9 @@
/// # `PUT /_matrix/client/../dehydrated_device`
///
/// Creates or overwrites the user's dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
#[tracing::instrument(skip_all)]
pub(crate) async fn put_dehydrated_device_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let device_id = body.device_id.clone();
@@ -38,10 +36,9 @@ pub(crate) async fn put_dehydrated_device_route(
/// # `DELETE /_matrix/client/../dehydrated_device`
///
/// Deletes the user's dehydrated device without replacement.
#[tracing::instrument(skip_all, fields(%client))]
#[tracing::instrument(skip_all)]
pub(crate) async fn delete_dehydrated_device_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -56,10 +53,9 @@ pub(crate) async fn delete_dehydrated_device_route(
/// # `GET /_matrix/client/../dehydrated_device`
///
/// Gets the user's dehydrated device
#[tracing::instrument(skip_all, fields(%client))]
#[tracing::instrument(skip_all)]
pub(crate) async fn get_dehydrated_device_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -72,10 +68,9 @@ pub(crate) async fn get_dehydrated_device_route(
/// # `GET /_matrix/client/../dehydrated_device/{device_id}/events`
///
/// Paginates the events of the dehydrated device.
#[tracing::instrument(skip_all, fields(%client))]
#[tracing::instrument(skip_all)]
pub(crate) async fn get_dehydrated_events_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.identity.expect_sender_user()?;
+7 -11
View File
@@ -1,15 +1,14 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, debug, err, utils};
use conduwuit::{Err, Result, debug, err};
use futures::StreamExt;
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId,
MilliSecondsSinceUnixEpoch,
api::client::device::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use crate::{Ruma, client::DEVICE_ID_LENGTH};
use crate::{Ruma, client_ip::ClientIp};
/// # `GET /_matrix/client/r0/devices`
///
@@ -46,10 +45,10 @@ pub(crate) async fn get_device_route(
/// # `PUT /_matrix/client/r0/devices/{deviceId}`
///
/// Updates the metadata on a given device of the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "update_device", level = "debug")]
#[tracing::instrument(skip_all, name = "update_device", level = "debug")]
pub(crate) async fn update_device_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
ClientIp(client): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -85,16 +84,13 @@ pub(crate) async fn update_device_route(
appservice.registration.id
);
let device_id = OwnedDeviceId::from(utils::random_string(DEVICE_ID_LENGTH));
services
.users
.create_device(
sender_user,
&device_id,
&appservice.registration.as_token,
None,
Some(body.device_id.clone()),
None,
body.display_name.clone(),
Some(client.to_string()),
)
.await?;
+3 -7
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err, info,
utils::{
@@ -35,10 +34,9 @@
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
#[tracing::instrument(skip_all, name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_filtered_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms_filtered::v3::Request>,
) -> Result<get_public_rooms_filtered::v3::Response> {
if let Some(server) = &body.server {
@@ -71,10 +69,9 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// Lists the public rooms on this server.
///
/// - Rooms are ordered by the number of joined members
#[tracing::instrument(skip_all, fields(%client), name = "publicrooms", level = "info")]
#[tracing::instrument(skip_all, name = "publicrooms", level = "info")]
pub(crate) async fn get_public_rooms_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms::v3::Request>,
) -> Result<get_public_rooms::v3::Response> {
if let Some(server) = &body.server {
@@ -106,10 +103,9 @@ pub(crate) async fn get_public_rooms_route(
/// # `PUT /_matrix/client/r0/directory/list/room/{roomId}`
///
/// Sets the visibility of a given room in the room directory.
#[tracing::instrument(skip_all, fields(%client), name = "room_directory", level = "info")]
#[tracing::instrument(skip_all, name = "room_directory", level = "info")]
pub(crate) async fn set_room_visibility_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
-36
View File
@@ -1,7 +1,6 @@
use std::time::Duration;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
utils::{self, content_disposition::make_content_disposition, math::ruma_from_usize},
@@ -42,15 +41,8 @@ pub(crate) async fn get_media_config_route(
///
/// - Some metadata will be saved in the database
/// - Media will be saved in the media/ directory
#[tracing::instrument(
name = "media_upload",
level = "debug",
skip_all,
fields(%client),
)]
pub(crate) async fn create_content_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> {
let user = body.identity.expect_sender_user()?;
@@ -81,15 +73,8 @@ pub(crate) async fn create_content_route(
/// # `GET /_matrix/client/v1/media/thumbnail/{serverName}/{mediaId}`
///
/// Load media thumbnail from our server or over federation.
#[tracing::instrument(
name = "media_thumbnail_get",
level = "debug",
skip_all,
fields(%client),
)]
pub(crate) async fn get_content_thumbnail_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let user = body.identity.expect_sender_user()?;
@@ -131,15 +116,8 @@ pub(crate) async fn get_content_thumbnail_route(
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}`
///
/// Load media from our server or over federation.
#[tracing::instrument(
name = "media_get",
level = "debug",
skip_all,
fields(%client),
)]
pub(crate) async fn get_content_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let user = body.identity.expect_sender_user()?;
@@ -178,15 +156,8 @@ pub(crate) async fn get_content_route(
/// # `GET /_matrix/client/v1/media/download/{serverName}/{mediaId}/{fileName}`
///
/// Load media from our server or over federation as fileName.
#[tracing::instrument(
name = "media_get_af",
level = "debug",
skip_all,
fields(%client),
)]
pub(crate) async fn get_content_as_filename_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> {
let user = body.identity.expect_sender_user()?;
@@ -229,15 +200,8 @@ pub(crate) async fn get_content_as_filename_route(
/// # `GET /_matrix/client/v1/media/preview_url`
///
/// Returns URL preview.
#[tracing::instrument(
name = "url_preview",
level = "debug",
skip_all,
fields(%client),
)]
pub(crate) async fn get_media_preview_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> {
let sender_user = body.identity.expect_sender_user()?;
+5 -20
View File
@@ -1,7 +1,6 @@
#![allow(deprecated)]
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
utils::{content_disposition::make_content_disposition, math::ruma_from_usize},
@@ -50,10 +49,8 @@ pub(crate) async fn get_media_config_legacy_legacy_route(
/// # `GET /_matrix/media/v3/preview_url`
///
/// Returns URL preview.
#[tracing::instrument(skip_all, fields(%client), name = "url_preview_legacy", level = "debug")]
pub(crate) async fn get_media_preview_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -95,10 +92,9 @@ pub(crate) async fn get_media_preview_legacy_route(
/// Returns URL preview.
pub(crate) async fn get_media_preview_legacy_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<RumaResponse<get_media_preview::v3::Response>> {
get_media_preview_legacy_route(State(services), ClientIp(client), body)
get_media_preview_legacy_route(State(services), body)
.await
.map(RumaResponse)
}
@@ -115,10 +111,9 @@ pub(crate) async fn get_media_preview_legacy_legacy_route(
/// - Media will be saved in the media/ directory
pub(crate) async fn create_content_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<RumaResponse<create_content::v3::Response>> {
create_content_route(State(services), ClientIp(client), body)
create_content_route(State(services), body)
.await
.map(RumaResponse)
}
@@ -131,10 +126,8 @@ pub(crate) async fn create_content_legacy_route(
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<get_content::v3::Response> {
let mxc = Mxc {
@@ -209,13 +202,11 @@ pub(crate) async fn get_content_legacy_route(
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_legacy_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content::v3::Request>,
) -> Result<RumaResponse<get_content::v3::Response>> {
get_content_legacy_route(State(services), ClientIp(client), body)
get_content_legacy_route(State(services), body)
.await
.map(RumaResponse)
}
@@ -228,10 +219,8 @@ pub(crate) async fn get_content_legacy_legacy_route(
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_get_legacy", level = "debug")]
pub(crate) async fn get_content_as_filename_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<get_content_as_filename::v3::Response> {
let mxc = Mxc {
@@ -307,10 +296,9 @@ pub(crate) async fn get_content_as_filename_legacy_route(
/// seconds
pub(crate) async fn get_content_as_filename_legacy_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v3::Request>,
) -> Result<RumaResponse<get_content_as_filename::v3::Response>> {
get_content_as_filename_legacy_route(State(services), ClientIp(client), body)
get_content_as_filename_legacy_route(State(services), body)
.await
.map(RumaResponse)
}
@@ -323,10 +311,8 @@ pub(crate) async fn get_content_as_filename_legacy_legacy_route(
/// - Only redirects if `allow_redirect` is true
/// - Uses client-provided `timeout_ms` if available, else defaults to 20
/// seconds
#[tracing::instrument(skip_all, fields(%client), name = "media_thumbnail_get_legacy", level = "debug")]
pub(crate) async fn get_content_thumbnail_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<get_content_thumbnail::v3::Response> {
let mxc = Mxc {
@@ -404,10 +390,9 @@ pub(crate) async fn get_content_thumbnail_legacy_route(
/// seconds
pub(crate) async fn get_content_thumbnail_legacy_legacy_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v3::Request>,
) -> Result<RumaResponse<get_content_thumbnail::v3::Response>> {
get_content_thumbnail_legacy_route(State(services), ClientIp(client), body)
get_content_thumbnail_legacy_route(State(services), body)
.await
.map(RumaResponse)
}
+3 -11
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
@@ -23,10 +22,9 @@
/// # `POST /_matrix/client/r0/rooms/{roomId}/invite`
///
/// Tries to send an invite event into the room.
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
#[tracing::instrument(skip_all, name = "invite", level = "info")]
pub(crate) async fn invite_user_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -42,14 +40,8 @@ pub(crate) async fn invite_user_route(
return Err!(Request(Forbidden("Invites are not allowed on this server.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
banned_room_check(&services, sender_user, Some(&body.room_id), body.room_id.server_name())
.await?;
match &body.recipient {
| invite_user::v3::InvitationRecipient::UserId(InviteUserId {
+7 -23
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug,
result::FlatOk,
@@ -22,10 +21,9 @@
/// rules locally
/// - If the server does not know about the room: asks other servers over
/// federation
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
#[tracing::instrument(skip_all, name = "join", level = "info")]
pub(crate) async fn join_room_by_id_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -33,14 +31,8 @@ pub(crate) async fn join_room_by_id_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
banned_room_check(
&services,
sender_user,
Some(&body.room_id),
body.room_id.server_name(),
client,
)
.await?;
banned_room_check(&services, sender_user, Some(&body.room_id), body.room_id.server_name())
.await?;
// There is no body.server_name for /roomId/join
let mut servers: Vec<_> = services
@@ -91,10 +83,9 @@ pub(crate) async fn join_room_by_id_route(
/// - If the server does not know about the room: use the server name query
/// param if specified. if not specified, asks other servers over federation
/// via room alias server name and room ID server name
#[tracing::instrument(skip_all, fields(%client), name = "join", level = "info")]
#[tracing::instrument(skip_all, name = "join", level = "info")]
pub(crate) async fn join_room_by_id_or_alias_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -105,15 +96,9 @@ pub(crate) async fn join_room_by_id_or_alias_route(
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.boxed()
.await?;
banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name())
.boxed()
.await?;
let mut servers = body.via.clone();
if servers.is_empty() {
@@ -159,7 +144,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
+3 -12
View File
@@ -1,7 +1,6 @@
use std::{borrow::Borrow, collections::HashMap, iter::once, sync::Arc};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug, debug_info, debug_warn, err, info,
matrix::{
@@ -45,10 +44,9 @@
/// # `POST /_matrix/client/*/knock/{roomIdOrAlias}`
///
/// Tries to knock the room to ask permission to join for the sender user.
#[tracing::instrument(skip_all, fields(%client), name = "knock", level = "info")]
#[tracing::instrument(skip_all, name = "knock", level = "info")]
pub(crate) async fn knock_room_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<knock_room::v3::Request>,
) -> Result<knock_room::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -59,14 +57,8 @@ pub(crate) async fn knock_room_route(
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias.clone()) {
| Ok(room_id) => {
banned_room_check(
&services,
sender_user,
Some(&room_id),
room_id.server_name(),
client,
)
.await?;
banned_room_check(&services, sender_user, Some(&room_id), room_id.server_name())
.await?;
let mut servers = body.via.clone();
servers.extend(
@@ -109,7 +101,6 @@ pub(crate) async fn knock_room_route(
sender_user,
Some(&room_id),
Some(room_alias.server_name()),
client,
)
.await?;
+2 -5
View File
@@ -8,8 +8,6 @@
mod members;
mod unban;
use std::net::IpAddr;
use axum::extract::State;
use conduwuit::{Err, Result, warn};
use futures::{FutureExt, StreamExt};
@@ -58,7 +56,6 @@ pub(crate) async fn banned_room_check(
user_id: &UserId,
room_id: Option<&RoomId>,
server_name: Option<&ServerName>,
client_ip: IpAddr,
) -> Result {
if services.users.is_admin(user_id).await {
return Ok(());
@@ -85,7 +82,7 @@ pub(crate) async fn banned_room_check(
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
room join"
))
.await;
}
@@ -121,7 +118,7 @@ pub(crate) async fn banned_room_check(
.admin
.send_text(&format!(
"Automatically deactivating user {user_id} due to attempted banned \
room join from IP {client_ip}"
room join"
))
.await;
}
+2 -3
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Error, Result, at, debug_warn,
matrix::{
@@ -38,7 +37,7 @@
};
use ruminuwuity::invite_permission_config::FilterLevel;
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// list of safe and common non-state events to ignore if the user is ignored
const IGNORED_MESSAGE_TYPES: &[TimelineEventType] = &[
@@ -72,7 +71,7 @@
/// where the user was joined, depending on `history_visibility`)
pub(crate) async fn get_message_events_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
-3
View File
@@ -90,8 +90,5 @@
pub(super) use voip::*;
pub(super) use well_known::*;
/// generated device ID length
const DEVICE_ID_LENGTH: usize = 10;
/// generated user access token length
const TOKEN_LENGTH: usize = 32;
+33 -3
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::api::client::membership::mutual_rooms;
use ruma::{OwnedRoomId, api::client::membership::mutual_rooms};
use crate::Ruma;
@@ -11,14 +11,14 @@
///
/// An implementation of [MSC2666](https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
#[tracing::instrument(skip_all, name = "mutual_rooms", level = "info")]
pub(crate) async fn get_mutual_rooms_route(
pub(crate) async fn get_mutual_rooms_unstable_route(
State(services): State<crate::State>,
body: Ruma<mutual_rooms::unstable::Request>,
) -> Result<mutual_rooms::unstable::Response> {
let sender_user = body.identity.expect_sender_user()?;
if sender_user == body.user_id {
return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
return Err!(Request(InvalidParam("You cannot request rooms in common with yourself.")));
}
let mutual_rooms = services
@@ -30,3 +30,33 @@ pub(crate) async fn get_mutual_rooms_route(
Ok(mutual_rooms::unstable::Response::new(mutual_rooms))
}
/// # `GET /_matrix/client/v1/mutual_rooms`
///
/// Gets all the rooms the sender shares with the specified user.
#[tracing::instrument(skip_all, name = "mutual_rooms", level = "info")]
pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
body: Ruma<mutual_rooms::v1::Request>,
) -> Result<mutual_rooms::v1::Response> {
let sender_user = body.identity.expect_sender_user()?;
if sender_user == body.user_id {
return Err!(Request(InvalidParam("You cannot request rooms in common with yourself.")));
}
let mutual_rooms: Vec<OwnedRoomId> = services
.rooms
.state_cache
.get_shared_rooms(sender_user, &body.user_id)
.collect()
.await;
Ok(mutual_rooms::v1::Response::new(
mutual_rooms
.len()
.try_into()
.expect("user should be in fewer than 9.1 quadrillion rooms"),
mutual_rooms,
))
}
+13
View File
@@ -0,0 +1,13 @@
use axum::{Form, Json, extract::State, response::IntoResponse};
use http::StatusCode;
use service::oauth::grant::DeviceCodeRequest;
pub(crate) async fn device_authorization_route(
State(services): State<crate::State>,
Form(request): Form<DeviceCodeRequest>,
) -> impl IntoResponse {
match services.oauth.request_device_code(request).await {
| Ok(response) => Ok(Json(response)),
| Err(err) => Err((StatusCode::BAD_REQUEST, Json(err))),
}
}
+3
View File
@@ -10,6 +10,7 @@
use serde_json::json;
pub(crate) use server_metadata::*;
mod device;
mod register_client;
mod server_metadata;
mod token;
@@ -20,6 +21,7 @@
const CLIENT_REGISTER_PATH: &str = "client/register";
const TOKEN_REVOKE_PATH: &str = "client/revoke";
const TOKEN_PATH: &str = "grant/token";
const DEVICE_AUTHORIZATION_PATH: &str = "device";
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
@@ -53,4 +55,5 @@ fn oauth_router() -> Router<crate::State> {
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
.route(concat!("/", TOKEN_PATH), post(token::token_route))
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
.route(concat!("/", DEVICE_AUTHORIZATION_PATH), post(device::device_authorization_route))
}
+1 -1
View File
@@ -24,5 +24,5 @@ pub(crate) async fn register_client_route(
.await
.map_err(|err| (StatusCode::BAD_REQUEST, Json(err)).into_response())?;
Ok(Json(RegisteredClient { client_id, metadata }).into_response())
Ok((StatusCode::CREATED, Json(RegisteredClient { client_id, metadata })).into_response())
}
+16 -4
View File
@@ -12,8 +12,8 @@
use crate::{
Ruma,
client::oauth::{
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
TOKEN_REVOKE_PATH,
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, DEVICE_AUTHORIZATION_PATH,
JWKS_URI_PATH, TOKEN_PATH, TOKEN_REVOKE_PATH,
},
};
@@ -37,6 +37,17 @@ pub(crate) async fn authorization_server_metadata(services: &Services) -> Value
.join(super::BASE_PATH)
.unwrap();
let prompt_values_supported = if services
.uiaa
.registration_flow_status()
.await
.any_available()
{
json!(["create"])
} else {
json!([])
};
json!({
"account_management_uri": endpoint_base.join(ACCOUNT_MANAGEMENT_PATH).unwrap(),
"account_management_actions_supported": [
@@ -49,10 +60,11 @@ pub(crate) async fn authorization_server_metadata(services: &Services) -> Value
],
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"device_authorization_endpoint": endpoint_base.join(DEVICE_AUTHORIZATION_PATH).unwrap(),
"grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
"issuer": services.config.get_client_domain(),
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
"prompt_values_supported": ["create"],
"prompt_values_supported": prompt_values_supported,
"registration_endpoint": endpoint_base.join(CLIENT_REGISTER_PATH).unwrap(),
"response_modes_supported": ["query", "fragment"],
"response_types_supported": ["code"],
+2 -3
View File
@@ -1,7 +1,6 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, PduCount, Result, err};
use ruma::{
MilliSecondsSinceUnixEpoch,
@@ -13,7 +12,7 @@
},
};
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `POST /_matrix/client/r0/rooms/{roomId}/read_markers`
///
@@ -117,7 +116,7 @@ pub(crate) async fn set_read_marker_route(
/// Sets private read marker and public read receipt EDU.
pub(crate) async fn create_receipt_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<create_receipt::v3::Request>,
) -> Result<create_receipt::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+2 -3
View File
@@ -1,11 +1,10 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, matrix::pdu::PartialPdu};
use ruma::{
api::client::redact::redact_event, assign, events::room::redaction::RoomRedactionEventContent,
};
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/redact/{eventId}/{txnId}`
///
@@ -14,7 +13,7 @@
/// - TODO: Handle txn id
pub(crate) async fn redact_event_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: required for updating device metadata
body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+3 -7
View File
@@ -1,7 +1,6 @@
use std::{fmt::Write as _, time::Duration};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Event, Result, debug_info, info, matrix::pdu::PduEvent, utils::ReadyExt};
use conduwuit_service::Services;
use ruma::{
@@ -30,10 +29,9 @@ struct Report {
/// # `POST /_matrix/client/v3/rooms/{roomId}/report`
///
/// Reports an abusive room to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_room", level = "info")]
#[tracing::instrument(skip_all, name = "report_room", level = "info")]
pub(crate) async fn report_room_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -85,10 +83,9 @@ pub(crate) async fn report_room_route(
/// # `POST /_matrix/client/v3/rooms/{roomId}/report/{eventId}`
///
/// Reports an inappropriate event to homeserver admins
#[tracing::instrument(skip_all, fields(%client), name = "report_event", level = "info")]
#[tracing::instrument(skip_all, name = "report_event", level = "info")]
pub(crate) async fn report_event_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> {
// user authentication
@@ -129,10 +126,9 @@ pub(crate) async fn report_event_route(
Ok(report_content::v3::Response::new())
}
#[tracing::instrument(skip_all, fields(%client), name = "report_user", level = "info")]
#[tracing::instrument(skip_all, name = "report_user", level = "info")]
pub(crate) async fn report_user_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<report_user::v3::Request>,
) -> Result<report_user::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+36 -21
View File
@@ -2,7 +2,7 @@
use axum::extract::State;
use conduwuit::{
Err, Result, debug, debug_info, err, info,
Err, Error, Result, debug, debug_info, err, info,
matrix::{StateKey, pdu::PartialPdu},
trace, warn,
};
@@ -11,10 +11,13 @@
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, Int, MilliSecondsSinceUnixEpoch, OwnedRoomAliasId,
OwnedUserId, RoomAliasId, RoomId, RoomVersionId, UserId,
api::client::room::{self, create_room},
api::{
client::room::{self, create_room},
error::ErrorKind::Forbidden,
},
assign,
events::{
TimelineEventType,
AnyStateEventContent, StateEventType, TimelineEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
@@ -512,30 +515,42 @@ pub(crate) async fn create_room_route(
// 7. Events listed in initial_state
for event in &body.initial_state {
let mut partial_pdu = event
.deserialize_as_unchecked::<PartialPdu>()
.map_err(|e| {
err!(Request(InvalidParam(warn!("Invalid initial state event: {e:?}"))))
let event_type: StateEventType = event.get_field("type")?.ok_or_else(|| {
err!(Request(InvalidParam(warn!(
r#"Invalid initial state event: missing "type" field "#
))))
})?;
let content: Raw<AnyStateEventContent> =
event.get_field("content")?.ok_or_else(|| {
err!(Request(InvalidParam(warn!(
r#"Invalid initial state event: missing "content" field "#
))))
})?;
debug_info!("Room creation initial state event: {event:?}");
let state_key: String = event.get_field("state_key")?.unwrap_or_default();
// Implicit state key defaults to ""
partial_pdu.state_key.get_or_insert_with(StateKey::new);
// Silently skip encryption events if they are not allowed
if partial_pdu.event_type == TimelineEventType::RoomEncryption
&& !services.config.allow_encryption
{
continue;
}
services
match services
.rooms
.timeline
.build_and_append_pdu(partial_pdu, sender_user, Some(&room_id), &state_lock)
.send_state_event_for_key(
sender_user,
&room_id,
&state_lock,
&event_type,
&content,
&state_key,
None,
)
.boxed()
.await?;
.await
{
| Err(Error::Request(Forbidden, ..)) => {
// Silently skip forbidden events
continue;
},
| r => r,
}?;
}
// 8. Events implied by name and topic
+1 -3
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result};
use ruma::api::client::room::get_summary;
use service::rooms::summary::Accessibility;
@@ -9,10 +8,9 @@
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
///
/// Returns a short description of the state of a room.
#[tracing::instrument(skip_all, fields(%client), name = "room_summary", level = "info")]
#[tracing::instrument(skip_all, name = "room_summary", level = "info")]
pub(crate) async fn get_room_summary(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_summary::v1::Request>,
) -> Result<get_summary::v1::Response> {
let (room_id, servers) = services
+2 -3
View File
@@ -1,12 +1,11 @@
use std::collections::BTreeMap;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu, utils};
use ruma::{api::client::message::send_message_event, events::MessageLikeEventType};
use serde_json::from_str;
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `PUT /_matrix/client/v3/rooms/{roomId}/send/{eventType}/{txnId}`
///
@@ -19,7 +18,7 @@
/// allowed
pub(crate) async fn send_message_event_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: required for updating device metadata
body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+24 -34
View File
@@ -1,10 +1,9 @@
use std::time::Duration;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug, err, info,
utils::{self, ReadyExt, stream::BroadbandExt},
utils::{ReadyExt, stream::BroadbandExt},
warn,
};
use conduwuit_service::Services;
@@ -29,18 +28,17 @@
},
assign,
};
use service::users::DeviceToken;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `GET /_matrix/client/v3/login`
///
/// Get the supported login types of this server. One of these should be used as
/// the `type` field when logging in.
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
#[tracing::instrument(skip_all, name = "login", level = "info")]
pub(crate) async fn get_login_types_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
_body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
if !services.config.oauth.compatibility_mode().uiaa_available() {
@@ -114,10 +112,10 @@ pub async fn handle_login(
/// Note: You can use [`GET
/// /_matrix/client/r0/login`](fn.get_supported_versions_route.html) to see
/// supported login types.
#[tracing::instrument(skip_all, fields(%client), name = "login", level = "info")]
#[tracing::instrument(skip_all, name = "login", level = "info")]
pub(crate) async fn login_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
ClientIp(client): ClientIp, // NOTE: Required for device metadata
body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> {
if !services.config.oauth.compatibility_mode().uiaa_available() {
@@ -190,44 +188,39 @@ pub(crate) async fn login_route(
},
};
// Generate new device id if the user didn't specify one
let device_id = body
.device_id
.clone()
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate a new token for the device (ensuring no collisions)
let token = services.users.generate_unique_token().await;
// Generate a new token for the device
let token = DeviceToken::new_random();
// Determine if device_id was provided and exists in the db for this user
let device_exists = if body.device_id.is_some() {
let existing_device_id = if let Some(device_id) = &body.device_id {
services
.users
.all_device_ids(&user_id)
.ready_any(|v| v == device_id)
.ready_find(|v| v == device_id)
.await
} else {
false
None
};
if device_exists {
let device_id = if let Some(existing_device_id) = existing_device_id {
services
.users
.set_token(&user_id, &device_id, &token, None)
.set_token(&user_id, &existing_device_id, token.clone())
.await?;
existing_device_id
} else {
services
.users
.create_device(
&user_id,
&device_id,
&token,
None,
body.device_id.clone(),
Some(token.clone()),
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
.await?;
}
.await?
};
// send client well-known if specified so the client knows to reconfigure itself
let client_discovery_info: Option<DiscoveryInfo> = services
@@ -241,7 +234,7 @@ pub(crate) async fn login_route(
info!("{user_id} logged in");
#[allow(deprecated)]
Ok(assign!(login::v3::Response::new(user_id, token, device_id), {
Ok(assign!(login::v3::Response::new(user_id, token.into_token(), device_id), {
well_known: client_discovery_info,
expires_in: None,
home_server: Some(services.config.server_name.clone()),
@@ -255,10 +248,9 @@ pub(crate) async fn login_route(
/// to log in with the m.login.token flow.
///
/// <https://spec.matrix.org/v1.13/client-server-api/#post_matrixclientv1loginget_token>
#[tracing::instrument(skip_all, fields(%client), name = "login_token", level = "info")]
#[tracing::instrument(skip_all, name = "login_token", level = "info")]
pub(crate) async fn login_token_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_login_token::v1::Request>,
) -> Result<get_login_token::v1::Response> {
if !services.config.login_via_existing_session {
@@ -273,7 +265,7 @@ pub(crate) async fn login_token_route(
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
.await?;
let login_token = utils::random_string(TOKEN_LENGTH);
let login_token = DeviceToken::new_random().into_token();
let expires_in = services.users.create_login_token(sender_user, &login_token);
Ok(get_login_token::v1::Response::new(
@@ -291,10 +283,9 @@ pub(crate) async fn login_token_route(
/// last seen ts)
/// - Forgets to-device events
/// - Triggers device list updates
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
#[tracing::instrument(skip_all, name = "logout", level = "info")]
pub(crate) async fn logout_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -339,10 +330,9 @@ pub(crate) async fn logout_route(
/// Note: This is equivalent to calling [`GET
/// /_matrix/client/r0/logout`](fn.logout_route.html) from each device of this
/// user.
#[tracing::instrument(skip_all, fields(%client), name = "logout", level = "info")]
#[tracing::instrument(skip_all, name = "logout", level = "info")]
pub(crate) async fn logout_all_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+4 -3
View File
@@ -19,9 +19,10 @@ pub(crate) async fn get_hierarchy_route(
// there's no reasonable way to handle a space hierarchy changing during
// pagination.
let max_depth = body
.max_depth
.map(|max_depth| max_depth.min(UInt::from(MAX_MAX_DEPTH)));
// Default to MAX_MAX_DEPTH when the client doesn't specify one, so an
// unbounded traversal can never happen.
let max_max_depth = UInt::from(MAX_MAX_DEPTH);
let max_depth = Some(body.max_depth.unwrap_or(max_max_depth).min(max_max_depth));
let hierarchy = services
.rooms
+30 -290
View File
@@ -1,41 +1,25 @@
#[cfg(test)]
mod tests;
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, err,
matrix::{Event, pdu::PartialPdu},
};
use conduwuit_service::Services;
use conduwuit::{Err, Result, err, matrix::Event};
use futures::{FutureExt, TryStreamExt};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
RoomId,
api::client::state::{
get_state_event_for_key::{self, v3::StateEventFormat},
get_state_events, send_state_event,
},
events::{
AnyStateEventContent, StateEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
server_acl::RoomServerAclEventContent,
},
},
serde::Raw,
};
use serde_json::{json, value::to_raw_value};
use crate::{Ruma, RumaResponse};
use crate::{Ruma, RumaResponse, client_ip::ClientIp};
/// # `PUT /_matrix/client/*/rooms/{roomId}/state/{eventType}/{stateKey}`
///
/// Sends a state event into the room.
pub(crate) async fn send_state_event_for_key_route(
State(services): State<crate::State>,
ClientIp(ip): ClientIp,
ClientIp(ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<send_state_event::v3::Request>,
) -> Result<send_state_event::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -48,21 +32,31 @@ pub(crate) async fn send_state_event_for_key_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
let event_id = send_state_event_for_key_helper(
&services,
sender_user,
&body.room_id,
&body.event_type,
&body.body.body,
&body.state_key,
if body.identity.is_appservice() {
body.timestamp
} else {
None
},
)
.boxed()
.await?;
let state_lock = services
.rooms
.state
.mutex
.lock::<RoomId>(&body.room_id)
.await;
let event_id = services
.rooms
.timeline
.send_state_event_for_key(
sender_user,
&body.room_id,
&state_lock,
&body.event_type,
&body.body.body,
&body.state_key,
if body.identity.is_appservice() {
body.timestamp
} else {
None
},
)
.boxed()
.await?;
Ok(send_state_event::v3::Response::new(event_id))
}
@@ -72,7 +66,7 @@ pub(crate) async fn send_state_event_for_key_route(
/// Sends a state event into the room.
pub(crate) async fn send_state_event_for_empty_key_route(
State(services): State<crate::State>,
ClientIp(ip): ClientIp,
ClientIp(ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<send_state_event::v3::Request>,
) -> Result<RumaResponse<send_state_event::v3::Response>> {
send_state_event_for_key_route(State(services), ClientIp(ip), body)
@@ -186,257 +180,3 @@ pub(crate) async fn get_state_events_for_empty_key_route(
.await
.map(RumaResponse)
}
async fn send_state_event_for_key_helper(
services: &Services,
sender: &UserId,
room_id: &RoomId,
event_type: &StateEventType,
json: &Raw<AnyStateEventContent>,
state_key: &str,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Result<OwnedEventId> {
let json: &mut Raw<AnyStateEventContent> = &mut json.clone();
allowed_to_send_state_event(services, room_id, event_type, state_key, json).await?;
let state_lock = services.rooms.state.mutex.lock(room_id).await;
let event_id = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu {
event_type: event_type.to_string().into(),
content: serde_json::from_str(json.json().get())?,
state_key: Some(state_key.into()),
timestamp,
..Default::default()
},
sender,
Some(room_id),
&state_lock,
)
.await?;
Ok(event_id)
}
async fn allowed_to_send_state_event(
services: &Services,
room_id: &RoomId,
event_type: &StateEventType,
state_key: &str,
json: &mut Raw<AnyStateEventContent>,
) -> Result {
match event_type {
| StateEventType::RoomCreate => {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"You cannot update m.room.create after a room has been created."
))));
},
| StateEventType::RoomServerAcl => {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
match json.deserialize_as_unchecked::<RoomServerAclEventContent>() {
| Ok(acl_content) => {
let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*");
let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*");
let allow_has_server = acl_content
.allow
.iter()
.any(|entry| entry == services.globals.server_name().as_str());
if acl_content.allow.is_empty() {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with an empty allow key will permanently \
brick the room for non-conduwuit's as this equates to no servers \
being allowed to participate in this room."
))));
}
if allow_has_wildcard && deny_has_wildcard {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
permanently brick the room for non-conduwuit's as this equates to \
no servers being allowed to participate in this room."
))));
}
if deny_has_wildcard
&& !acl_content.is_allowed(services.globals.server_name())
&& !allow_has_server
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with a deny key value of \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
if !allow_has_wildcard
&& !acl_content.is_allowed(services.globals.server_name())
&& !allow_has_server
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event for an allow key without \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room server ACL event is invalid: {e}"
))));
},
}
},
| StateEventType::RoomEncryption =>
// Forbid m.room.encryption if encryption is disabled
if !services.config.allow_encryption {
return Err!(Request(Forbidden("Encryption is disabled on this homeserver.")));
},
| StateEventType::RoomJoinRules => {
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
if admin_room_id == room_id {
match json.deserialize_as_unchecked::<RoomJoinRulesEventContent>() {
| Ok(join_rule) =>
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
"Admin room is a sensitive room, it cannot be made public"
)));
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room join rules event is invalid: {e}"
))));
},
}
}
}
},
| StateEventType::RoomHistoryVisibility => {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = services.admin.get_admin_room().await {
match json.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>() {
| Ok(visibility_content) => {
if admin_room_id == room_id
&& visibility_content.history_visibility
== HistoryVisibility::WorldReadable
{
return Err!(Request(Forbidden(
"Admin room is a sensitive room, it cannot be made world \
readable (public room history)."
)));
}
},
| Err(e) => {
return Err!(Request(BadJson(debug_warn!(
"Room history visibility event is invalid: {e}"
))));
},
}
}
},
| StateEventType::RoomCanonicalAlias => {
match json.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>() {
| Ok(canonical_alias_content) => {
let mut aliases = canonical_alias_content.alt_aliases.clone();
if let Some(alias) = canonical_alias_content.alias {
aliases.push(alias);
}
for alias in aliases {
let (alias_room_id, _) = services
.rooms
.alias
.resolve_alias(&alias)
.await
.map_err(|e| {
err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}")))
})?;
if alias_room_id != room_id {
return Err!(Request(BadAlias(
"Room alias {alias} does not belong to room {room_id}"
)));
}
}
},
| Err(e) => {
return Err!(Request(InvalidParam(debug_warn!(
"Room canonical alias event is invalid: {e}"
))));
},
}
},
| StateEventType::RoomMember =>
match json.deserialize_as_unchecked::<RoomMemberEventContent>() {
| Ok(mut membership_content) => {
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
if let Some(authorising_user) =
membership_content.join_authorized_via_users_server
{
// join_authorized_via_users_server must be thrown away, if user is
// already a member of the room.
if services
.rooms
.state_cache
.is_joined(&state_key, room_id)
.await
{
membership_content.join_authorized_via_users_server = None;
*json = Raw::<AnyStateEventContent>::from_json_string(
serde_json::to_string(&membership_content)?,
)?;
return Ok(());
}
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if !services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this \
homeserver"
)));
}
if !services
.rooms
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they \
cannot authorise the join."
)));
}
}
},
| Err(e) => {
return Err!(Request(BadJson(
"Membership content must have a valid JSON body with at least a valid \
membership state: {e}"
)));
},
},
| _ => (),
}
Ok(())
}
+5
View File
@@ -1,3 +1,8 @@
use ruma::{
events::{AnyStateEventContent, room::member::RoomMemberEventContent},
serde::Raw,
};
use super::*;
#[test]
+15 -4
View File
@@ -269,13 +269,22 @@ async fn build_state_and_timeline(
)
.await?;
let (state_events, notification_counts, joined_since_last_sync) = try_join3(
build_state_events(services, sync_context, room_id, shortstatehashes, &timeline),
let (notification_counts, joined_since_last_sync) = try_join(
build_notification_counts(services, sync_context, room_id, &timeline),
check_joined_since_last_sync(services, shortstatehashes, sync_context),
)
.await?;
let state_events = build_state_events(
services,
sync_context,
room_id,
shortstatehashes,
&timeline,
joined_since_last_sync,
)
.await?;
// the timeline should always include at least one PDU if the syncing user
// joined since the last sync, that being the syncing user's join event. if
// it's empty something is wrong.
@@ -459,6 +468,7 @@ async fn build_state_events(
room_id: &RoomId,
shortstatehashes: ShortStateHashes,
timeline: &TimelinePdus,
joined_since_last_sync: bool,
) -> Result<Vec<PduEvent>> {
let SyncContext {
syncing_user,
@@ -488,9 +498,10 @@ async fn build_state_events(
/*
if `last_sync_end_count` is Some (meaning this is an incremental sync), and `last_sync_end_shortstatehash`
is Some (meaning the syncing user didn't just join this room for the first time ever), and `full_state` is false,
then use `build_state_incremental`.
and the user didn't just join the room since the last sync, then use `build_state_incremental`.
*/
| (Some(_), Some(last_sync_end_shortstatehash)) if !full_state =>
| (Some(_), Some(last_sync_end_shortstatehash))
if !full_state && !joined_since_last_sync =>
build_state_incremental(
services,
syncing_user,
+15 -1
View File
@@ -230,7 +230,7 @@ async fn build_left_state_and_timeline(
.and_then(|limit| limit.try_into().ok())
.unwrap_or(DEFAULT_TIMELINE_LIMIT);
let timeline = load_timeline(
let mut timeline = load_timeline(
services,
syncing_user,
room_id,
@@ -240,6 +240,20 @@ async fn build_left_state_and_timeline(
)
.await?;
// the timeline index can lag behind the leave PDU, so `load_timeline` may omit
// it. we already hold the leave event, so ensure it's always synced to the
// client.
let leave_synced = timeline
.pdus
.iter()
.any(|(_, pdu)| pdu.event_id() == leave_membership_event.event_id());
if !leave_synced {
timeline
.pdus
.push_back((timeline_end_count, leave_membership_event));
}
let lazily_loaded_members =
prepare_lazily_loaded_members(services, sync_context, room_id, timeline.senders()).await;
+2 -2
View File
@@ -9,7 +9,6 @@
};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, at, error, extract_variant,
utils::{
@@ -48,6 +47,7 @@
is_ignored_invite,
sync::v3::{joined::load_joined_room, left::load_left_room},
},
client_ip::ClientIp,
};
/// The default maximum number of events to return in the `timeline` key of
@@ -181,7 +181,7 @@ fn lazy_loading_enabled(&self) -> bool {
)]
pub(crate) async fn sync_events_route(
State(services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> {
let sender_user = body.identity.expect_sender_user()?;
+129 -29
View File
@@ -1,12 +1,11 @@
use std::{
cmp::{self, Ordering},
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
ops::Deref,
time::Duration,
};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Error, Result, at, error, extract_variant, is_equal_to,
matrix::{Event, TypeStateKey, pdu::PduCount},
@@ -49,6 +48,7 @@
client::{
DEFAULT_BUMP_TYPES, TimelinePdus, ignored_filter, is_ignored_invite, sync::load_timeline,
},
client_ip::ClientIp,
};
type SyncInfo<'a> = (&'a UserId, &'a DeviceId, u64, &'a sync_events::v5::Request);
@@ -67,7 +67,7 @@
/// [MSC4186]: https://github.com/matrix-org/matrix-spec-proposals/pull/4186
pub(crate) async fn sync_events_v5_route(
State(ref services): State<crate::State>,
ClientIp(client_ip): ClientIp,
ClientIp(client_ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<sync_events::v5::Request>,
) -> Result<sync_events::v5::Response> {
let sender_user = body.identity.expect_sender_user()?;
@@ -138,6 +138,13 @@ pub(crate) async fn sync_events_v5_route(
let (all_joined_rooms, all_invited_rooms, all_knocked_rooms) =
join3(all_joined_rooms, all_invited_rooms, all_knocked_rooms).await;
let allowed_rooms: BTreeSet<OwnedRoomId> = all_joined_rooms
.iter()
.chain(all_invited_rooms.iter())
.chain(all_knocked_rooms.iter())
.cloned()
.collect();
let all_joined_rooms = all_joined_rooms.iter().map(AsRef::as_ref);
let all_invited_rooms = all_invited_rooms.iter().map(AsRef::as_ref);
let all_knocked_rooms = all_knocked_rooms.iter().map(AsRef::as_ref);
@@ -190,29 +197,42 @@ pub(crate) async fn sync_events_v5_route(
)
.await;
fetch_subscriptions(services, sync_info, &known_rooms, &mut todo_rooms).await;
fetch_subscriptions(services, sync_info, &known_rooms, &allowed_rooms, &mut todo_rooms).await;
response.rooms = process_rooms(
services,
sender_user,
next_batch,
all_invited_rooms.clone(),
all_knocked_rooms.clone(),
&todo_rooms,
&mut response,
&body,
)
.await?;
if response.rooms.iter().all(|(id, r)| {
let no_account_data = response.extensions.account_data.global.is_empty()
&& response
.extensions
.account_data
.rooms
.values()
.all(Vec::is_empty);
let no_room_data = response.rooms.iter().all(|(id, r)| {
r.timeline.is_empty()
&& r.required_state.is_empty()
&& r.invite_state.is_none()
&& !response.extensions.receipts.rooms.contains_key(id)
}) && response
});
let no_to_device_messages = response
.extensions
.to_device
.clone()
.is_none_or(|to| to.events.is_empty())
{
.is_none_or(|to| to.events.is_empty());
if no_account_data && no_room_data && no_to_device_messages {
// Hang a few seconds so requests are not spammed
// Stop hanging if new info arrives
let default = Duration::from_secs(30);
@@ -236,10 +256,17 @@ async fn fetch_subscriptions(
services: &Services,
(sender_user, sender_device, globalsince, body): SyncInfo<'_>,
known_rooms: &KnownRooms,
allowed_rooms: &BTreeSet<OwnedRoomId>,
todo_rooms: &mut TodoRooms,
) {
let mut known_subscription_rooms = BTreeSet::new();
for (room_id, room) in &body.room_subscriptions {
// Silently ignore subscriptions to rooms the user is not a member of
// (joined or invited).
if !allowed_rooms.contains(room_id) {
continue;
}
let not_exists = services.rooms.metadata.exists(room_id).eq(&false);
let is_disabled = services.rooms.metadata.is_disabled(room_id);
@@ -398,11 +425,13 @@ async fn handle_lists<'a, Rooms, AllRooms>(
BTreeMap::default()
}
#[allow(clippy::too_many_arguments)]
async fn process_rooms<'a, Rooms>(
services: &Services,
sender_user: &UserId,
next_batch: u64,
all_invited_rooms: Rooms,
all_knocked_rooms: Rooms,
todo_rooms: &TodoRooms,
response: &mut sync_events::v5::Response,
body: &sync_events::v5::Request,
@@ -415,38 +444,101 @@ async fn process_rooms<'a, Rooms>(
let roomsincecount = PduCount::Normal(*roomsince);
let mut timestamp: Option<_> = None;
let mut invite_state = None;
let (timeline_pdus, limited);
let new_room_id: &RoomId = (*room_id).as_ref();
if all_invited_rooms.clone().any(is_equal_to!(new_room_id)) {
let Ok(invite_count) = services
.rooms
.state_cache
.get_invite_count(room_id, sender_user)
.await
else {
continue;
};
if *roomsince >= invite_count {
continue;
}
// TODO: figure out a timestamp we can use for remote invites
invite_state = services
let invite_state = services
.rooms
.state_cache
.invite_state(sender_user, room_id)
.await
.ok();
(timeline_pdus, limited) = (VecDeque::new(), true);
} else {
TimelinePdus { pdus: timeline_pdus, limited } = match load_timeline(
services,
sender_user,
room_id,
Some(roomsincecount),
Some(PduCount::from(next_batch)),
*timeline_limit,
)
.await
{
| Ok(value) => value,
| Err(err) => {
warn!("Encountered missing timeline in {}, error {}", room_id, err);
continue;
},
};
rooms.insert(
room_id.clone(),
assign!(sync_events::v5::response::Room::new(), {
initial: Some(roomsince == &0),
invite_state,
limited: true,
}),
);
continue;
}
if all_knocked_rooms.clone().any(is_equal_to!(new_room_id)) {
let Ok(knock_count) = services
.rooms
.state_cache
.get_knock_count(room_id, sender_user)
.await
else {
continue;
};
if *roomsince >= knock_count {
continue;
}
let Ok(knock_state) = services
.rooms
.state_cache
.knock_state(sender_user, room_id)
.await
else {
continue;
};
rooms.insert(
room_id.clone(),
assign!(sync_events::v5::response::Room::new(), {
initial: Some(roomsince == &0),
invite_state: Some(knock_state),
limited: true,
}),
);
continue;
}
if !services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
continue;
}
TimelinePdus { pdus: timeline_pdus, limited } = match load_timeline(
services,
sender_user,
room_id,
Some(roomsincecount),
Some(PduCount::from(next_batch)),
*timeline_limit,
)
.await
{
| Ok(value) => value,
| Err(err) => {
warn!("Encountered missing timeline in {}, error {}", room_id, err);
continue;
},
};
if body.extensions.account_data.enabled == Some(true) {
response.extensions.account_data.rooms.insert(
room_id.to_owned(),
@@ -630,7 +722,6 @@ async fn process_rooms<'a, Rooms>(
},
initial: Some(roomsince == &0),
is_dm: None,
invite_state,
unread_notifications: assign!(UnreadNotificationsCount::new(), {
highlight_count: Some(
services
@@ -758,6 +849,15 @@ async fn collect_typing_events(
let mut typing_response = sync_events::v5::response::Typing::default();
for (room_id, (_, _, roomsince)) in todo_rooms {
if !services
.rooms
.state_cache
.is_joined(sender_user, room_id)
.await
{
continue;
}
if services.rooms.typing.last_typing_update(room_id).await? <= *roomsince {
continue;
}
+2 -3
View File
@@ -1,16 +1,15 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, utils, utils::math::Tried};
use ruma::api::client::typing::create_typing_event::{self, v3::TypingInfo};
use crate::Ruma;
use crate::{Ruma, client_ip::ClientIp};
/// # `PUT /_matrix/client/r0/rooms/{roomId}/typing/{userId}`
///
/// Sets the typing state of the sender user.
pub(crate) async fn create_typing_event_route(
State(services): State<crate::State>,
ClientIp(ip): ClientIp,
ClientIp(ip): ClientIp, // NOTE: Required for updating device metadata
body: Ruma<create_typing_event::v3::Request>,
) -> Result<create_typing_event::v3::Response> {
use create_typing_event::v3::Typing;
+15 -1
View File
@@ -44,11 +44,25 @@ pub(crate) async fn continuwuity_server_version() -> Result<impl IntoResponse> {
///
/// conduwuit-specific API to return the amount of users registered on this
/// homeserver. Endpoint is disabled if federation is disabled for privacy. This
///
/// only includes active users (not deactivated, etc)
pub(crate) async fn continuwuity_local_user_count(
State(services): State<crate::State>,
) -> Result<impl IntoResponse> {
let user_count = services.users.stream_local_users().count().await;
let user_count = services
.users
.stream_local_users()
.filter_map(async |user_id| {
if !services.users.status(&user_id).await.is_active()
|| services.appservice.is_exclusive_user_id(&user_id).await
{
None
} else {
Some(user_id)
}
})
.count()
.await;
Ok(Json(serde_json::json!({
"count": user_count
+212
View File
@@ -0,0 +1,212 @@
use core::net::{IpAddr, SocketAddr};
use std::ops::Deref;
use axum::{
extract::{ConnectInfo, FromRequestParts},
response::{IntoResponse, Response},
};
use conduwuit::{debug_info, debug_warn};
use http::{HeaderMap, StatusCode, request::Parts};
use service::Services;
#[derive(Debug, PartialEq)]
pub enum ClientIpError {
Header(client_ip::Error),
Direct,
}
impl IntoResponse for ClientIpError {
fn into_response(self) -> Response {
let text = match self {
| Self::Header(e) => format!("{e}"),
| Self::Direct => "Failed to extract IP from ConnectionInfo".to_owned(),
};
(StatusCode::INTERNAL_SERVER_ERROR, text).into_response()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct ClientIp(pub IpAddr);
type PartsToIpFn = Box<dyn Fn(&Parts) -> Result<ClientIp, ClientIpError>>;
impl ClientIp {
fn from_header<T>(func: T) -> PartsToIpFn
where
T: Fn(&HeaderMap) -> Result<IpAddr, client_ip::Error> + 'static,
{
Box::new(move |parts: &Parts| {
func(&parts.headers)
.map(Self)
.map_err(ClientIpError::Header)
})
}
fn from_connection_info(parts: &Parts) -> Result<Self, ClientIpError> {
parts
.extensions
.get::<ConnectInfo<SocketAddr>>()
.ok_or_else(|| ClientIpError::Direct)
.map(|ConnectInfo(addr)| Self(addr.ip()))
}
fn for_source(source: &str) -> Option<PartsToIpFn> {
match source {
| "cf_connecting_ip" => Some(Self::from_header(client_ip::cf_connecting_ip)),
| "cloudfront_viewer_address" =>
Some(Self::from_header(client_ip::cloudfront_viewer_address)),
| "fly_client_ip" => Some(Self::from_header(client_ip::fly_client_ip)),
| "x_forwarded_for" => Some(Self::from_header(client_ip::rightmost_x_forwarded_for)),
| "true_client_ip" => Some(Self::from_header(client_ip::true_client_ip)),
| "x_envoy_external_address" =>
Some(Self::from_header(client_ip::x_envoy_external_address)),
| "x_real_ip" => Some(Self::from_header(client_ip::x_real_ip)),
| "direct" => Some(Box::new(Self::from_connection_info)),
| s => {
debug_warn!("Invalid client IP source option supplied, skipping: {s}");
None
},
}
}
fn for_config(
parts: &Parts,
accepted_ip_sources: &[String],
request_ip_source: Option<&String>,
) -> Result<Self, ClientIpError> {
accepted_ip_sources
.iter()
.filter_map(|s| Self::for_source(s))
.map(|f| f(parts))
.reduce(Result::or)
.or_else(|| {
debug_info!(
"No (valid) options set in `accepted_ip_sources`; falling back to \
`request_ip_source`"
);
request_ip_source
.and_then(|s| Self::for_source(s))
.map(|f| f(parts).or_else(|_| Self::from_connection_info(parts)))
})
.unwrap_or_else(|| {
debug_info!(
"No (valid) options set in `accepted_ip_sources` or `request_ip_source`; \
using peer address"
);
Self::from_connection_info(parts)
})
}
}
impl<S> FromRequestParts<S> for ClientIp
where
S: Deref<Target = Services> + Sync,
{
type Rejection = ClientIpError;
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
Self::for_config(
parts,
&state.config.accepted_ip_sources,
state.config.request_ip_source.as_ref(),
)
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use super::*;
const SOCKET_V4: IpAddr = IpAddr::V4(Ipv4Addr::new(198, 51, 100, 1));
const HEADER_V4: IpAddr = IpAddr::V4(Ipv4Addr::new(203, 0, 113, 1));
const SOCKET_V6: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xDB8, 0, 0, 0, 0, 0, 1));
const HEADER_V6: IpAddr = IpAddr::V6(Ipv6Addr::new(0x2001, 0xDB8, 0, 0, 0, 0, 0, 2));
fn parts(peer: Option<IpAddr>, headers: &[(&str, &str)]) -> Parts {
let mut builder = http::Request::builder();
for (name, value) in headers {
builder = builder.header(*name, *value);
}
let (mut parts, ()) = builder.body(()).unwrap().into_parts();
if let Some(peer) = peer {
parts
.extensions
.insert(ConnectInfo(SocketAddr::new(peer, 8448)));
}
parts
}
fn extract(
parts: &Parts,
accepted_ip_sources: &[String],
request_ip_source: Option<&String>,
) -> Result<IpAddr, ClientIpError> {
ClientIp::for_config(parts, accepted_ip_sources, request_ip_source).map(|ClientIp(ip)| ip)
}
#[tokio::test]
async fn resolves_from_configured_source_ipv4() {
let parts = parts(Some(SOCKET_V4), &[("x-real-ip", &HEADER_V4.to_string())]);
assert_eq!(extract(&parts, &["x_real_ip".to_owned()], None), Ok(HEADER_V4));
assert_eq!(extract(&parts, &[], Some(&"x_real_ip".to_owned())), Ok(HEADER_V4));
}
#[tokio::test]
async fn resolves_from_configured_source_ipv6() {
let parts = parts(Some(SOCKET_V6), &[("x-real-ip", &HEADER_V6.to_string())]);
assert_eq!(extract(&parts, &["x_real_ip".to_owned()], None), Ok(HEADER_V6));
assert_eq!(extract(&parts, &[], Some(&"x_real_ip".to_owned())), Ok(HEADER_V6));
}
#[tokio::test]
async fn accepted_ip_sources_no_fall_back_to_peer_when_header_missing() {
let parts = parts(Some(SOCKET_V4), &[]);
extract(&parts, &["x_real_ip".to_owned()], None).unwrap_err();
}
#[tokio::test]
async fn request_ip_source_falls_back_to_peer_when_header_missing() {
let parts = parts(Some(SOCKET_V4), &[]);
assert_eq!(extract(&parts, &[], Some(&"x_real_ip".to_owned())), Ok(SOCKET_V4));
}
#[tokio::test]
async fn accepted_ip_sources_no_fall_back_to_peer_when_header_unparsable() {
let parts = parts(Some(SOCKET_V4), &[("x-real-ip", "not-an-ip")]);
extract(&parts, &["x_real_ip".to_owned()], None).unwrap_err();
}
#[tokio::test]
async fn request_ip_source_falls_back_to_peer_when_header_unparsable() {
let parts = parts(Some(SOCKET_V4), &[("x-real-ip", "not-an-ip")]);
assert_eq!(extract(&parts, &[], Some(&"x_real_ip".to_owned())), Ok(SOCKET_V4));
}
#[tokio::test]
async fn accepted_ip_sources_falls_back_when_configured() {
let parts = parts(Some(SOCKET_V4), &[]);
assert_eq!(
extract(&parts, &["x_real_ip".to_owned(), "direct".to_owned()], None),
Ok(SOCKET_V4)
);
}
#[tokio::test]
async fn falls_back_to_peer_no_configuration() {
let parts = parts(Some(SOCKET_V4), &[("x-real-ip", &HEADER_V4.to_string())]);
assert_eq!(extract(&parts, &[], None), Ok(SOCKET_V4));
}
}
+2
View File
@@ -11,6 +11,8 @@
pub mod router;
pub mod server;
pub mod client_ip;
pub mod admin;
pub(crate) use self::router::{Ruma, RumaResponse, State};
+1
View File
@@ -177,6 +177,7 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.ruma_route(&client::get_relating_events_with_rel_type_route)
.ruma_route(&client::get_relating_events_route)
.ruma_route(&client::get_hierarchy_route)
.ruma_route(&client::get_mutual_rooms_unstable_route)
.ruma_route(&client::get_mutual_rooms_route)
.ruma_route(&client::get_room_summary)
.ruma_route(&client::get_suspended_status)
+2 -3
View File
@@ -15,10 +15,9 @@
#[derive(Deserialize)]
pub(crate) struct AuthQueryParams {
pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
#[serde(alias = "org.matrix.msc3202.device_id")]
pub(super) device_id: Option<String>,
#[serde(rename = "org.matrix.msc3202.device_id")]
pub(super) legacy_device_id: Option<String>,
}
/// Extractor for Ruma request structs
+22 -18
View File
@@ -219,25 +219,29 @@ async fn verify<B: AsRef<[u8]> + Sync>(
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device =
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as \
this device."
)));
}
let sender_device = if let Some(device_id) = query
.device_id
.or(query.legacy_device_id)
.as_deref()
.map(Into::into)
{
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as this \
device."
)));
}
Some(device_id.to_owned())
} else {
None
};
Some(device_id.to_owned())
} else {
None
};
Ok(ClientIdentity::Appservice {
sender_user,
+155 -71
View File
@@ -1,10 +1,9 @@
use std::collections::{HashMap, hash_map::Entry};
use axum::extract::State;
use axum_client_ip::ClientIp;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{
Err, Error, EventTypeExt, PduEvent, Result, debug, err, error,
Err, Error, EventTypeExt, PduEvent, Result, debug, debug_warn, err, error,
matrix::{Event, StateKey},
result::FlatOk,
state_res, trace,
@@ -12,8 +11,8 @@
warn,
};
use ruma::{
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, ServerName,
UserId,
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedUserId,
ServerName, UserId,
api::{
error::{ErrorKind, IncompatibleRoomVersionErrorData},
federation::membership::{RawStrippedState, create_invite},
@@ -22,16 +21,16 @@
room_version_rules::RoomVersionRules,
};
use serde::Deserialize;
use serde_json::value::RawValue;
use crate::{Ruma, server::utils::validate_any_membership_event};
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
///
/// Invites a remote user to a room.
#[tracing::instrument(skip_all, fields(%client), name = "invite", level = "info")]
#[tracing::instrument(skip_all, name = "invite", level = "info")]
pub(crate) async fn create_invite_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<create_invite::v2::Request>,
) -> Result<create_invite::v2::Response> {
if !services.server.supported_room_version(&body.room_version) {
@@ -170,10 +169,14 @@ pub(crate) async fn create_invite_route(
// request, but we will get that during send_join anyway.
// This is safe to just add directly as an outlier as we already auth checked it
// during validation.
services
.rooms
.outlier
.add_pdu_outlier(&create_event_id, create_event_json);
if let Some(create_event_id) = create_event_id {
let mut create_event_json = create_event_json.clone();
create_event_json.insert("event_id".to_owned(), create_event_id.as_str().into());
services
.rooms
.outlier
.add_pdu_outlier(&create_event_id, &create_event_json);
}
services
.rooms
@@ -243,10 +246,10 @@ pub(crate) async fn create_invite_route(
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid
async fn validate_invite_membership_event(
services: &crate::State,
body: &serde_json::value::RawValue,
body: &RawValue,
room_version_rules: &RoomVersionRules,
origin: &ServerName,
create_event_id: OwnedEventId,
create_event_id: Option<OwnedEventId>,
room_id: OwnedRoomId,
event_id: OwnedEventId,
) -> Result<(CanonicalJsonObject, OwnedUserId, OwnedUserId)> {
@@ -281,6 +284,9 @@ async fn validate_invite_membership_event(
Ok((pdu, sender_user, recipient_user))
}
type ValidatedInviteState =
(Option<OwnedEventId>, HashMap<(StateEventType, StateKey), CanonicalJsonObject>);
/// Validates the *invite state* of an invite request, per the steps listed
/// under the endpoint's [spec].
///
@@ -292,7 +298,8 @@ async fn validate_invite_state(
invite_state: &[RawStrippedState],
room_version_rules: &RoomVersionRules,
room_id: OwnedRoomId,
) -> Result<(OwnedEventId, HashMap<(StateEventType, StateKey), CanonicalJsonObject>)> {
) -> Result<ValidatedInviteState> {
let allow_stripped = services.config.enable_legacy_invite_support;
trace!(?invite_state, "Raw invite state");
let mut invite_state_map: HashMap<(StateEventType, StateKey), _> =
HashMap::with_capacity(invite_state.len());
@@ -301,49 +308,69 @@ async fn validate_invite_state(
for (idx, invite_state_event) in invite_state.iter().cloned().enumerate() {
trace!(%idx, ?invite_state_event, "Invite state event");
// Stripped state hasn't been sent over federation since v1.16.
let RawStrippedState::Pdu(raw_pdu) = invite_state_event else {
debug!(%idx, "Invite state event is not a PDU");
return Err!(Request(InvalidParam(
"PDU in invite state (index {idx}) violates the room event format"
)));
// we allow stripped state for compatibility with outdated servers if enabled.
#[allow(deprecated)]
let (raw_pdu, full) = match invite_state_event {
| RawStrippedState::Pdu(pdu) => (pdu, true),
| RawStrippedState::Stripped(event) if allow_stripped => {
warn!(index=%idx, "Event in incoming invite state is not a PDU and cannot be verified");
(
serde_json::value::to_raw_value(&event)
.expect("must be able to convert raw stripped state event to JSON value"),
false,
)
},
| RawStrippedState::Stripped(_) => {
debug_warn!(%idx, "Invite state event is not a PDU");
return Err!(Request(InvalidParam(
"PDU in invite state (index {idx}) violates the room event format"
)));
},
| _ =>
return Err!(Request(BadJson(
"PDU in invite state (index {idx}) is completely malformed"
))),
};
let (state_event_id, state_event_json) = if !allow_stripped || full {
validate_invite_state_pdu(services, &raw_pdu, &room_id, room_version_rules).await?
} else {
validate_legacy_invite_state_event(&raw_pdu, idx)?
};
let (state_event_room_id, state_event_id, state_event_json) = services
.rooms
.event_handler
.parse_incoming_pdu(&raw_pdu, Some(room_version_rules))
.await
.map_err(|e| {
err!(Request(InvalidParam(debug_warn!("Invalid PDU in invite state: {e}"))))
})?;
if state_event_room_id != room_id {
return Err!(Request(InvalidParam(debug_warn!(
%state_event_room_id,
%room_id,
"PDU in invite state ({state_event_id}) belongs to the wrong room"
))));
}
services
.server_keys
.verify_event(&state_event_json, room_version_rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(debug_info!(
"PDU in invite state ({state_event_id}) is not a state event"
"Event in invite state ({state_event_id}) is not a state event"
))));
};
let Some(event_type) = state_event_json.get("type").and_then(|k| k.as_str()) else {
return Err!(Request(InvalidParam(debug_warn!(
"PDU in invite state ({state_event_id}) is not an event?"
"Event in invite state ({state_event_id}) is not an event?"
))));
};
let key = StateEventType::from(event_type).with_state_key(state_key);
if key.0 == StateEventType::RoomCreate && key.1.is_empty() {
// Ensure this is a legal create event.
match PduEvent::from_id_val(&state_event_id, state_event_json.clone()) {
| Ok(pdu_event) => {
debug!("Validating discovered create event in invite room state");
create_event_id = Some(
validate_invite_create_event(&pdu_event, room_version_rules)
.await
.map(|_| state_event_id.clone())?,
)
},
| Err(e) => {
if !allow_stripped {
return Err!(Request(InvalidParam(
"Invalid create event in invite room state: {e:?}"
)));
}
warn!(error=?e, "Invalid create event in invite room state");
},
};
}
match invite_state_map.entry(key) {
| Entry::Occupied(entry) =>
return Err!(Request(InvalidParam(
@@ -351,41 +378,98 @@ async fn validate_invite_state(
entry.key(),
))),
| Entry::Vacant(entry) => {
if entry.key().0 == StateEventType::RoomCreate {
// Ensure this is a legal create event.
let pdu_event =
PduEvent::from_id_val(&state_event_id, state_event_json.clone())
.expect("must be able to create pdu event from event json");
debug!("Validating discovered create event in invite room state");
validate_invite_create_event(&pdu_event, room_version_rules).await?;
create_event_id = Some(state_event_id);
}
entry.insert(state_event_json);
},
}
}
let Some(create_event_id) = create_event_id else {
return Err!(Request(InvalidParam(debug_warn!(
parsed_state=?invite_state_map,
"Invite state does not contain the m.room.create event"
))));
};
invite_state_map.iter().try_for_each(|(key, event_json)| {
service::rooms::event_handler::Service::pdu_format_check_1(
event_json,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam(
"PDU in invite state for {key:?} violates the room event format: {e}"
)))
})
})?;
format_check_state_map(
create_event_id.as_deref(),
&invite_state_map,
room_version_rules,
allow_stripped,
)?;
Ok((create_event_id, invite_state_map))
}
async fn validate_invite_state_pdu(
services: &crate::State,
raw_pdu: &RawValue,
room_id: &ruma::RoomId,
rules: &RoomVersionRules,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let (state_event_room_id, state_event_id, state_event_json) = services
.rooms
.event_handler
.parse_incoming_pdu(raw_pdu, Some(rules))
.await
.map_err(|e| {
err!(Request(InvalidParam(debug_warn!("Invalid PDU in invite state: {e}"))))
})?;
if state_event_room_id != room_id {
return Err!(Request(InvalidParam(debug_warn!(
%state_event_room_id,
%room_id,
"PDU in invite state ({state_event_id}) belongs to the wrong room"
))));
}
services
.server_keys
.verify_event(&state_event_json, rules)
.await
.map_err(|e| {
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
})?;
Ok((state_event_id, state_event_json))
}
fn validate_legacy_invite_state_event(
event: &RawValue,
idx: usize,
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
let pdu = serde_json::from_str::<CanonicalJsonObject>(event.get()).map_err(|e| {
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
})?;
Ok((EventId::parse(format!("$stripped_invite_state_{idx}"))?, pdu))
}
fn format_check_state_map(
create_event_id: Option<&EventId>,
invite_state_map: &HashMap<(StateEventType, StateKey), CanonicalJsonObject>,
room_version_rules: &RoomVersionRules,
allow_stripped: bool,
) -> Result<()> {
let Some(create_event_id) = create_event_id else {
if !allow_stripped {
return Err!(Request(InvalidParam(debug_warn!(
parsed_state=?invite_state_map,
"Invite state does not contain a valid m.room.create event"
))));
}
warn!("Not validating final invite state map, no valid create event was included.");
return Ok(());
};
invite_state_map.iter().try_for_each(|(key, event_json)| {
if event_json.get("signatures").is_none() && allow_stripped {
warn!(state_key=?key, "Skipping validation of nonconformant invite state event");
Ok(())
} else {
service::rooms::event_handler::Service::pdu_format_check_1(
event_json,
room_version_rules,
create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam(
"PDU in invite state for {key:?} violates the room event format: {e}"
)))
})
}
})
}
#[derive(Deserialize)]
struct MFederate {
#[serde(rename = "m.federate")]
+2 -15
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, utils::content_disposition::make_content_disposition};
use conduwuit_service::media::{Dim, FileMeta};
use ruma::api::federation::authenticated_media::{
@@ -12,15 +11,9 @@
/// # `GET /_matrix/federation/v1/media/download/{mediaId}`
///
/// Load media from our server.
#[tracing::instrument(
name = "media_get",
level = "debug",
skip_all,
fields(%client)
)]
#[tracing::instrument(name = "media_get", level = "debug", skip_all)]
pub(crate) async fn get_content_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let mxc = Mxc {
@@ -54,15 +47,9 @@ pub(crate) async fn get_content_route(
/// # `GET /_matrix/federation/v1/media/thumbnail/{mediaId}`
///
/// Load media thumbnail from our server.
#[tracing::instrument(
name = "media_thumbnail_get",
level = "debug",
skip_all,
fields(%client)
)]
#[tracing::instrument(name = "media_thumbnail_get", level = "debug", skip_all)]
pub(crate) async fn get_content_thumbnail_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
+2 -5
View File
@@ -1,5 +1,4 @@
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{Err, Result, err};
use ruma::{
api::federation::directory::{get_public_rooms, get_public_rooms_filtered},
@@ -12,10 +11,9 @@
/// # `POST /_matrix/federation/v1/publicRooms`
///
/// Lists the public rooms on this server.
#[tracing::instrument(name = "publicrooms", level = "debug", skip_all, fields(%client))]
#[tracing::instrument(name = "publicrooms", level = "debug", skip_all)]
pub(crate) async fn get_public_rooms_filtered_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms_filtered::v1::Request>,
) -> Result<get_public_rooms_filtered::v1::Response> {
if !services
@@ -48,10 +46,9 @@ pub(crate) async fn get_public_rooms_filtered_route(
/// # `GET /_matrix/federation/v1/publicRooms`
///
/// Lists the public rooms on this server.
#[tracing::instrument(name = "publicrooms", level = "debug", skip_all, fields(%client))]
#[tracing::instrument(name = "publicrooms", level = "debug", skip_all)]
pub(crate) async fn get_public_rooms_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<get_public_rooms::v1::Request>,
) -> Result<get_public_rooms::v1::Response> {
if !services
+13 -33
View File
@@ -1,11 +1,9 @@
use std::{
collections::{BTreeMap, HashMap},
net::IpAddr,
time::{Duration, Instant},
};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Error, Result, debug, debug_error, debug_warn, err, error,
result::LogErr,
@@ -57,7 +55,6 @@
/// Push EDUs and PDUs to this server.
pub(crate) async fn send_transaction_message_route(
State(services): State<crate::State>,
ClientIp(client): ClientIp,
body: Ruma<send_transaction_message::v1::Request>,
) -> Result<send_transaction_message::v1::Response> {
if body.identity != body.body.origin {
@@ -98,7 +95,7 @@ pub(crate) async fn send_transaction_message_route(
services
.server
.runtime()
.spawn(process_inbound_transaction(services, body, client, txn_key, sender));
.spawn(process_inbound_transaction(services, body, txn_key, sender));
// and wait for it
wait_for_result(receiver).await
},
@@ -141,7 +138,6 @@ async fn wait_for_result(
async fn process_inbound_transaction(
services: crate::State,
body: Ruma<send_transaction_message::v1::Request>,
client: IpAddr,
txn_key: TxnKey,
sender: Sender<WrappedTransactionResponse>,
) {
@@ -163,7 +159,7 @@ async fn process_inbound_transaction(
.stream();
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",);
let results = match handle(&services, &client, &body.identity, pdus, edus).await {
let results = match handle(&services, &body.identity, pdus, edus).await {
| Ok(results) => results,
| Err(err) => {
fail_federation_txn(services, &txn_key, &sender, err);
@@ -236,7 +232,6 @@ fn transaction_error_to_response(err: &TransactionError) -> Error {
}
async fn handle(
services: &Services,
client: &IpAddr,
origin: &ServerName,
pdus: impl Stream<Item = Pdu> + Send,
edus: impl Stream<Item = Edu> + Send,
@@ -257,7 +252,7 @@ async fn handle(
.into_iter()
.try_stream()
.broad_and_then(|(room_id, pdus): (_, Vec<_>)| {
handle_room(services, client, origin, room_id, pdus.into_iter())
handle_room(services, origin, room_id, pdus.into_iter())
.map_ok(Vec::into_iter)
.map_ok(IterStream::try_stream)
})
@@ -267,7 +262,7 @@ async fn handle(
.await?;
// Evaluate EDUs after PDUs in case some of the PDUs then forbid some EDUs.
edus.for_each_concurrent(automatic_width(), |edu| handle_edu(services, client, origin, edu))
edus.for_each_concurrent(automatic_width(), |edu| handle_edu(services, origin, edu))
.boxed()
.await;
@@ -276,7 +271,6 @@ async fn handle(
async fn handle_room(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
room_id: OwnedRoomId,
pdus: impl Iterator<Item = Pdu> + Send,
@@ -323,25 +317,25 @@ async fn handle_room(
Ok(results)
}
async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, edu: Edu) {
async fn handle_edu(services: &Services, origin: &ServerName, edu: Edu) {
match edu {
| Edu::Presence(presence) if services.server.config.allow_incoming_presence =>
handle_edu_presence(services, client, origin, presence).await,
handle_edu_presence(services, origin, presence).await,
| Edu::Receipt(receipt) if services.server.config.allow_incoming_read_receipts =>
handle_edu_receipt(services, client, origin, receipt).await,
handle_edu_receipt(services, origin, receipt).await,
| Edu::Typing(typing) if services.server.config.allow_incoming_typing =>
handle_edu_typing(services, client, origin, typing).await,
handle_edu_typing(services, origin, typing).await,
| Edu::DeviceListUpdate(content) =>
handle_edu_device_list_update(services, client, origin, content).await,
handle_edu_device_list_update(services, origin, content).await,
| Edu::DirectToDevice(content) =>
handle_edu_direct_to_device(services, client, origin, content).await,
handle_edu_direct_to_device(services, origin, content).await,
| Edu::SigningKeyUpdate(content) =>
handle_edu_signing_key_update(services, client, origin, content).await,
handle_edu_signing_key_update(services, origin, content).await,
| Edu::_Custom(ref _custom) => debug_warn!(?edu, "received custom/unknown EDU"),
@@ -351,7 +345,6 @@ async fn handle_edu(services: &Services, client: &IpAddr, origin: &ServerName, e
async fn handle_edu_presence(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
presence: PresenceContent,
) {
@@ -392,12 +385,7 @@ async fn handle_edu_presence_update(
.ok();
}
async fn handle_edu_receipt(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
receipt: ReceiptContent,
) {
async fn handle_edu_receipt(services: &Services, origin: &ServerName, receipt: ReceiptContent) {
receipt
.receipts
.into_iter()
@@ -492,12 +480,7 @@ async fn handle_edu_receipt_room_user(
.await;
}
async fn handle_edu_typing(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
typing: TypingContent,
) {
async fn handle_edu_typing(services: &Services, origin: &ServerName, typing: TypingContent) {
if typing.user_id.server_name() != origin {
debug_warn!(
%typing.user_id, %origin,
@@ -557,7 +540,6 @@ async fn handle_edu_typing(
async fn handle_edu_device_list_update(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
content: DeviceListUpdateContent,
) {
@@ -576,7 +558,6 @@ async fn handle_edu_device_list_update(
async fn handle_edu_direct_to_device(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
content: DirectDeviceContent,
) {
@@ -696,7 +677,6 @@ async fn handle_edu_direct_to_device_event(
async fn handle_edu_signing_key_update(
services: &Services,
_client: &IpAddr,
origin: &ServerName,
content: SigningKeyUpdateContent,
) {
+1 -1
View File
@@ -252,7 +252,7 @@ pub(crate) async fn create_join_event_v2_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
Some(create_event.event_id.clone()),
body.room_id.clone(),
body.event_id.clone(),
)
+1 -1
View File
@@ -67,7 +67,7 @@ pub(crate) async fn create_knock_event_v1_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id().to_owned(),
Some(create_event.event_id().to_owned()),
body.room_id.clone(),
body.event_id.clone(),
)
+1 -1
View File
@@ -53,7 +53,7 @@ pub(crate) async fn create_leave_event_v2_route(
&services,
&body.pdu,
&room_version_rules,
create_event.event_id.clone(),
Some(create_event.event_id.clone()),
body.room_id.clone(),
body.event_id.clone(),
)
+56 -12
View File
@@ -80,6 +80,12 @@ pub(super) async fn assert(&self) -> Result {
/// 6. `state_key` field presence (and parsing)
/// 7. PDU room format check (PDU check 1)
///
/// ## Compatibility
///
/// If `create_event_id` is `None`, step 7 is not performed. This should only be
/// the case for the invite route where servers still send stripped events on
/// occasion, and as such the create event may not be included in the state.
///
/// ## Returns
///
/// A resulting tuple of (PDU JSON, target membership state, sender, recipient).
@@ -87,11 +93,11 @@ pub(crate) async fn validate_any_membership_event(
services: &crate::State,
body: &serde_json::value::RawValue,
room_version_rules: &RoomVersionRules,
create_event_id: OwnedEventId,
create_event_id: Option<OwnedEventId>,
expected_room_id: OwnedRoomId,
expected_event_id: OwnedEventId,
) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> {
let (template_room_id, template_event_id, pdu) = services
let (template_room_id, template_event_id, mut pdu) = services
.rooms
.event_handler
.parse_incoming_pdu(body, Some(room_version_rules))
@@ -109,6 +115,37 @@ pub(crate) async fn validate_any_membership_event(
))));
}
// Only `join` events carry `join_authorised_via_users_server`; co-sign
// restricted joins so verification passes. Authorisation is enforced in
// create_join_event.
let membership_is_join = pdu
.get("content")
.and_then(|v| v.as_object())
.and_then(|c| c.get("membership"))
.and_then(|v| v.as_str())
.is_some_and(|m| m == "join");
let authorising_user = pdu
.get("content")
.and_then(|v| v.as_object())
.and_then(|c| c.get("join_authorised_via_users_server"))
.and_then(|v| v.as_str())
.map(UserId::parse)
.and_then(Result::ok);
if room_version_rules.authorization.restricted_join_rule
&& membership_is_join
&& let Some(authorising_user) = authorising_user
&& services.globals.user_is_local(&authorising_user)
{
services
.server_keys
.hash_and_sign_event(&mut pdu, room_version_rules)
.map_err(|e| {
err!(Request(InvalidParam("Failed to sign restricted join event: {e}")))
})?;
}
services
.server_keys
.verify_event(&pdu, room_version_rules)
@@ -140,6 +177,11 @@ pub(crate) async fn validate_any_membership_event(
.ok_or_else(|| err!(Request(BadJson("Event is not a string"))))?
.to_owned();
assert!(
create_event_id.is_some() || membership == "invite",
"non-invite membership check had no associated create event"
);
let sender_user = pdu
.get("sender")
.and_then(|v| v.as_str())
@@ -153,16 +195,18 @@ pub(crate) async fn validate_any_membership_event(
.and_then(Result::ok)
.ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?;
// Do a quick format check. The spec doesn't suggest this, but it's probably
// a good idea nonetheless.
service::rooms::event_handler::Service::pdu_format_check_1(
&pdu,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam("Membership event violates the room event format: {e}")))
})?;
if let Some(create_event_id) = create_event_id {
// Do a quick format check. The spec doesn't suggest this, but it's probably
// a good idea nonetheless.
service::rooms::event_handler::Service::pdu_format_check_1(
&pdu,
room_version_rules,
&create_event_id,
)
.map_err(|e| {
err!(Request(InvalidParam("Membership event violates the room event format: {e}")))
})?;
}
Ok((pdu, membership.into(), sender_user, recipient_user))
}
+44 -2
View File
@@ -370,7 +370,7 @@ pub struct Config {
#[serde(default = "default_ip_lookup_strategy")]
pub ip_lookup_strategy: u8,
/// The source to use for discovering the real connecting client IP.
/// The preferred source to use for getting the real client IP.
///
/// Takes any of the following options:
///
@@ -381,16 +381,49 @@ pub struct Config {
/// "true_client_ip" - `True-Client-Ip` header
/// "x_envoy_external_address" - `X-Envoy-External-Address` header
/// "x_real_ip" - `X-Real-Ip` header
/// "direct" - the directly connected IP address (fallback for other
/// options)
///
/// Only set this if you are certain only your reverse proxy
/// will send the expected header. There is no "is the connecting IP allowed
/// to set this header" check; if the header selected is present, it is
/// used.
///
/// Defaults to the IP address actually making the connection.
/// Ignored if `accepted_ip_sources` is set.
///
/// If not set set, `direct` is used. Additionally, `direct` is used
/// as a fallback if the chosen header is not found.
#[serde(default)]
pub request_ip_source: Option<String>,
/// The accepted source(s) for the real client IP.
///
/// Takes any combination of the following options:
///
/// "cf_connecting_ip" - `Cf-Connecting-Ip` header
/// "cloudfront_viewer_address" - `CloudFront-Viewer-Address` header
/// "fly_client_ip" - `Fly-Client-IP` header
/// "x_forwarded_for" - rightmost value of the `X-Forwarded-For` header
/// "true_client_ip" - `True-Client-Ip` header
/// "x_envoy_external_address" - `X-Envoy-External-Address` header
/// "x_real_ip" - `X-Real-Ip` header
/// "direct" - the directly connected IP address
///
/// Only set this if you are certain only your reverse proxy
/// will send the expected header. There is no "is the connecting IP allowed
/// to set this header" check; if the header selected is present, it is
/// used.
///
/// One of the configured sources must be available or the request is
/// rejected. If you want a fallback (including `direct`!), you have to
/// configure it. Priority is based on configuration order.
///
/// If not set, `request_ip_source` configuration is used.
///
/// default: []
#[serde(default)]
pub accepted_ip_sources: Vec<String>,
/// Max request size for file uploads in bytes. Defaults to 20MB.
/// Also limits incoming federated media.
///
@@ -1690,6 +1723,15 @@ pub struct Config {
#[serde(default)]
pub send_messages_from_ignored_users_to_client: bool,
/// Disables strict validation on incoming federated invites. This weakens
/// your server's security - only enable it if you encounter issues
/// receiving invites from outdated servers.
///
/// Validation will still be performed, but it will log a warning instead of
/// causing an error.
#[serde(default)]
pub enable_legacy_invite_support: bool,
/// Send "org.matrix.dummy_event" events to the client. This is a debugging
/// option.
#[serde(default)]
+1
View File
@@ -34,6 +34,7 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
// query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
// Expected for spec v1.19
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true),
("uk.half-shot.msc2666.query_mutual_rooms.stable".to_owned(), true),
// Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186)
// Expected for spec v1.19
("org.matrix.simplified_msc3575".to_owned(), true),
+4
View File
@@ -478,6 +478,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "logintoken_expiresatuserid",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "roomuserid_lastnotificationread",
..descriptor::RANDOM
},
Descriptor {
name: "userroomid_highlightcount",
..descriptor::RANDOM
+4 -1
View File
@@ -35,7 +35,10 @@ systemd-units = { unit-name = "conduwuit", start = false, unit-scripts = "../../
assets = [
["../../pkg/debian/README.md", "usr/share/doc/conduwuit/README.Debian", "644"],
["../../README.md", "usr/share/doc/conduwuit/", "644"],
["../../target/release/conduwuit", "usr/bin/conduwuit", "755"],
# cargo-deb only treats the exact prefix "target/release/" as the magic
# path that resolves to the real build dir (e.g. target/<triple>/release
# when cross-compiling); a ../../ prefix would be read as a literal path
["target/release/conduwuit", "usr/bin/conduwuit", "755"],
["../../conduwuit-example.toml", "etc/conduwuit/conduwuit.toml", "640"],
]
-1
View File
@@ -91,7 +91,6 @@ zstd_compression = [
]
[dependencies]
axum-client-ip.workspace = true
axum-server-dual-protocol.workspace = true
axum-server-dual-protocol.optional = true
axum-server.workspace = true
+21 -23
View File
@@ -2,10 +2,10 @@
use axum::{
Router,
extract::{DefaultBodyLimit, MatchedPath},
extract::{self, DefaultBodyLimit, FromRequestParts, MatchedPath, State},
};
use axum_client_ip::ClientIpSource;
use conduwuit::{Result, Server, debug, error};
use conduwuit_api::client_ip::ClientIp;
use conduwuit_service::{Services, state::Guard};
use http::{
HeaderValue, Method, StatusCode,
@@ -20,7 +20,6 @@
timeout::{RequestBodyTimeoutLayer, ResponseBodyTimeoutLayer, TimeoutLayer},
trace::{DefaultOnFailure, DefaultOnRequest, DefaultOnResponse, TraceLayer},
};
use tracing::Level;
use crate::{request, router};
@@ -48,34 +47,18 @@ pub(crate) fn build(services: &Arc<Services>) -> Result<(Router, Guard)> {
))]
let layers = layers.layer(compression_layer(server));
let client_ip_layer = match services
.config
.request_ip_source
.as_ref()
.map(AsRef::as_ref)
{
| Some("cf_connecting_ip") => ClientIpSource::CfConnectingIp,
| Some("cloudfront_viewer_address") => ClientIpSource::CloudFrontViewerAddress,
| Some("fly_client_ip") => ClientIpSource::FlyClientIp,
| Some("x_forwarded_for") => ClientIpSource::RightmostXForwardedFor,
| Some("true_client_ip") => ClientIpSource::TrueClientIp,
| Some("x_envoy_external_address") => ClientIpSource::XEnvoyExternalAddress,
| Some("x_real_ip") => ClientIpSource::XRealIp,
| None | Some(_) => ClientIpSource::ConnectInfo,
};
let services_ = services.clone();
let layers = layers
.layer(SetSensitiveHeadersLayer::new([header::AUTHORIZATION]))
.layer(
TraceLayer::new_for_http()
.make_span_with(tracing_span::<_>)
.on_failure(DefaultOnFailure::new().level(Level::ERROR))
.on_request(DefaultOnRequest::new().level(Level::TRACE))
.on_response(DefaultOnResponse::new().level(Level::DEBUG)),
.on_failure(DefaultOnFailure::new().level(tracing::Level::ERROR))
.on_request(DefaultOnRequest::new().level(tracing::Level::TRACE))
.on_response(DefaultOnResponse::new().level(tracing::Level::DEBUG)),
)
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request_ip))
.layer(axum::middleware::from_fn_with_state(Arc::clone(services), request::handle))
.layer(client_ip_layer.into_extension())
.layer(ResponseBodyTimeoutLayer::new(Duration::from_secs(
server.config.client_response_timeout,
)))
@@ -230,11 +213,26 @@ fn tracing_span<T>(request: &http::Request<T>) -> tracing::Span {
parent: None,
debug::INFO_SPAN_LEVEL,
"router",
ip=tracing::field::Empty,
method = %request.method(),
%path,
}
}
/// Annotates the tracing span with the client IP
async fn request_ip(
State(services): State<Arc<Services>>,
request: extract::Request,
next: axum::middleware::Next,
) -> axum::response::Response {
let (mut parts, body) = request.into_parts();
if let Ok(ip) = ClientIp::from_request_parts(&mut parts, &services).await {
let span = tracing::Span::current();
span.record("ip", ip.0.to_string());
}
next.run(extract::Request::from_parts(parts, body)).await
}
fn request_path_str<T>(request: &http::Request<T>) -> &str {
request
.uri()
+1 -1
View File
@@ -11,7 +11,7 @@
use ruma::{
ServerName,
api::{
EndpointError, IncomingResponse, OutgoingRequest, SupportedVersions,
EndpointError, IncomingResponse, OutgoingRequest, OutgoingRequestExt, SupportedVersions,
auth_scheme::{AuthScheme, NoAuthentication},
error::Error as RumaError,
federation::authentication::{ServerSignatures, ServerSignaturesInput},
+6 -4
View File
@@ -32,7 +32,7 @@
/// - If database is opened at lesser version we apply migrations up to this.
/// Note that named-feature migrations may also be performed when opening at
/// equal or lesser version. These are expected to be backward-compatible.
pub(crate) const DATABASE_VERSION: u64 = 18;
pub(crate) const DATABASE_VERSION: u64 = 19;
pub(crate) async fn migrations(services: &Services) -> Result<()> {
let users_count = services.users.count().await;
@@ -71,6 +71,8 @@ async fn fresh(services: &Services) -> Result<()> {
db["global"].insert(b"fix_corrupt_msc4133_fields", []);
db["global"].insert(b"populate_userroomid_leftstate_table", []);
db["global"].insert(b"fix_local_invite_state", []);
db["global"].insert(SPLIT_USERID_PASSWORD, []);
db["global"].insert(DROP_ROOMSYNCTOKEN_SHORTSTATEHASH, []);
// Create the admin room and server user on first run
info!("Creating admin room and server user");
@@ -228,9 +230,9 @@ async fn migrate(services: &Services) -> Result<()> {
.map_err(|e| err!("Failed to run 'fix_local_invite_state' migration': {e}"))?;
}
if services.globals.db.database_version().await < 18 {
services.globals.db.bump_database_version(18);
info!("Migration: Bumped database version to 18");
if services.globals.db.database_version().await < 19 {
services.globals.db.bump_database_version(19);
info!("Migration: Bumped database version to 19");
}
if db["global"].get(SPLIT_USERID_PASSWORD).await.is_not_found() {
+2
View File
@@ -132,6 +132,8 @@ pub enum ApplicationType {
#[serde(rename_all = "snake_case")]
pub enum GrantType {
AuthorizationCode,
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
DeviceCode,
RefreshToken,
}
+62 -17
View File
@@ -13,6 +13,7 @@
use url::Url;
use super::client_metadata::ResponseType;
use crate::oauth::client_metadata::GrantType;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct AuthorizationCodeQuery {
@@ -29,6 +30,33 @@ pub struct AuthorizationCodeQuery {
pub prompt: Option<Prompt>,
}
#[derive(Deserialize, Serialize)]
pub struct AuthorizationCodeResponse {
pub state: String,
pub code: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DeviceCodeRequest {
pub client_id: String,
pub scope: RawScopes,
}
#[derive(Deserialize, Serialize)]
pub struct DeviceCodeResponse {
pub device_code: String,
pub user_code: String,
pub verification_uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub verification_uri_complete: Option<Url>,
pub expires_in: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DeviceCodeVerifyQuery {
pub user_code: Option<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
@@ -126,19 +154,29 @@ pub struct OAuthError {
impl OAuthError {
#[must_use]
pub const fn invalid_request(error_description: &'static str) -> Self {
pub fn new(error: ErrorCode, error_description: String) -> Self {
Self {
error: ErrorCode::InvalidRequest,
error,
error_description: Cow::Owned(error_description),
}
}
#[must_use]
pub const fn new_static(error: ErrorCode, error_description: &'static str) -> Self {
Self {
error,
error_description: Cow::Borrowed(error_description),
}
}
#[must_use]
pub const fn invalid_request(error_description: &'static str) -> Self {
Self::new_static(ErrorCode::InvalidRequest, error_description)
}
#[must_use]
pub const fn invalid_grant(error_description: &'static str) -> Self {
Self {
error: ErrorCode::InvalidGrant,
error_description: Cow::Borrowed(error_description),
}
Self::new_static(ErrorCode::InvalidGrant, error_description)
}
}
@@ -157,36 +195,43 @@ pub enum ErrorCode {
AccessDenied,
InvalidScope,
InvalidGrant,
InvalidClient,
InvalidClientMetadata,
AuthorizationPending,
ExpiredToken,
}
#[derive(Serialize, Deserialize)]
pub struct AuthorizationCodeResponse {
pub state: String,
pub code: String,
#[derive(Deserialize)]
pub struct TokenRequest {
pub client_id: String,
#[serde(flatten)]
pub request: TokenRequestType,
}
#[derive(Deserialize)]
#[serde(tag = "grant_type", rename_all = "snake_case")]
pub enum TokenRequest {
pub enum TokenRequestType {
AuthorizationCode {
code: String,
redirect_uri: Url,
client_id: String,
code_verifier: String,
},
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
DeviceCode {
device_code: String,
},
RefreshToken {
client_id: String,
refresh_token: String,
},
}
impl TokenRequest {
impl TokenRequestType {
#[must_use]
pub fn client_id(&self) -> &str {
pub fn grant_type(&self) -> GrantType {
match self {
| Self::AuthorizationCode { client_id, .. }
| Self::RefreshToken { client_id, .. } => client_id,
| Self::AuthorizationCode { .. } => GrantType::AuthorizationCode,
| Self::DeviceCode { .. } => GrantType::DeviceCode,
| Self::RefreshToken { .. } => GrantType::RefreshToken,
}
}
}
+247 -53
View File
@@ -12,20 +12,22 @@
use database::{Deserialized, Json, Map};
use itertools::Itertools;
use lru_cache::LruCache;
use rand::distr::{Distribution, slice::Choose};
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{
Dep,
Dep, config,
oauth::{
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
grant::{
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, ErrorCode,
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod,
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, ResponseMode, Scope,
TokenRequest, TokenRequestType, TokenResponse, TokenType,
},
},
users,
users::{self, DeviceToken},
};
pub mod client_metadata;
@@ -35,7 +37,8 @@ pub struct Service {
services: Services,
db: Data,
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
pending_code_grants: tokio::sync::Mutex<LruCache<String, PendingCodeGrant>>,
pending_auth_code_grants: tokio::sync::Mutex<LruCache<String, PendingAuthCodeGrant>>,
pending_device_code_grants: tokio::sync::Mutex<LruCache<String, PendingDeviceCodeGrant>>,
}
struct Data {
@@ -46,6 +49,7 @@ struct Data {
struct Services {
users: Dep<users::Service>,
config: Dep<config::Service>,
}
#[derive(Debug, Deserialize, Serialize)]
@@ -62,7 +66,7 @@ struct RefreshTokenInfo {
device_id: OwnedDeviceId,
}
struct PendingCodeGrant {
struct PendingAuthCodeGrant {
authorizing_user: OwnedUserId,
requested_scopes: BTreeSet<Scope>,
client_name: Option<String>,
@@ -72,12 +76,8 @@ struct PendingCodeGrant {
requested_at: SystemTime,
}
impl PendingCodeGrant {
impl PendingAuthCodeGrant {
const MAX_AGE: Duration = Duration::from_mins(1);
const RANDOM_CODE_LENGTH: usize = 32;
#[must_use]
pub(crate) fn generate_code() -> String { utils::random_string(Self::RANDOM_CODE_LENGTH) }
#[must_use]
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
@@ -90,6 +90,43 @@ pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
}
}
struct PendingDeviceCodeGrant {
state: DeviceCodeGrantState,
requested_scopes: BTreeSet<Scope>,
client_name: Option<String>,
client_id: String,
requested_at: SystemTime,
}
enum DeviceCodeGrantState {
Unverified {
user_code: String,
},
Verified {
authorizing_user: OwnedUserId,
},
}
impl PendingDeviceCodeGrant {
const MAX_AGE: Duration = Duration::from_mins(1);
#[must_use]
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
let now = SystemTime::now();
self.client_id == client_id
&& now
.duration_since(self.requested_at)
.is_ok_and(|age| age < Self::MAX_AGE)
}
}
pub struct DeviceCodeGrantInfo {
pub device_code: String,
pub client_metadata: ClientMetadata,
pub requested_scopes: BTreeSet<Scope>,
}
/// A time-limited grant for a client to perform some sensitive action.
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum OAuthTicket {
@@ -112,6 +149,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
users: args.depend::<users::Service>("users"),
config: args.depend::<config::Service>("config"),
},
db: Data {
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
@@ -119,8 +157,11 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
},
tickets: Mutex::default(),
pending_code_grants: tokio::sync::Mutex::new(LruCache::new(
Self::MAX_PENDING_CODE_GRANTS,
pending_auth_code_grants: tokio::sync::Mutex::new(LruCache::new(
Self::MAX_PENDING_GRANTS,
)),
pending_device_code_grants: tokio::sync::Mutex::new(LruCache::new(
Self::MAX_PENDING_GRANTS,
)),
}))
}
@@ -133,11 +174,21 @@ impl Service {
// Maximum number of pending code grants which will be held in memory at once,
// to prevent unbounded memory use if someone decides to repeatedly reload the
// grant page.
const MAX_PENDING_CODE_GRANTS: usize = 100;
const MAX_PENDING_GRANTS: usize = 100;
const RANDOM_TOKEN_LENGTH: usize = 32;
const USER_CODE_CHARACTERS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
const USER_CODE_LENGTH: usize = 6;
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
fn generate_user_code() -> String {
Choose::new(Self::USER_CODE_CHARACTERS)
.unwrap()
.sample_iter(&mut rand::rng())
.take(Self::USER_CODE_LENGTH)
.collect()
}
pub async fn register_client(&self, metadata: &ClientMetadata) -> Result<String, OAuthError> {
metadata.validate().map_err(|error| OAuthError {
error: ErrorCode::InvalidClientMetadata,
@@ -231,14 +282,14 @@ pub async fn request_authorization_code(
| ResponseMode::Query => '?',
};
let code = PendingCodeGrant::generate_code();
let code = Self::generate_token();
info!(
client_id = &query.client_id,
client_name = &client_metadata.client_name,
?requested_scopes,
?authorizing_user,
"Issuing oauth authorization code"
"Issuing OAuth authorization code"
);
let redirect_uri = format!(
@@ -252,7 +303,7 @@ pub async fn request_authorization_code(
.unwrap(),
);
let pending_grant = PendingCodeGrant {
let pending_grant = PendingAuthCodeGrant {
authorizing_user,
requested_scopes,
client_name: client_metadata.client_name,
@@ -262,7 +313,7 @@ pub async fn request_authorization_code(
requested_at: SystemTime::now(),
};
self.pending_code_grants
self.pending_auth_code_grants
.lock()
.await
.insert(code, pending_grant);
@@ -270,15 +321,129 @@ pub async fn request_authorization_code(
Ok(redirect_uri)
}
pub async fn request_device_code(
&self,
query: DeviceCodeRequest,
) -> Result<DeviceCodeResponse, OAuthError> {
let Some(client_metadata) = self.get_client_metadata(&query.client_id).await else {
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
};
let requested_scopes = query
.scope
.to_scopes()
.map_err(|err| OAuthError::new(ErrorCode::InvalidGrant, err))?;
let device_code = Self::generate_token();
let user_code = Self::generate_user_code();
let verification_uri = self
.services
.config
.get_client_domain()
.join(&format!("{}/oauth2/grant/device_code", conduwuit::ROUTE_PREFIX))
.unwrap();
let mut verification_uri_complete = verification_uri.clone();
verification_uri_complete
.query_pairs_mut()
.append_pair("user_code", &user_code);
info!(
client_id = &query.client_id,
client_name = &client_metadata.client_name,
?requested_scopes,
"Issuing OAuth device code"
);
let pending_grant = PendingDeviceCodeGrant {
state: DeviceCodeGrantState::Unverified { user_code: user_code.clone() },
requested_scopes,
client_name: client_metadata.client_name,
client_id: query.client_id,
requested_at: SystemTime::now(),
};
self.pending_device_code_grants
.lock()
.await
.insert(device_code.clone(), pending_grant);
Ok(DeviceCodeResponse {
device_code,
user_code,
verification_uri,
verification_uri_complete: Some(verification_uri_complete),
expires_in: PendingDeviceCodeGrant::MAX_AGE.as_secs(),
})
}
pub async fn grant_info_for_user_code(
&self,
supplied_user_code: &str,
) -> Option<DeviceCodeGrantInfo> {
let pending_grants = self.pending_device_code_grants.lock().await;
let (device_code, grant) = pending_grants
.iter()
.find(|(_, grant)| {
matches!(&grant.state, DeviceCodeGrantState::Unverified { user_code } if user_code == supplied_user_code)
})?;
let client_metadata = self
.get_client_metadata(&grant.client_id)
.await
.expect("client should exist");
Some(DeviceCodeGrantInfo {
device_code: device_code.clone(),
client_metadata,
requested_scopes: grant.requested_scopes.clone(),
})
}
pub async fn validate_device_code(
&self,
authorizing_user: OwnedUserId,
device_code: &str,
) -> Result<(), String> {
let mut pending_grants = self.pending_device_code_grants.lock().await;
let Some(pending_grant) = pending_grants.get_mut(device_code) else {
return Err("Invalid device code".to_owned());
};
match &mut pending_grant.state {
| state @ DeviceCodeGrantState::Unverified { .. } => {
*state = DeviceCodeGrantState::Verified { authorizing_user };
Ok(())
},
| DeviceCodeGrantState::Verified {
authorizing_user: previous_authorizing_user,
} =>
if *previous_authorizing_user == authorizing_user {
Ok(())
} else {
Err("Device code is already verified".to_owned())
},
}
}
pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse, OAuthError> {
let TokenRequest { client_id, request } = request;
let Some(client_metadata) = self.get_client_metadata(&client_id).await else {
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
};
if !client_metadata.grant_types.contains(&request.grant_type()) {
return Err(OAuthError::invalid_grant("Client cannot request this grant type"));
}
match request {
| TokenRequest::AuthorizationCode {
code,
redirect_uri,
client_id,
code_verifier,
} => {
let mut pending_grants = self.pending_code_grants.lock().await;
| TokenRequestType::AuthorizationCode { code, redirect_uri, code_verifier } => {
let mut pending_grants = self.pending_auth_code_grants.lock().await;
let Some(pending_grant) = pending_grants
.remove(&code)
@@ -305,7 +470,39 @@ pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse,
)
.await
},
| TokenRequest::RefreshToken { client_id, refresh_token } =>
| TokenRequestType::DeviceCode { device_code } => {
let mut pending_grants = self.pending_device_code_grants.lock().await;
let Some(pending_grant) = pending_grants
.remove(&device_code)
.filter(|grant| grant.is_valid_for(&client_id))
else {
return Err(OAuthError::new_static(
ErrorCode::ExpiredToken,
"Invalid device code",
));
};
match &pending_grant.state {
| DeviceCodeGrantState::Unverified { .. } => {
pending_grants.insert(device_code, pending_grant);
Err(OAuthError::new_static(
ErrorCode::AuthorizationPending,
"Authorization is pending",
))
},
| DeviceCodeGrantState::Verified { authorizing_user } =>
self.create_session(
authorizing_user.to_owned(),
pending_grant.requested_scopes,
pending_grant.client_name,
client_id,
)
.await,
}
},
| TokenRequestType::RefreshToken { refresh_token } =>
self.refresh_session(client_id, refresh_token).await,
}
}
@@ -343,14 +540,14 @@ async fn create_session(
client_name: Option<String>,
client_id: String,
) -> Result<TokenResponse, OAuthError> {
let access_token = Self::generate_token();
let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let refresh_token = Self::generate_token();
let device_id = requested_scopes
.iter()
.find_map(|scope| {
if let Scope::Device(device_id) = scope {
Some(device_id)
Some(device_id.to_owned())
} else {
None
}
@@ -360,24 +557,22 @@ async fn create_session(
if self
.services
.users
.get_device_metadata(&authorizing_user, device_id)
.get_device_metadata(&authorizing_user, &device_id)
.await
.is_ok()
{
return Err(OAuthError {
error: ErrorCode::InvalidScope,
error_description: "A device with the supplied ID already exists for this user"
.into(),
});
return Err(OAuthError::new_static(
ErrorCode::InvalidScope,
"A device with the supplied ID already exists for this user",
));
}
self.services
let device_id = self.services
.users
.create_device(
&authorizing_user,
device_id,
&access_token,
Some(Self::ACCESS_TOKEN_MAX_AGE),
Some(device_id),
Some(access_token.clone()),
client_name,
None,
)
@@ -386,8 +581,16 @@ async fn create_session(
// failure during authentication, which should(?) be impossible(?)
.expect("failed to create device");
info!(
?client_id,
?authorizing_user,
?device_id,
?requested_scopes,
"Created new oauth session"
);
self.db.userdeviceid_oauthsessioninfo.put(
(&authorizing_user, device_id),
(&authorizing_user, &device_id),
Json(SessionInfo {
client_id: client_id.clone(),
current_refresh_token: refresh_token.clone(),
@@ -400,20 +603,12 @@ async fn create_session(
Json(RefreshTokenInfo {
client_id: client_id.clone(),
user_id: authorizing_user.clone(),
device_id: device_id.to_owned(),
device_id,
}),
);
info!(
?client_id,
?authorizing_user,
?device_id,
?requested_scopes,
"Created new oauth session"
);
Ok(TokenResponse {
access_token,
access_token: access_token.into_token(),
token_type: TokenType::Bearer,
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
scope: requested_scopes.iter().join(" "),
@@ -449,7 +644,7 @@ async fn refresh_session(
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
let new_access_token = Self::generate_token();
let new_access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
let new_refresh_token = Self::generate_token();
let scope = session_info.scopes.iter().join(" ");
session_info
@@ -461,8 +656,7 @@ async fn refresh_session(
.set_token(
&refresh_token_info.user_id,
&refresh_token_info.device_id,
&new_access_token,
Some(Self::ACCESS_TOKEN_MAX_AGE),
new_access_token.clone(),
)
.await
.expect("should be able to set token");
@@ -479,7 +673,7 @@ async fn refresh_session(
.raw_put(&new_refresh_token, Json(refresh_token_info));
Ok(TokenResponse {
access_token: new_access_token,
access_token: new_access_token.into_token(),
token_type: TokenType::Bearer,
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
scope,
+1 -1
View File
@@ -367,7 +367,7 @@ pub async fn complete_session(
// Create a new shadow user
self.services
.users
.create_local_account(&user_id, None, None)
.create_local_account(&user_id, None, None, None, None)
.await
.map_err(|err| {
error!("Failed to create a shadow user for {user_id}: {err}");
+41 -26
View File
@@ -13,7 +13,7 @@
use ruma::{
DeviceId, OwnedDeviceId, RoomId, UInt, UserId,
api::{
IncomingResponse, OutgoingRequest,
IncomingResponse, OutgoingRequest, OutgoingRequestExt,
auth_scheme::NoAuthentication,
client::push::{Pusher, PusherKind, set_pusher},
path_builder::SinglePath,
@@ -347,6 +347,42 @@ pub async fn send_push_notice<E>(
Ok(())
}
pub async fn push_joined_count(&self, room_id: &RoomId) -> UInt {
self.services
.state_cache
.room_joined_count(room_id)
.await
.unwrap_or(1)
.try_into()
.unwrap_or_else(|_| uint!(0))
}
#[tracing::instrument(skip(self, user), level = "debug")]
pub async fn push_condition_ctx(
&self,
user: &UserId,
power_levels: RoomPowerLevels,
room_id: &RoomId,
room_joined_count: UInt,
) -> PushConditionRoomCtx {
let power_levels = PushConditionPowerLevelsCtx::from(power_levels);
let user_display_name = self
.services
.users
.displayname(user)
.await
.unwrap_or_else(|_| user.localpart().to_owned());
PushConditionRoomCtx::new(
room_id.to_owned(),
room_joined_count,
user.to_owned(),
user_display_name,
)
.with_power_levels(power_levels)
}
#[tracing::instrument(skip(self, user, ruleset, pdu), level = "debug")]
pub async fn get_actions<'a>(
&self,
@@ -356,31 +392,10 @@ pub async fn get_actions<'a>(
pdu: &Raw<AnySyncTimelineEvent>,
room_id: &RoomId,
) -> &'a [Action] {
let power_levels = PushConditionPowerLevelsCtx::from(power_levels);
let room_joined_count = self
.services
.state_cache
.room_joined_count(room_id)
.await
.unwrap_or(1)
.try_into()
.unwrap_or_else(|_| uint!(0));
let user_display_name = self
.services
.users
.displayname(user)
.await
.unwrap_or_else(|_| user.localpart().to_owned());
let ctx = PushConditionRoomCtx::new(
room_id.to_owned(),
room_joined_count,
user.to_owned(),
user_display_name,
)
.with_power_levels(power_levels);
let room_joined_count = self.push_joined_count(room_id).await;
let ctx = self
.push_condition_ctx(user, power_levels, room_id, room_joined_count)
.await;
ruleset.get_actions(pdu, &ctx).await
}
+24 -7
View File
@@ -6,8 +6,9 @@
};
use futures::future::ready;
use ruma::{
CanonicalJsonObject, EventId, OwnedEventId, ServerName, api::error::ErrorKind,
canonical_json::redact, events::StateEventType, room_version_rules::RoomVersionRules,
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, ServerName,
api::error::ErrorKind, canonical_json::redact, events::StateEventType,
room_version_rules::RoomVersionRules,
};
use crate::rooms::{
@@ -42,11 +43,27 @@ pub fn pdu_format_check_1(
return Err!(Request(BadJson("PDU has too many auth events")));
}
let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id);
if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events {
return Err!(Request(BadJson("PDU references a create event")));
} else if event_format.allow_room_create_in_auth_events && !create_event_in_auth_events {
return Err!(Request(BadJson("PDU does not reference the room create event")));
// The m.room.create event is the genesis event and has empty auth_events
// by definition, so it is exempt from the checks below requiring or
// forbidding the create event in auth_events (it cannot reference itself).
let Some(event_type) = pdu_json.get("type").and_then(CanonicalJsonValue::as_str) else {
return Err!(Request(BadJson("PDU is missing a type")));
};
let state_key = pdu_json
.get("state_key")
.and_then(CanonicalJsonValue::as_str);
let is_create_event = event_type == "m.room.create" && state_key == Some("");
if !is_create_event {
let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id);
if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events {
return Err!(Request(BadJson("PDU references a create event")));
} else if event_format.allow_room_create_in_auth_events
&& !create_event_in_auth_events
{
return Err!(Request(BadJson("PDU does not reference the room create event")));
}
}
let prev_events = expect_event_id_array(pdu_json, "prev_events")?;
+10 -1
View File
@@ -210,6 +210,10 @@ pub async fn get_room_hierarchy_for_user(
let mut summaries = vec![root_summary.summary];
let mut inaccessible_children: HashSet<_> =
root_summary.inaccessible_children.into_iter().collect();
// Rooms already included in the response. The spec requires each room to
// appear at most once, and this also terminates traversal of cyclic space
// graphs (e.g. a space containing itself).
let mut visited: HashSet<OwnedRoomId> = HashSet::from([room_id]);
// TODO refactor this with Vec::peek_mut once it's stabilized
while let Some(layer) = queue.last_mut() {
@@ -224,6 +228,11 @@ pub async fn get_room_hierarchy_for_user(
continue;
}
// Skip rooms already present in the response
if !visited.insert(room_id.clone()) {
continue;
}
let summary = match self
.get_room_summary_and_children_for_user(
Some(querying_user),
@@ -269,7 +278,7 @@ pub async fn get_room_hierarchy_for_user(
// suggest you reconsider some of the choices you made which led you to this
// point.
if queue.len() > 50 {
return Err!("Space hierarchy is unreasonably large");
return Err!(Request(TooLarge("Space hierarchy is unreasonably large")));
}
// Add accessible children as a new layer
+22 -6
View File
@@ -26,12 +26,13 @@
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, RoomId,
RoomVersionId, UserId,
events::{
GlobalAccountDataEventType, TimelineEventType,
AnySyncTimelineEvent, GlobalAccountDataEventType, TimelineEventType,
push_rules::PushRulesEvent,
room::{encrypted::Relation, redaction::RoomRedactionEventContent},
},
push::{Action, Ruleset, Tweak},
room_version_rules::RoomVersionRules,
serde::Raw,
};
use super::{ExtractBody, ExtractRelatesTo, ExtractRelatesToEventId, RoomMutexGuard};
@@ -132,6 +133,18 @@ pub async fn append_incoming_pdu<'a>(
self.services.sync.wake_all_joined(&room_id).await;
if *pdu.kind() == TimelineEventType::RoomMember {
if let Some(target_user) = pdu
.state_key
.as_ref()
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
{
if self.services.globals.user_is_local(&target_user) {
self.services.sync.wake(&target_user).await;
}
}
}
Ok(Some(pdu_id))
}
@@ -415,7 +428,9 @@ async fn notify_local_users(&self, pdu: &PduEvent, pdu_id: &RawPduId, room_id: &
return;
}
let serialized = pdu.to_format();
let serialized: Raw<AnySyncTimelineEvent> = pdu.to_format();
let room_joined_count = self.services.pusher.push_joined_count(room_id).await;
for user in &push_targets {
let rules_for_user = self
.services
@@ -430,12 +445,13 @@ async fn notify_local_users(&self, pdu: &PduEvent, pdu_id: &RawPduId, room_id: &
let mut highlight = false;
let mut notify = false;
for action in self
let ctx = self
.services
.pusher
.get_actions(user, &rules_for_user, power_levels.clone(), &serialized, room_id)
.await
{
.push_condition_ctx(user, power_levels.clone(), room_id, room_joined_count)
.await;
for action in rules_for_user.get_actions(&serialized, &ctx).await {
match action {
| Action::Notify => notify = true,
| Action::SetTweak(Tweak::Highlight(
+12
View File
@@ -155,6 +155,18 @@ pub async fn build_and_append_pdu(
self.services.sync.wake_all_joined(&room_id).await;
if *pdu.kind() == TimelineEventType::RoomMember {
if let Some(target_user) = pdu
.state_key
.as_ref()
.and_then(|state_key| UserId::parse(state_key.as_str()).ok())
{
if self.services.globals.user_is_local(&target_user) {
self.services.sync.wake(&target_user).await;
}
}
}
let mut servers: HashSet<OwnedServerName> = self
.services
.state_cache
+316
View File
@@ -0,0 +1,316 @@
//! Helpers for submitting events with the right checks performed
use conduwuit::{Err, Result, err, matrix::pdu::PartialPdu};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, UserId,
events::{
AnyStateEventContent, StateEventType,
room::{
canonical_alias::RoomCanonicalAliasEventContent,
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
server_acl::RoomServerAclEventContent,
},
},
serde::Raw,
};
use crate::rooms::state::RoomMutexGuard;
impl super::Service {
#[allow(clippy::too_many_arguments)]
pub async fn send_state_event_for_key(
&self,
sender: &UserId,
room_id: &RoomId,
state_lock: &RoomMutexGuard,
event_type: &StateEventType,
content: &Raw<AnyStateEventContent>,
state_key: &str,
timestamp: Option<MilliSecondsSinceUnixEpoch>,
) -> Result<OwnedEventId> {
let mut content: Raw<AnyStateEventContent> = content.clone();
self.assert_allowed_to_send_state_event(room_id, event_type, state_key, &mut content)
.await?;
let content = serde_json::from_str(content.json().get())
.map_err(|e| err!(Request(BadJson("Invalid JSON body: {e}"))))?;
let event_id = self
.build_and_append_pdu(
PartialPdu {
event_type: event_type.to_string().into(),
content,
state_key: Some(state_key.into()),
timestamp,
..Default::default()
},
sender,
Some(room_id),
state_lock,
)
.await?;
Ok(event_id)
}
async fn assert_allowed_to_send_state_event(
&self,
room_id: &RoomId,
event_type: &StateEventType,
state_key: &str,
json: &mut Raw<AnyStateEventContent>,
) -> Result {
match event_type {
| StateEventType::RoomCreate => {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"You cannot update m.room.create after a room has been created."
))));
},
| StateEventType::RoomServerAcl =>
self.assert_allowed_to_send_room_server_acl_event(room_id, json)
.await?,
| StateEventType::RoomEncryption =>
// Forbid m.room.encryption if encryption is disabled
if !self.services.config.allow_encryption {
return Err!(Request(Forbidden(
"Encryption is disabled on this homeserver."
)));
},
| StateEventType::RoomJoinRules =>
self.assert_allowed_to_send_room_join_rules_event(room_id, json)
.await?,
| StateEventType::RoomHistoryVisibility =>
self.assert_allowed_to_send_room_history_visibility_event(room_id, json)
.await?,
| StateEventType::RoomCanonicalAlias =>
self.assert_allowed_to_send_room_canonical_alias_event(room_id, json)
.await?,
| StateEventType::RoomMember =>
self.assert_allowed_to_send_room_member_event(room_id, state_key, json)
.await?,
| _ => (),
}
Ok(())
}
async fn assert_allowed_to_send_room_server_acl_event(
&self,
room_id: &RoomId,
json: &Raw<AnyStateEventContent>,
) -> Result {
// prevents common ACL paw-guns as ACL management is difficult and prone to
// irreversible mistakes
let acl_content = json
.deserialize_as_unchecked::<RoomServerAclEventContent>()
.map_err(|e| {
err!(Request(BadJson(debug_warn!("Room server ACL event is invalid: {e}"))))
})?;
let allow_has_wildcard = acl_content.allow.iter().any(|entry| entry == "*");
let deny_has_wildcard = acl_content.deny.iter().any(|entry| entry == "*");
let allow_has_server = acl_content
.allow
.iter()
.any(|entry| entry == self.services.globals.server_name().as_str());
if acl_content.allow.is_empty() {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with an empty allow key will permanently \
brick the room for non-conduwuit's as this equates to no servers \
being allowed to participate in this room."
))));
}
if allow_has_wildcard && deny_has_wildcard {
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with a deny and allow key value of \"*\" will \
permanently brick the room for non-conduwuit's as this equates to \
no servers being allowed to participate in this room."
))));
}
if deny_has_wildcard
&& !acl_content.is_allowed(self.services.globals.server_name())
&& !allow_has_server
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event with a deny key value of \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
if !allow_has_wildcard
&& !acl_content.is_allowed(self.services.globals.server_name())
&& !allow_has_server
{
return Err!(Request(BadJson(debug_warn!(
%room_id,
"Sending an ACL event for an allow key without \"*\" and without \
your own server name in the allow key will result in you being \
unable to participate in this room."
))));
}
Ok(())
}
async fn assert_allowed_to_send_room_join_rules_event(
&self,
room_id: &RoomId,
json: &Raw<AnyStateEventContent>,
) -> Result {
// admin room is a sensitive room, it should not ever be made public
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await
&& admin_room_id == room_id
{
let join_rule = json
.deserialize_as_unchecked::<RoomJoinRulesEventContent>()
.map_err(|e| {
err!(Request(BadJson(debug_warn!("Room join rules event is invalid: {e}"))))
})?;
if join_rule.join_rule == JoinRule::Public {
return Err!(Request(Forbidden(
"Admin room is a sensitive room, it cannot be made public"
)));
}
}
Ok(())
}
async fn assert_allowed_to_send_room_history_visibility_event(
&self,
room_id: &RoomId,
json: &Raw<AnyStateEventContent>,
) -> Result {
// admin room is a sensitive room, it should not ever be made world readable
if let Ok(admin_room_id) = self.services.admin.get_admin_room().await
&& admin_room_id == room_id
{
let visibility_content = json
.deserialize_as_unchecked::<RoomHistoryVisibilityEventContent>()
.map_err(|e| {
err!(Request(BadJson(debug_warn!(
"Room history visibility event is invalid: {e}"
))))
})?;
if visibility_content.history_visibility == HistoryVisibility::WorldReadable {
return Err!(Request(Forbidden(
"Admin room is a sensitive room, it cannot be made public"
)));
}
}
Ok(())
}
async fn assert_allowed_to_send_room_canonical_alias_event(
&self,
room_id: &RoomId,
json: &Raw<AnyStateEventContent>,
) -> Result {
let canonical_alias_content = json
.deserialize_as_unchecked::<RoomCanonicalAliasEventContent>()
.map_err(|e| {
err!(Request(BadJson(debug_warn!("Room canonical alias event is invalid: {e}"))))
})?;
let mut aliases = canonical_alias_content.alt_aliases.clone();
if let Some(alias) = canonical_alias_content.alias {
aliases.push(alias);
}
for alias in aliases {
let (alias_room_id, _) = self
.services
.alias
.resolve_alias(&alias)
.await
.map_err(|e| err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}"))))?;
if alias_room_id != room_id {
return Err!(Request(BadAlias(
"Room alias {alias} does not belong to room {room_id}"
)));
}
}
Ok(())
}
async fn assert_allowed_to_send_room_member_event(
&self,
room_id: &RoomId,
state_key: &str,
json: &mut Raw<AnyStateEventContent>,
) -> Result {
let mut membership_content = json
.deserialize_as_unchecked::<RoomMemberEventContent>()
.map_err(|e| {
err!(Request(BadJson(debug_warn!("Room member event is invalid: {e}"))))
})?;
let Ok(state_key) = UserId::parse(state_key) else {
return Err!(Request(BadJson(
"Membership event has invalid or non-existent state key"
)));
};
let Some(authorising_user) = membership_content.join_authorized_via_users_server else {
return Ok(());
};
// join_authorized_via_users_server must be thrown away, if user is
// already a member of the room.
if self
.services
.state_cache
.is_joined(&state_key, room_id)
.await
{
membership_content.join_authorized_via_users_server = None;
*json = Raw::<AnyStateEventContent>::from_json_string(serde_json::to_string(
&membership_content,
)?)?;
return Ok(());
}
if membership_content.membership != MembershipState::Join {
return Err!(Request(BadJson(
"join_authorised_via_users_server is only for member joins"
)));
}
if !self.services.globals.user_is_local(&authorising_user) {
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} does not belong to this homeserver"
)));
}
if !self
.services
.state_cache
.is_joined(&authorising_user, room_id)
.await
{
return Err!(Request(InvalidParam(
"Authorising user {authorising_user} is not in the room, they cannot authorise \
the join."
)));
}
Ok(())
}
}

Some files were not shown because too many files have changed in this diff Show More