mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-04-26 14:27:22 +00:00
Compare commits
46 Commits
nex/feat/a
...
nex/stater
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfd89425a1 | ||
|
|
c69e7c7d1b | ||
|
|
bd404e808c | ||
|
|
0899985476 | ||
|
|
b3cf649732 | ||
|
|
722bacbe89 | ||
|
|
46907e3dce | ||
|
|
31e2195e56 | ||
|
|
7ecac93ddc | ||
|
|
6a0b103722 | ||
|
|
23d77b614f | ||
|
|
e01aa44b16 | ||
|
|
a08739c246 | ||
|
|
c14864b881 | ||
|
|
1773e72e68 | ||
|
|
0f94d55689 | ||
|
|
abfb6377c2 | ||
|
|
91d64f5b24 | ||
|
|
9a3f3f6e78 | ||
|
|
b3e31a4aad | ||
|
|
8cda431cc6 | ||
|
|
02b9a3f713 | ||
|
|
d40893730c | ||
|
|
28fae58cf6 | ||
|
|
f458f6ab76 | ||
|
|
fdf9cea533 | ||
|
|
ecb1b73c84 | ||
|
|
e03082480a | ||
|
|
f9e7f019ad | ||
|
|
12069e7c86 | ||
|
|
77928a62b4 | ||
|
|
c73cb5c1bf | ||
|
|
a140eacb04 | ||
|
|
40536b13da | ||
|
|
cacd8681d1 | ||
|
|
b095518e6f | ||
|
|
a91add4aca | ||
|
|
7fec48423a | ||
|
|
2f6b7c7a40 | ||
|
|
48ab6adec1 | ||
|
|
592244d5aa | ||
|
|
091893f8bc | ||
|
|
6eba6a838e | ||
|
|
1a11c784f5 | ||
|
|
55ccfdb973 | ||
|
|
a9a39e6d5e |
@@ -23,7 +23,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.43.4
|
||||
rev: v1.43.5
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
|
||||
@@ -24,3 +24,5 @@ extend-ignore-re = [
|
||||
"continuwity" = "continuwuity"
|
||||
"execuse" = "execuse"
|
||||
"oltp" = "OTLP"
|
||||
|
||||
rememvering = "remembering"
|
||||
|
||||
@@ -85,24 +85,31 @@ ### Matrix tests
|
||||
|
||||
### Writing documentation
|
||||
|
||||
Continuwuity's website uses [`mdbook`][mdbook] and is deployed via CI using Cloudflare Pages
|
||||
Continuwuity's website uses [`rspress`][rspress] and is deployed via CI using Cloudflare Pages
|
||||
in the [`documentation.yml`][documentation.yml] workflow file. All documentation is in the `docs/`
|
||||
directory at the top level.
|
||||
|
||||
To build the documentation locally:
|
||||
To load the documentation locally:
|
||||
|
||||
1. Install NodeJS and npm from their [official website][nodejs-download] or via your package manager of choice
|
||||
|
||||
2. From the project's root directory, install the relevant npm modules
|
||||
|
||||
1. Install mdbook if you don't have it already:
|
||||
```bash
|
||||
cargo install mdbook # or cargo binstall, or another method
|
||||
npm ci
|
||||
```
|
||||
|
||||
2. Build the documentation:
|
||||
3. Make changes to the document pages as you see fit
|
||||
|
||||
4. Generate a live preview of the documentation
|
||||
|
||||
```bash
|
||||
mdbook build
|
||||
npm run docs:dev
|
||||
```
|
||||
|
||||
The output of the mdbook generation is in `public/`. You can open the HTML files directly in your browser without needing a web server.
|
||||
A webserver for the docs will be spun up for you (e.g. at `http://localhost:3000`). Any changes you make to the documentation will be live-reloaded on the webpage.
|
||||
|
||||
Alternatively, you can build the documentation using `npm run docs:build` - the output of this will be in the `/doc_build` directory. Once you're happy with your documentation updates, you can commit the changes.
|
||||
|
||||
### Commit Messages
|
||||
|
||||
@@ -169,5 +176,6 @@ ### Creating pull requests
|
||||
[continuwuity-matrix]: https://matrix.to/#/#continuwuity:continuwuity.org?via=continuwuity.org&via=ellis.link&via=explodie.org&via=matrix.org
|
||||
[complement]: https://github.com/matrix-org/complement/
|
||||
[sytest]: https://github.com/matrix-org/sytest/
|
||||
[mdbook]: https://rust-lang.github.io/mdBook/
|
||||
[nodejs-download]: https://nodejs.org/en/download
|
||||
[rspress]: https://rspress.rs/
|
||||
[documentation.yml]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/.forgejo/workflows/documentation.yml
|
||||
|
||||
604
Cargo.lock
generated
604
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -84,7 +84,7 @@ version = "1.3.1"
|
||||
version = "1.11.1"
|
||||
|
||||
[workspace.dependencies.axum]
|
||||
version = "0.7.9"
|
||||
version = "0.8.8"
|
||||
default-features = false
|
||||
features = [
|
||||
"form",
|
||||
@@ -97,7 +97,7 @@ features = [
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
version = "0.9.6"
|
||||
version = "0.10.1"
|
||||
default-features = false
|
||||
features = ["typed-header", "tracing"]
|
||||
|
||||
@@ -110,7 +110,7 @@ default-features = false
|
||||
version = "0.7"
|
||||
|
||||
[workspace.dependencies.axum-client-ip]
|
||||
version = "0.6.1"
|
||||
version = "0.7"
|
||||
|
||||
[workspace.dependencies.tower]
|
||||
version = "0.5.2"
|
||||
@@ -118,7 +118,7 @@ default-features = false
|
||||
features = ["util"]
|
||||
|
||||
[workspace.dependencies.tower-http]
|
||||
version = "0.6.2"
|
||||
version = "0.6.8"
|
||||
default-features = false
|
||||
features = [
|
||||
"add-extension",
|
||||
@@ -158,7 +158,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.18"
|
||||
version = "0.0.19"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -342,7 +342,8 @@ version = "0.1.2"
|
||||
# Used for matrix spec type definitions and helpers
|
||||
[workspace.dependencies.ruma]
|
||||
git = "https://forgejo.ellis.link/continuwuation/ruwuma"
|
||||
rev = "b496b7f38d517149361a882e75d3fd4faf210441"
|
||||
#branch = "conduwuit-changes"
|
||||
rev = "3126cb5eea991ec40590e54d8c9d75637650641a"
|
||||
features = [
|
||||
"compat",
|
||||
"rand",
|
||||
@@ -553,7 +554,7 @@ version = "0.7.5"
|
||||
version = "1.0.1"
|
||||
|
||||
[workspace.dependencies.askama]
|
||||
version = "0.14.0"
|
||||
version = "0.15.0"
|
||||
|
||||
#
|
||||
# Patches
|
||||
|
||||
1
changelog.d/1393.bugfix
Normal file
1
changelog.d/1393.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Removed non-compliant nor functional room alias lookups over federation. Contributed by @nex
|
||||
1
changelog.d/1399.feature
Normal file
1
changelog.d/1399.feature
Normal file
@@ -0,0 +1 @@
|
||||
Outgoing presence is now disabled by default, and the config option documentation has been adjusted to more accurately represent the weight of presence, typing indicators, and read receipts. Contributed by @nex.
|
||||
1
changelog.d/1418.bugfix
Normal file
1
changelog.d/1418.bugfix
Normal file
@@ -0,0 +1 @@
|
||||
Removed ability to set rocksdb as read only. Doing so would cause unintentional and buggy behaviour. Contributed by @Terryiscool160.
|
||||
1
changelog.d/list-backups-formatting.feature
Normal file
1
changelog.d/list-backups-formatting.feature
Normal file
@@ -0,0 +1 @@
|
||||
Updated `list-backups` admin command to output one backup per line.
|
||||
@@ -1056,14 +1056,6 @@
|
||||
#
|
||||
#rocksdb_repair = false
|
||||
|
||||
# This item is undocumented. Please contribute documentation for it.
|
||||
#
|
||||
#rocksdb_read_only = false
|
||||
|
||||
# This item is undocumented. Please contribute documentation for it.
|
||||
#
|
||||
#rocksdb_secondary = false
|
||||
|
||||
# Enables idle CPU priority for compaction thread. This is not enabled by
|
||||
# default to prevent compaction from falling too far behind on busy
|
||||
# systems.
|
||||
@@ -1120,27 +1112,34 @@
|
||||
|
||||
# Allow local (your server only) presence updates/requests.
|
||||
#
|
||||
# Note that presence on continuwuity is very fast unlike Synapse's. If
|
||||
# using outgoing presence, this MUST be enabled.
|
||||
# Local presence must be enabled for outgoing presence to function.
|
||||
#
|
||||
# Note that local presence is not as heavy on the CPU as federated
|
||||
# presence, but will still become more expensive the more local users you
|
||||
# have.
|
||||
#
|
||||
#allow_local_presence = true
|
||||
|
||||
# Allow incoming federated presence updates/requests.
|
||||
# Allow incoming federated presence updates.
|
||||
#
|
||||
# This option receives presence updates from other servers, but does not
|
||||
# send any unless `allow_outgoing_presence` is true. Note that presence on
|
||||
# continuwuity is very fast unlike Synapse's.
|
||||
# This option enables processing inbound presence updates from other
|
||||
# servers. Without it, remote users will appear as if they are always
|
||||
# offline to your local users. This does not affect typing indicators or
|
||||
# read receipts.
|
||||
#
|
||||
#allow_incoming_presence = true
|
||||
|
||||
# Allow outgoing presence updates/requests.
|
||||
#
|
||||
# This option sends presence updates to other servers, but does not
|
||||
# receive any unless `allow_incoming_presence` is true. Note that presence
|
||||
# on continuwuity is very fast unlike Synapse's. If using outgoing
|
||||
# presence, you MUST enable `allow_local_presence` as well.
|
||||
# This option sends presence updates to other servers, and requires that
|
||||
# `allow_local_presence` is also enabled.
|
||||
#
|
||||
#allow_outgoing_presence = true
|
||||
# Note that outgoing presence is very heavy on the CPU and network, and
|
||||
# will typically cause extreme strain and slowdowns for no real benefit.
|
||||
# There are only a few clients that even implement presence, so you
|
||||
# probably don't want to enable this.
|
||||
#
|
||||
#allow_outgoing_presence = false
|
||||
|
||||
# How many seconds without presence updates before you become idle.
|
||||
# Defaults to 5 minutes.
|
||||
@@ -1174,6 +1173,10 @@
|
||||
|
||||
# Allow sending read receipts to remote servers.
|
||||
#
|
||||
# Note that sending read receipts to remote servers in large rooms with
|
||||
# lots of other homeservers may cause additional strain on the CPU and
|
||||
# network.
|
||||
#
|
||||
#allow_outgoing_read_receipts = true
|
||||
|
||||
# Allow local typing updates.
|
||||
@@ -1185,6 +1188,10 @@
|
||||
|
||||
# Allow outgoing typing updates to federation.
|
||||
#
|
||||
# Note that sending typing indicators to remote servers in large rooms
|
||||
# with lots of other homeservers may cause additional strain on the CPU
|
||||
# and network.
|
||||
#
|
||||
#allow_outgoing_typing = true
|
||||
|
||||
# Allow incoming typing updates from federation.
|
||||
|
||||
@@ -162,6 +162,7 @@ ENV CONDUWUIT_VERSION_EXTRA=$CONDUWUIT_VERSION_EXTRA
|
||||
ENV CONTINUWUITY_VERSION_EXTRA=$CONTINUWUITY_VERSION_EXTRA
|
||||
|
||||
ARG RUST_PROFILE=release
|
||||
ARG CARGO_FEATURES="default,http3"
|
||||
|
||||
# Build the binary
|
||||
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
@@ -171,11 +172,20 @@ RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
||||
set -o allexport
|
||||
set -o xtrace
|
||||
. /etc/environment
|
||||
|
||||
# Check if http3 feature is enabled and set appropriate RUSTFLAGS
|
||||
if echo "${CARGO_FEATURES}" | grep -q "http3"; then
|
||||
export RUSTFLAGS="${RUSTFLAGS} --cfg reqwest_unstable"
|
||||
else
|
||||
export RUSTFLAGS="${RUSTFLAGS}"
|
||||
fi
|
||||
|
||||
TARGET_DIR=($(cargo metadata --no-deps --format-version 1 | \
|
||||
jq -r ".target_directory"))
|
||||
mkdir /out/sbin
|
||||
PACKAGE=conduwuit
|
||||
xx-cargo build --locked --profile ${RUST_PROFILE} \
|
||||
--no-default-features --features ${CARGO_FEATURES} \
|
||||
-p $PACKAGE;
|
||||
BINARIES=($(cargo metadata --no-deps --format-version 1 | \
|
||||
jq -r ".packages[] | select(.name == \"$PACKAGE\") | .targets[] | select( .kind | map(. == \"bin\") | any ) | .name"))
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
"label": "Deploying"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "turn",
|
||||
"label": "TURN"
|
||||
"type": "dir",
|
||||
"name": "calls",
|
||||
"label": "Calls"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
{
|
||||
"text": "Guide",
|
||||
"link": "/introduction",
|
||||
"activeMatch": "^/(introduction|configuration|deploying|turn|appservices|maintenance|troubleshooting)"
|
||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting)"
|
||||
},
|
||||
{
|
||||
"text": "Development",
|
||||
|
||||
13
docs/calls.mdx
Normal file
13
docs/calls.mdx
Normal file
@@ -0,0 +1,13 @@
|
||||
# Calls
|
||||
|
||||
Matrix supports two types of calls:
|
||||
|
||||
- Element Call powered by [MatrixRTC](https://half-shot.github.io/msc-crafter/#msc/4143) and [LiveKit](https://github.com/livekit/livekit)
|
||||
- Legacy calls, sometimes using Jitsi
|
||||
|
||||
Both types of calls are supported by different sets of clients, but most clients are moving towards MatrixRTC / Element Call.
|
||||
|
||||
For either one to work correctly, you have to do some additional setup.
|
||||
|
||||
- For legacy calls to work, you need to set up a TURN/STUN server. [Read the TURN guide for tips on how to set up coturn](./calls/turn.mdx)
|
||||
- For MatrixRTC / Element Call to work, you have to set up the LiveKit backend (foci). LiveKit also uses TURN/STUN to increase reliability, so you might want to configure your TURN server first. [Read the LiveKit guide](./calls/livekit.mdx)
|
||||
12
docs/calls/_meta.json
Normal file
12
docs/calls/_meta.json
Normal file
@@ -0,0 +1,12 @@
|
||||
[
|
||||
{
|
||||
"type": "file",
|
||||
"name": "turn",
|
||||
"label": "TURN"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "livekit",
|
||||
"label": "MatrixRTC / LiveKit"
|
||||
}
|
||||
]
|
||||
268
docs/calls/livekit.mdx
Normal file
268
docs/calls/livekit.mdx
Normal file
@@ -0,0 +1,268 @@
|
||||
# Matrix RTC/Element Call Setup
|
||||
|
||||
:::info
|
||||
This guide assumes that you are using docker compose for deployment. LiveKit only provides Docker images.
|
||||
:::
|
||||
|
||||
## Instructions
|
||||
|
||||
### 1. Domain
|
||||
|
||||
LiveKit should live on its own domain or subdomain. In this guide we use `livekit.example.com` - this should be replaced with a domain you control.
|
||||
|
||||
Make sure the DNS record for the (sub)domain you plan to use is pointed to your server.
|
||||
|
||||
### 2. Services
|
||||
|
||||
Using LiveKit with Matrix requires two services - Livekit itself, and a service (`lk-jwt-service`) that grants Matrix users permission to connect to it.
|
||||
|
||||
You must generate a key and secret to allow the Matrix service to authenticate with LiveKit. `LK_MATRIX_KEY` should be around 20 random characters, and `LK_MATRIX_SECRET` should be around 64. Remember to replace these with the actual values!
|
||||
|
||||
:::tip Generating the secrets
|
||||
LiveKit provides a utility to generate secure random keys
|
||||
```bash
|
||||
docker run --rm livekit/livekit-server:latest generate-keys
|
||||
```
|
||||
:::
|
||||
|
||||
```yaml
|
||||
services:
|
||||
lk-jwt-service:
|
||||
image: ghcr.io/element-hq/lk-jwt-service:latest
|
||||
container_name: lk-jwt-service
|
||||
environment:
|
||||
- LIVEKIT_JWT_BIND=:8081
|
||||
- LIVEKIT_URL=wss://livekit.example.com
|
||||
- LIVEKIT_KEY=LK_MATRIX_KEY
|
||||
- LIVEKIT_SECRET=LK_MATRIX_SECRET
|
||||
- LIVEKIT_FULL_ACCESS_HOMESERVERS=example.com
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8081:8081"
|
||||
|
||||
livekit:
|
||||
image: livekit/livekit-server:latest
|
||||
container_name: livekit
|
||||
command: --config /etc/livekit.yaml
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./livekit.yaml:/etc/livekit.yaml:ro
|
||||
network_mode: "host" # /!\ LiveKit binds to all addresses by default.
|
||||
# Make sure port 7880 is blocked by your firewall to prevent access bypassing your reverse proxy
|
||||
# Alternatively, uncomment the lines below and comment `network_mode: "host"` above to specify port mappings.
|
||||
# ports:
|
||||
# - "127.0.0.1:7880:7880/tcp"
|
||||
# - "7881:7881/tcp"
|
||||
# - "50100-50200:50100-50200/udp"
|
||||
```
|
||||
|
||||
Next, we need to configure LiveKit. In the same directory, create `livekit.yaml` with the following content - remembering to replace `LK_MATRIX_KEY` and `LK_MATRIX_SECRET` with the values you generated:
|
||||
|
||||
```yaml
|
||||
port: 7880
|
||||
bind_addresses:
|
||||
- ""
|
||||
rtc:
|
||||
tcp_port: 7881
|
||||
port_range_start: 50100
|
||||
port_range_end: 50200
|
||||
use_external_ip: true
|
||||
enable_loopback_candidate: false
|
||||
keys:
|
||||
LK_MATRIX_KEY: LK_MATRIX_SECRET
|
||||
```
|
||||
|
||||
#### Firewall hints
|
||||
|
||||
You will need to allow ports `7881/tcp` and `50100:50200/udp` through your firewall. If you use UFW, the commands are: `ufw allow 7881/tcp` and `ufw allow 50100:50200/udp`.
|
||||
|
||||
### 3. Telling clients where to find LiveKit
|
||||
|
||||
To tell clients where to find LiveKit, you need to add the address of your `lk-jwt-service` to your client .well-known file. To do so, in the config section `global.well-known`, add (or modify) the option `rtc_focus_server_urls`.
|
||||
|
||||
The variable should be a list of servers serving as MatrixRTC endpoints to serve in the well-known file to the client.
|
||||
|
||||
```toml
|
||||
rtc_focus_server_urls = [
|
||||
{ type = "livekit", livekit_service_url = "https://livekit.example.com" },
|
||||
]
|
||||
```
|
||||
|
||||
Remember to replace the URL with the address you are deploying your instance of lk-jwt-service to.
|
||||
|
||||
#### Serving .well-known manually
|
||||
|
||||
If you don't let Continuwuity serve your `.well-known` files, you need to add the following lines to your `.well-known/matrix/client` file, remembering to replace the URL with your own `lk-jwt-service` deployment:
|
||||
|
||||
```json
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://livekit.example.com"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
The final file should look something like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"m.homeserver": {
|
||||
"base_url":"https://matrix.example.com"
|
||||
},
|
||||
"org.matrix.msc4143.rtc_foci": [
|
||||
{
|
||||
"type": "livekit",
|
||||
"livekit_service_url": "https://livekit.example.com"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Configure your Reverse Proxy
|
||||
|
||||
Reverse proxies can be configured in many different ways - so we can't provide a step by step for this.
|
||||
|
||||
By default, all routes should be forwarded to Livekit with the exception of the following path prefixes, which should be forwarded to the JWT/Authentication service:
|
||||
|
||||
- `/sfu/get`
|
||||
- `/healthz`
|
||||
- `/get_token`
|
||||
|
||||
<details>
|
||||
<summary>Example caddy config</summary>
|
||||
```
|
||||
matrix-rtc.example.com {
|
||||
|
||||
# for lk-jwt-service
|
||||
@lk-jwt-service path /sfu/get* /healthz* /get_token*
|
||||
route @lk-jwt-service {
|
||||
reverse_proxy 127.0.0.1:8081
|
||||
}
|
||||
|
||||
# for livekit
|
||||
reverse_proxy 127.0.0.1:7880
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example nginx config</summary>
|
||||
```
|
||||
server {
|
||||
server_name matrix-rtc.example.com;
|
||||
|
||||
# for lk-jwt-service
|
||||
location ~ ^/(sfu/get|healthz|get_token) {
|
||||
proxy_pass http://127.0.0.1:8081$request_uri;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
}
|
||||
|
||||
# for livekit
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:7880$request_uri;
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_buffering off;
|
||||
|
||||
# websocket
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that for websockets to work, you need to have this somewhere outside your server block:
|
||||
```
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
'' close;
|
||||
}
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Example traefik router</summary>
|
||||
```
|
||||
# on LiveKit itself
|
||||
traefik.http.routers.livekit.rule=Host(`livekit.example.com`)
|
||||
# on the JWT service
|
||||
traefik.http.routers.livekit-jwt.rule=Host(`livekit.example.com`) && (PathPrefix(`/sfu/get`) || PathPrefix(`/healthz`) || PathPrefix(`/get_token`))
|
||||
```
|
||||
</details>
|
||||
|
||||
|
||||
### 6. Start Everything
|
||||
|
||||
Start up the services using your usual method - for example `docker compose up -d`.
|
||||
|
||||
## Additional Configuration
|
||||
|
||||
### TURN Integration
|
||||
|
||||
If you've already set up coturn, there may be a port clash between the two services. To fix this, make sure the `min-port` and `max-port` for coturn so it doesn't overlap with LiveKit's range:
|
||||
|
||||
```ini
|
||||
min-port=50201
|
||||
max-port=65535
|
||||
```
|
||||
|
||||
To improve LiveKit's reliability, you can configure it to use your coturn server.
|
||||
|
||||
Generate a long random secret for LiveKit, and add it to your coturn config under the `static-auth-secret` option. You can add as many secrets as you want - so set a different one for each thing using your TURN server.
|
||||
|
||||
Then configure livekit, making sure to replace `COTURN_SECRET`:
|
||||
|
||||
```yaml
|
||||
# livekit.yaml
|
||||
rtc:
|
||||
turn_servers:
|
||||
- host: coturn.ellis.link
|
||||
port: 3478
|
||||
protocol: tcp
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.ellis.link
|
||||
port: 5349
|
||||
protocol: tls # Only if you've set up TLS in your coturn
|
||||
secret: "COTURN_SECRET"
|
||||
- host: coturn.ellis.link
|
||||
port: 3478
|
||||
protocol: udp
|
||||
secret: "COTURN_SECRET"
|
||||
```
|
||||
|
||||
## LiveKit's built in TURN server
|
||||
|
||||
Livekit includes a built in TURN server which can be used in place of an external option. This TURN server will only work with Livekit, so you can't use it for legacy Matrix calling - or anything else.
|
||||
|
||||
If you don't want to set up a separate TURN server, you can enable this with the following changes:
|
||||
|
||||
```yaml
|
||||
### add this to livekit.yaml ###
|
||||
turn:
|
||||
enabled: true
|
||||
udp_port: 3478
|
||||
relay_range_start: 50300
|
||||
relay_range_end: 50400
|
||||
domain: matrix-rtc.example.com
|
||||
```
|
||||
|
||||
```yaml
|
||||
### Add these to docker-compose ###
|
||||
- "3478:3478/udp"
|
||||
- "50300-50400:50300-50400/udp"
|
||||
```
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [LiveKit GitHub](https://github.com/livekit/livekit)
|
||||
- [LiveKit Connection Tester](https://livekit.io/connection-test) - use with the token returned by `/sfu/get` or `/get_token`
|
||||
- [MatrixRTC proposal](https://half-shot.github.io/msc-crafter/#msc/4143)
|
||||
- [Synapse documentation](https://github.com/element-hq/element-call/blob/livekit/docs/self-hosting.md)
|
||||
- [Community guide](https://tomfos.tr/matrix/livekit/)
|
||||
- [Community guide](https://blog.kimiblock.top/2024/12/24/hosting-element-call/)
|
||||
214
docs/calls/turn.mdx
Normal file
214
docs/calls/turn.mdx
Normal file
@@ -0,0 +1,214 @@
|
||||
# Setting up TURN/STUN
|
||||
|
||||
[TURN](https://en.wikipedia.org/wiki/Traversal_Using_Relays_around_NAT) and [STUN](https://en.wikipedia.org/wiki/STUN) are used as a component in many calling systems. Matrix uses them directly for legacy calls and indirectly for MatrixRTC via Livekit.
|
||||
|
||||
Continuwuity recommends using [Coturn](https://github.com/coturn/coturn) as your TURN/STUN server, which is available as a Docker image or a distro package.
|
||||
|
||||
## Installing Coturn
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a configuration file called `coturn.conf` containing:
|
||||
|
||||
```ini
|
||||
use-auth-secret
|
||||
static-auth-secret=<a secret key>
|
||||
realm=<your server domain>
|
||||
```
|
||||
|
||||
:::tip Generating a secure secret
|
||||
A common way to generate a suitable alphanumeric secret key is by using:
|
||||
```bash
|
||||
pwgen -s 64 1
|
||||
```
|
||||
:::
|
||||
|
||||
#### Port Configuration
|
||||
|
||||
By default, coturn uses the following ports:
|
||||
- `3478` (UDP/TCP): Standard TURN/STUN port
|
||||
- `5349` (UDP/TCP): TURN/STUN over TLS
|
||||
- `49152-65535` (UDP): Media relay ports
|
||||
|
||||
If you're also running LiveKit, you'll need to avoid port conflicts. Configure non-overlapping port ranges:
|
||||
|
||||
```ini
|
||||
# In coturn.conf
|
||||
min-port=50201
|
||||
max-port=65535
|
||||
```
|
||||
|
||||
This leaves ports `50100-50200` available for LiveKit's default configuration.
|
||||
|
||||
### Running with Docker
|
||||
|
||||
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using:
|
||||
|
||||
```bash
|
||||
docker run -d --network=host \
|
||||
-v $(pwd)/coturn.conf:/etc/coturn/turnserver.conf \
|
||||
coturn/coturn
|
||||
```
|
||||
|
||||
### Running with Docker Compose
|
||||
|
||||
Create a `docker-compose.yml` file and run `docker compose up -d`:
|
||||
|
||||
```yaml
|
||||
version: '3'
|
||||
services:
|
||||
turn:
|
||||
container_name: coturn-server
|
||||
image: docker.io/coturn/coturn
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./coturn.conf:/etc/coturn/turnserver.conf
|
||||
```
|
||||
|
||||
:::info Why host networking?
|
||||
Coturn uses host networking mode because it needs to bind to multiple ports and work with various network protocols. Using host networking is better for performance, and reduces configuration complexity. To understand alternative configuration options, visit [Coturn's Docker documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
|
||||
:::
|
||||
|
||||
### Security Recommendations
|
||||
|
||||
For security best practices, see Synapse's [Coturn documentation](https://element-hq.github.io/synapse/latest/turn-howto.html), which includes important firewall and access control recommendations.
|
||||
|
||||
## Configuring Continuwuity
|
||||
|
||||
Once your TURN server is running, configure Continuwuity to provide credentials to clients. Add the following to your Continuwuity configuration file:
|
||||
|
||||
### Shared Secret Authentication (Recommended)
|
||||
|
||||
This is the most secure method and generates time-limited credentials automatically:
|
||||
|
||||
```toml
|
||||
# TURN URIs that clients should connect to
|
||||
turn_uris = [
|
||||
"turn:coturn.example.com?transport=udp",
|
||||
"turn:coturn.example.com?transport=tcp",
|
||||
"turns:coturn.example.com?transport=udp",
|
||||
"turns:coturn.example.com?transport=tcp"
|
||||
]
|
||||
|
||||
# Shared secret for generating credentials (must match coturn's static-auth-secret)
|
||||
turn_secret = "<your coturn static-auth-secret>"
|
||||
|
||||
# Optional: Read secret from a file instead (takes priority over turn_secret)
|
||||
# turn_secret_file = "/etc/continuwuity/.turn_secret"
|
||||
|
||||
# TTL for generated credentials in seconds (default: 86400 = 24 hours)
|
||||
turn_ttl = 86400
|
||||
```
|
||||
|
||||
:::tip Using TLS
|
||||
The `turns:` URI prefix instructs clients to connect to TURN over TLS, which is highly recommended for security. Make sure you've configured TLS in your coturn server first.
|
||||
:::
|
||||
|
||||
### Static Credentials (Alternative)
|
||||
|
||||
If you prefer static username/password credentials instead of shared secrets:
|
||||
|
||||
```toml
|
||||
turn_uris = [
|
||||
"turn:coturn.example.com?transport=udp",
|
||||
"turn:coturn.example.com?transport=tcp"
|
||||
]
|
||||
|
||||
turn_username = "your_username"
|
||||
turn_password = "your_password"
|
||||
```
|
||||
|
||||
:::warning
|
||||
Static credentials are less secure than shared secrets because they don't expire and must be configured in coturn separately. It is strongly advised you use shared secret authentication.
|
||||
:::
|
||||
|
||||
### Guest Access
|
||||
|
||||
By default, TURN credentials require client authentication. To allow unauthenticated access:
|
||||
|
||||
```toml
|
||||
turn_allow_guests = true
|
||||
```
|
||||
|
||||
:::caution
|
||||
This is not recommended as it allows unauthenticated users to access your TURN server, potentially enabling abuse by bots. All major Matrix clients that support legacy calls *also* support authenticated TURN access.
|
||||
:::
|
||||
|
||||
### Important Notes
|
||||
|
||||
- Replace `coturn.example.com` with your actual TURN server domain (the `realm` from coturn.conf)
|
||||
- The `turn_secret` must match the `static-auth-secret` in your coturn configuration
|
||||
- Restart or reload Continuwuity after making configuration changes
|
||||
|
||||
## Testing Your TURN Server
|
||||
|
||||
### Testing Credentials
|
||||
|
||||
Verify that Continuwuity is correctly serving TURN credentials to clients:
|
||||
|
||||
```bash
|
||||
curl "https://matrix.example.com/_matrix/client/r0/voip/turnServer" \
|
||||
-H "Authorization: Bearer <your_client_token>" | jq
|
||||
```
|
||||
|
||||
You should receive a response like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "1752792167:@jade:example.com",
|
||||
"password": "KjlDlawdPbU9mvP4bhdV/2c/h65=",
|
||||
"uris": [
|
||||
"turns:coturn.example.com?transport=udp",
|
||||
"turns:coturn.example.com?transport=tcp",
|
||||
"turn:coturn.example.com?transport=udp",
|
||||
"turn:coturn.example.com?transport=tcp"
|
||||
],
|
||||
"ttl": 86400
|
||||
}
|
||||
```
|
||||
|
||||
:::note MSC4166 Compliance
|
||||
If no TURN URIs are configured (`turn_uris` is empty), Continuwuity will return a 404 Not Found response, as specified in MSC4166.
|
||||
:::
|
||||
|
||||
### Testing Connectivity
|
||||
|
||||
Use [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/) to verify that the TURN credentials actually work:
|
||||
|
||||
1. Copy the credentials from the response above
|
||||
2. Paste them into the Trickle ICE testing tool
|
||||
3. Click "Gather candidates"
|
||||
4. Look for successful `relay` candidates in the results
|
||||
|
||||
If you see relay candidates, your TURN server is working correctly!
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Clients can't connect to TURN server
|
||||
|
||||
- Verify firewall rules allow the necessary ports (3478, 5349, and your media port range)
|
||||
- Check that DNS resolves correctly for your TURN domain
|
||||
- Ensure your `turn_secret` matches coturn's `static-auth-secret`
|
||||
- Test with Trickle ICE to isolate the issue
|
||||
|
||||
### Port conflicts with LiveKit
|
||||
|
||||
- Make sure coturn's `min-port` starts above LiveKit's `port_range_end` (default: 50200)
|
||||
- Or adjust LiveKit's port range to avoid coturn's default range
|
||||
|
||||
### 404 when calling turnServer endpoint
|
||||
|
||||
- Verify that `turn_uris` is not empty in your Continuwuity config
|
||||
- This behavior is correct per MSC4166 if no TURN URIs are configured
|
||||
|
||||
### Credentials expire too quickly
|
||||
|
||||
- Adjust the `turn_ttl` value in your Continuwuity configuration
|
||||
- Default is 86400 seconds (24 hours)
|
||||
|
||||
### Related Documentation
|
||||
|
||||
- [MatrixRTC/LiveKit Setup](./livekit.mdx) - Configure group calling with LiveKit
|
||||
- [Coturn GitHub](https://github.com/coturn/coturn) - Official coturn repository
|
||||
- [Synapse TURN Guide](https://element-hq.github.io/synapse/latest/turn-howto.html) - Additional security recommendations
|
||||
@@ -217,4 +217,4 @@ ### Use Traefik as Proxy
|
||||
|
||||
## Voice communication
|
||||
|
||||
See the [TURN](../turn.md) page.
|
||||
See the [Calls](../calls.mdx) page.
|
||||
|
||||
@@ -3,3 +3,5 @@ # Continuwuity for FreeBSD
|
||||
Continuwuity currently does not provide FreeBSD builds or FreeBSD packaging. However, Continuwuity does build and work on FreeBSD using the system-provided RocksDB.
|
||||
|
||||
Contributions to get Continuwuity packaged for FreeBSD are welcome.
|
||||
|
||||
Please join our [Continuwuity BSD](https://matrix.to/#/%23bsd:continuwuity.org) community room.
|
||||
|
||||
@@ -56,6 +56,8 @@ ### Building with the Rust toolchain
|
||||
|
||||
You can build Continuwuity using `cargo build --release`.
|
||||
|
||||
Continuwuity supports various optional features that can be enabled during compilation. Please see the Cargo.toml file for a comprehensive list, or ask in our rooms.
|
||||
|
||||
### Building with Nix
|
||||
|
||||
If you prefer, you can use Nix (or [Lix](https://lix.systems)) to build Continuwuity. This provides improved reproducibility and makes it easy to set up a build environment and generate output. This approach also allows for easy cross-compilation.
|
||||
@@ -277,7 +279,7 @@ # What's next?
|
||||
|
||||
## Audio/Video calls
|
||||
|
||||
For Audio/Video call functionality see the [TURN Guide](../turn.md).
|
||||
For Audio/Video call functionality see the [Calls](../calls.md) page.
|
||||
|
||||
## Appservices
|
||||
|
||||
|
||||
@@ -1,7 +1,109 @@
|
||||
# Continuwuity for Kubernetes
|
||||
|
||||
Continuwuity doesn't support horizontal scalability or distributed loading
|
||||
natively. However, [a community-maintained Helm Chart is available here to run
|
||||
natively. However, a deployment in Kubernetes is very similar to the docker
|
||||
setup. This is because Continuwuity can be fully configured using environment
|
||||
variables. A sample StatefulSet is shared below. The only thing missing is
|
||||
a PVC definition (named `continuwuity-data`) for the volume mounted to
|
||||
the StatefulSet, an Ingress resources to point your webserver to the
|
||||
Continuwuity Pods, and a Service resource (targeting `app.kubernetes.io/name: continuwuity`)
|
||||
to glue the Ingress and Pod together.
|
||||
|
||||
Carefully go through the `env` section and add, change, and remove any env vars you like using the [Configuration reference](https://continuwuity.org/reference/config.html)
|
||||
|
||||
```yaml
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: continuwuity
|
||||
namespace: matrix
|
||||
labels:
|
||||
app.kubernetes.io/name: continuwuity
|
||||
spec:
|
||||
replicas: 1
|
||||
serviceName: continuwuity
|
||||
podManagementPolicy: Parallel
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: continuwuity
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: continuwuity
|
||||
spec:
|
||||
securityContext:
|
||||
sysctls:
|
||||
- name: net.ipv4.ip_unprivileged_port_start
|
||||
value: "0"
|
||||
containers:
|
||||
- name: continuwuity
|
||||
# use a sha hash <3
|
||||
image: forgejo.ellis.link/continuwuation/continuwuity:latest
|
||||
imagePullPolicy: IfNotPresent
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 80
|
||||
volumeMounts:
|
||||
- mountPath: /data
|
||||
name: data
|
||||
subPath: data
|
||||
securityContext:
|
||||
capabilities:
|
||||
add:
|
||||
- NET_BIND_SERVICE
|
||||
env:
|
||||
- name: TOKIO_WORKER_THREADS
|
||||
value: "2"
|
||||
- name: CONTINUWUITY_SERVER_NAME
|
||||
value: "example.com"
|
||||
- name: CONTINUWUITY_DATABASE_PATH
|
||||
value: "/data/db"
|
||||
- name: CONTINUWUITY_DATABASE_BACKEND
|
||||
value: "rocksdb"
|
||||
- name: CONTINUWUITY_PORT
|
||||
value: "80"
|
||||
- name: CONTINUWUITY_MAX_REQUEST_SIZE
|
||||
value: "20000000"
|
||||
- name: CONTINUWUITY_ALLOW_FEDERATION
|
||||
value: "true"
|
||||
- name: CONTINUWUITY_TRUSTED_SERVERS
|
||||
value: '["matrix.org"]'
|
||||
- name: CONTINUWUITY_ADDRESS
|
||||
value: "0.0.0.0"
|
||||
- name: CONTINUWUITY_ROCKSDB_PARALLELISM_THREADS
|
||||
value: "1"
|
||||
- name: CONTINUWUITY_WELL_KNOWN__SERVER
|
||||
value: "matrix.example.com:443"
|
||||
- name: CONTINUWUITY_WELL_KNOWN__CLIENT
|
||||
value: "https://matrix.example.com"
|
||||
- name: CONTINUWUITY_ALLOW_REGISTRATION
|
||||
value: "false"
|
||||
- name: RUST_LOG
|
||||
value: info
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /_matrix/federation/v1/version
|
||||
port: http
|
||||
periodSeconds: 4
|
||||
failureThreshold: 5
|
||||
resources:
|
||||
# Continuwuity might use quite some RAM :3
|
||||
requests:
|
||||
cpu: "2"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "4"
|
||||
memory: "2048Mi"
|
||||
volumes:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: continuwuity-data
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
Apart from manually configuring the containers,
|
||||
[a community-maintained Helm Chart is available here to run
|
||||
conduwuit on Kubernetes](https://gitlab.cronce.io/charts/conduwuit)
|
||||
|
||||
This should be compatible with Continuwuity, but you will need to change the image reference.
|
||||
|
||||
@@ -1,13 +1,28 @@
|
||||
# Troubleshooting Continuwuity
|
||||
|
||||
> **Docker users ⚠️**
|
||||
>
|
||||
> Docker can be difficult to use and debug. It's common for Docker
|
||||
> misconfigurations to cause issues, particularly with networking and permissions.
|
||||
> Please check that your issues are not due to problems with your Docker setup.
|
||||
:::warning{title="Docker users:"}
|
||||
Docker can be difficult to use and debug. It's common for Docker
|
||||
misconfigurations to cause issues, particularly with networking and permissions.
|
||||
Please check that your issues are not due to problems with your Docker setup.
|
||||
:::
|
||||
|
||||
## Continuwuity and Matrix issues
|
||||
|
||||
### Slow joins to rooms
|
||||
|
||||
Some slowness is to be expected if you're the first person on your homserver to join a room (which will
|
||||
always be the case for single-user homeservers). In this situation, your homeserver has to verify the signatures of
|
||||
all of the state events sent by other servers before your join. To make this process as fast as possible, make sure you have
|
||||
multiple fast, trusted servers listed in `trusted_servers` in your configuration, and ensure
|
||||
`query_trusted_key_servers_first_on_join` is set to true (the default).
|
||||
If you need suggestions for trusted servers, ask in the Continuwuity main room.
|
||||
|
||||
However, _very_ slow joins, especially to rooms with only a few users in them or rooms created by another user
|
||||
on your homeserver, may be caused by [issue !779](https://forgejo.ellis.link/continuwuation/continuwuity/issues/779),
|
||||
which is a longstanding bug with synchronizing room joins to clients. In this situation, you did succeed in joining the room, but
|
||||
the bug caused your homeserver to forget to tell your client. **To fix this, clear your client's cache.** Both Element and Cinny
|
||||
have a button to clear their cache in the "About" section of their settings.
|
||||
|
||||
### Lost access to admin room
|
||||
|
||||
You can reinvite yourself to the admin room through the following methods:
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
# Setting up TURN/STURN
|
||||
|
||||
In order to make or receive calls, a TURN server is required. Continuwuity suggests
|
||||
using [Coturn](https://github.com/coturn/coturn) for this purpose, which is also
|
||||
available as a Docker image.
|
||||
|
||||
### Configuration
|
||||
|
||||
Create a configuration file called `coturn.conf` containing:
|
||||
|
||||
```
|
||||
use-auth-secret
|
||||
static-auth-secret=<a secret key>
|
||||
realm=<your server domain>
|
||||
```
|
||||
|
||||
A common way to generate a suitable alphanumeric secret key is by using `pwgen
|
||||
-s 64 1`.
|
||||
|
||||
These same values need to be set in Continuwuity. See the [example
|
||||
config](./reference/config.mdx) in the TURN section for configuring these and
|
||||
restart Continuwuity after.
|
||||
|
||||
`turn_secret` or a path to `turn_secret_file` must have a value of your
|
||||
coturn `static-auth-secret`, or use `turn_username` and `turn_password`
|
||||
if using legacy username:password TURN authentication (not preferred).
|
||||
|
||||
`turn_uris` must be the list of TURN URIs you would like to send to the client.
|
||||
Typically you will just replace the example domain `example.turn.uri` with the
|
||||
`realm` you set from the example config.
|
||||
|
||||
If you are using TURN over TLS, you can replace `turn:` with `turns:` in the
|
||||
`turn_uris` config option to instruct clients to attempt to connect to
|
||||
TURN over TLS. This is highly recommended.
|
||||
|
||||
If you need unauthenticated access to the TURN URIs, or some clients may be
|
||||
having trouble, you can enable `turn_guest_access` in Continuwuity which disables
|
||||
authentication for the TURN URI endpoint `/_matrix/client/v3/voip/turnServer`
|
||||
|
||||
### Run
|
||||
|
||||
Run the [Coturn](https://hub.docker.com/r/coturn/coturn) image using
|
||||
|
||||
```bash
|
||||
docker run -d --network=host -v
|
||||
$(pwd)/coturn.conf:/etc/coturn/turnserver.conf coturn/coturn
|
||||
```
|
||||
|
||||
or docker-compose. For the latter, paste the following section into a file
|
||||
called `docker-compose.yml` and run `docker compose up -d` in the same
|
||||
directory.
|
||||
|
||||
```yml
|
||||
version: 3
|
||||
services:
|
||||
turn:
|
||||
container_name: coturn-server
|
||||
image: docker.io/coturn/coturn
|
||||
restart: unless-stopped
|
||||
network_mode: "host"
|
||||
volumes:
|
||||
- ./coturn.conf:/etc/coturn/turnserver.conf
|
||||
```
|
||||
|
||||
To understand why the host networking mode is used and explore alternative
|
||||
configuration options, please visit [Coturn's Docker
|
||||
documentation](https://github.com/coturn/coturn/blob/master/docker/coturn/README.md).
|
||||
|
||||
For security recommendations see Synapse's [Coturn
|
||||
documentation](https://element-hq.github.io/synapse/latest/turn-howto.html).
|
||||
|
||||
### Testing
|
||||
|
||||
To make sure turn credentials are being correctly served to clients, you can manually make a HTTP request to the turnServer endpoint.
|
||||
|
||||
`curl "https://<matrix.example.com>/_matrix/client/r0/voip/turnServer" -H 'Authorization: Bearer <your_client_token>' | jq`
|
||||
|
||||
You should get a response like this:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "1752792167:@jade:example.com",
|
||||
"password": "KjlDlawdPbU9mvP4bhdV/2c/h65=",
|
||||
"uris": [
|
||||
"turns:coturn.example.com?transport=udp",
|
||||
"turns:coturn.example.com?transport=tcp",
|
||||
"turn:coturn.example.com?transport=udp",
|
||||
"turn:coturn.example.com?transport=tcp"
|
||||
],
|
||||
"ttl": 86400
|
||||
}
|
||||
```
|
||||
|
||||
You can test these credentials work using [Trickle ICE](https://webrtc.github.io/samples/src/content/peerconnection/trickle-ice/)
|
||||
@@ -20,7 +20,7 @@ rec {
|
||||
# we need to keep the `web` directory which would be filtered out by the regular source filtering function
|
||||
#
|
||||
# https://crane.dev/API.html#cranelibcleancargosource
|
||||
isWebTemplate = path: _type: builtins.match ".*src/web.*" path != null;
|
||||
isWebTemplate = path: _type: builtins.match ".*(src/(web|service)|docs).*" path != null;
|
||||
isRust = craneLib.filterCargoSources;
|
||||
isNix = path: _type: builtins.match ".+/nix.*" path != null;
|
||||
webOrRustNotNix = p: t: !(isNix p t) && (isWebTemplate p t || isRust p t);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": ["config:recommended", "replacements:all"],
|
||||
"dependencyDashboard": true,
|
||||
"osvVulnerabilityAlerts": true,
|
||||
"lockFileMaintenance": {
|
||||
"enabled": true,
|
||||
@@ -57,12 +58,25 @@
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"groupName": "github-actions-non-major"
|
||||
},
|
||||
{
|
||||
"description": "Batch patch-level Node.js dependency updates",
|
||||
"matchManagers": ["npm"],
|
||||
"matchUpdateTypes": ["patch"],
|
||||
"groupName": "node-patch-updates"
|
||||
},
|
||||
{
|
||||
"description": "Pin forgejo artifact actions to prevent breaking changes",
|
||||
"matchManagers": ["github-actions"],
|
||||
"matchPackageNames": ["forgejo/upload-artifact", "forgejo/download-artifact"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"description": "Auto-merge crate-ci/typos minor updates",
|
||||
"matchPackageNames": ["crate-ci/typos"],
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"automerge": true,
|
||||
"automergeStrategy": "fast-forward"
|
||||
},
|
||||
{
|
||||
"description": "Auto-merge renovatebot docker image updates",
|
||||
"matchDatasources": ["docker"],
|
||||
|
||||
@@ -56,6 +56,9 @@ export default defineConfig({
|
||||
}, {
|
||||
from: '/community$',
|
||||
to: '/community/guidelines'
|
||||
}, {
|
||||
from: "^/turn",
|
||||
to: "/calls/turn",
|
||||
}
|
||||
]
|
||||
})],
|
||||
|
||||
@@ -89,13 +89,7 @@ async fn ban_room(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||
locally, if not using get_alias_helper to fetch room ID remotely"
|
||||
);
|
||||
|
||||
match self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(room_alias, None)
|
||||
.await
|
||||
{
|
||||
match self.services.rooms.alias.resolve_alias(room_alias).await {
|
||||
| Ok((room_id, servers)) => {
|
||||
debug!(
|
||||
%room_id,
|
||||
@@ -235,7 +229,7 @@ async fn ban_list_of_rooms(&self) -> Result {
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(room_alias, None)
|
||||
.resolve_alias(room_alias)
|
||||
.await
|
||||
{
|
||||
| Ok((room_id, servers)) => {
|
||||
@@ -388,13 +382,7 @@ async fn unban_room(&self, room: OwnedRoomOrAliasId) -> Result {
|
||||
room ID over federation"
|
||||
);
|
||||
|
||||
match self
|
||||
.services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(room_alias, None)
|
||||
.await
|
||||
{
|
||||
match self.services.rooms.alias.resolve_alias(room_alias).await {
|
||||
| Ok((room_id, servers)) => {
|
||||
debug!(
|
||||
%room_id,
|
||||
|
||||
@@ -86,7 +86,7 @@ pub(super) async fn list_backups(&self) -> Result {
|
||||
.db
|
||||
.backup_list()?
|
||||
.try_stream()
|
||||
.try_for_each(|result| write!(self, "{result}"))
|
||||
.try_for_each(|result| writeln!(self, "{result}"))
|
||||
.await
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@ gzip_compression = [
|
||||
"conduwuit-service/gzip_compression",
|
||||
"reqwest/gzip",
|
||||
]
|
||||
http3 = [
|
||||
"conduwuit-core/http3",
|
||||
"conduwuit-service/http3",
|
||||
]
|
||||
io_uring = [
|
||||
"conduwuit-service/io_uring",
|
||||
]
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result, debug};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use rand::seq::SliceRandom;
|
||||
use ruma::{
|
||||
OwnedServerName, RoomAliasId, RoomId,
|
||||
api::client::alias::{create_alias, delete_alias, get_alias},
|
||||
};
|
||||
use conduwuit::{Err, Result};
|
||||
use ruma::api::client::alias::{create_alias, delete_alias, get_alias};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -96,65 +90,9 @@ pub(crate) async fn get_alias_route(
|
||||
) -> Result<get_alias::v3::Response> {
|
||||
let room_alias = body.body.room_alias;
|
||||
|
||||
let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias, None).await
|
||||
else {
|
||||
let Ok((room_id, servers)) = services.rooms.alias.resolve_alias(&room_alias).await else {
|
||||
return Err!(Request(NotFound("Room with alias not found.")));
|
||||
};
|
||||
|
||||
let servers = room_available_servers(&services, &room_id, &room_alias, servers).await;
|
||||
debug!(%room_alias, %room_id, "available servers: {servers:?}");
|
||||
|
||||
Ok(get_alias::v3::Response::new(room_id, servers))
|
||||
}
|
||||
|
||||
async fn room_available_servers(
|
||||
services: &Services,
|
||||
room_id: &RoomId,
|
||||
room_alias: &RoomAliasId,
|
||||
pre_servers: Vec<OwnedServerName>,
|
||||
) -> Vec<OwnedServerName> {
|
||||
// find active servers in room state cache to suggest
|
||||
let mut servers: Vec<OwnedServerName> = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.room_servers(room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
// push any servers we want in the list already (e.g. responded remote alias
|
||||
// servers, room alias server itself)
|
||||
servers.extend(pre_servers);
|
||||
|
||||
servers.sort_unstable();
|
||||
servers.dedup();
|
||||
|
||||
// shuffle list of servers randomly after sort and dedupe
|
||||
servers.shuffle(&mut rand::thread_rng());
|
||||
|
||||
// insert our server as the very first choice if in list, else check if we can
|
||||
// prefer the room alias server first
|
||||
match servers
|
||||
.iter()
|
||||
.position(|server_name| services.globals.server_is_ours(server_name))
|
||||
{
|
||||
| Some(server_index) => {
|
||||
servers.swap_remove(server_index);
|
||||
servers.insert(0, services.globals.server_name().to_owned());
|
||||
},
|
||||
| _ => {
|
||||
match servers
|
||||
.iter()
|
||||
.position(|server| server == room_alias.server_name())
|
||||
{
|
||||
| Some(alias_server_index) => {
|
||||
servers.swap_remove(alias_server_index);
|
||||
servers.insert(0, room_alias.server_name().into());
|
||||
},
|
||||
| _ => {},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
servers
|
||||
}
|
||||
|
||||
@@ -198,11 +198,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
|
||||
(servers, room_id)
|
||||
},
|
||||
| Err(room_alias) => {
|
||||
let (room_id, mut servers) = services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&room_alias, Some(body.via.clone()))
|
||||
.await?;
|
||||
let (room_id, mut servers) = services.rooms.alias.resolve_alias(&room_alias).await?;
|
||||
|
||||
banned_room_check(
|
||||
&services,
|
||||
|
||||
@@ -102,11 +102,7 @@ pub(crate) async fn knock_room_route(
|
||||
(servers, room_id)
|
||||
},
|
||||
| Err(room_alias) => {
|
||||
let (room_id, mut servers) = services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&room_alias, Some(body.via.clone()))
|
||||
.await?;
|
||||
let (room_id, mut servers) = services.rooms.alias.resolve_alias(&room_alias).await?;
|
||||
|
||||
banned_room_check(
|
||||
&services,
|
||||
|
||||
@@ -342,10 +342,10 @@ async fn allowed_to_send_state_event(
|
||||
}
|
||||
|
||||
for alias in aliases {
|
||||
let (alias_room_id, _servers) = services
|
||||
let (alias_room_id, _) = services
|
||||
.rooms
|
||||
.alias
|
||||
.resolve_alias(&alias, None)
|
||||
.resolve_alias(&alias)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
err!(Request(Unknown("Failed resolving alias \"{alias}\": {e}")))
|
||||
|
||||
@@ -122,23 +122,23 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
// Ruma doesn't have support for multiple paths for a single endpoint yet, and these routes
|
||||
// share one Ruma request / response type pair with {get,send}_state_event_for_key_route
|
||||
.route(
|
||||
"/_matrix/client/r0/rooms/:room_id/state/:event_type",
|
||||
"/_matrix/client/r0/rooms/{room_id}/state/{event_type}",
|
||||
get(client::get_state_events_for_empty_key_route)
|
||||
.put(client::send_state_event_for_empty_key_route),
|
||||
)
|
||||
.route(
|
||||
"/_matrix/client/v3/rooms/:room_id/state/:event_type",
|
||||
"/_matrix/client/v3/rooms/{room_id}/state/{event_type}",
|
||||
get(client::get_state_events_for_empty_key_route)
|
||||
.put(client::send_state_event_for_empty_key_route),
|
||||
)
|
||||
// These two endpoints allow trailing slashes
|
||||
.route(
|
||||
"/_matrix/client/r0/rooms/:room_id/state/:event_type/",
|
||||
"/_matrix/client/r0/rooms/{room_id}/state/{event_type}/",
|
||||
get(client::get_state_events_for_empty_key_route)
|
||||
.put(client::send_state_event_for_empty_key_route),
|
||||
)
|
||||
.route(
|
||||
"/_matrix/client/v3/rooms/:room_id/state/:event_type/",
|
||||
"/_matrix/client/v3/rooms/{room_id}/state/{event_type}/",
|
||||
get(client::get_state_events_for_empty_key_route)
|
||||
.put(client::send_state_event_for_empty_key_route),
|
||||
)
|
||||
@@ -177,7 +177,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&client::get_mutual_rooms_route)
|
||||
.ruma_route(&client::get_room_summary)
|
||||
.route(
|
||||
"/_matrix/client/unstable/im.nheko.summary/rooms/:room_id_or_alias/summary",
|
||||
"/_matrix/client/unstable/im.nheko.summary/rooms/{room_id_or_alias}/summary",
|
||||
get(client::get_room_summary_legacy)
|
||||
)
|
||||
.ruma_route(&client::get_suspended_status)
|
||||
@@ -196,7 +196,7 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.ruma_route(&server::get_server_version_route)
|
||||
.route("/_matrix/key/v2/server", get(server::get_server_keys_route))
|
||||
.route(
|
||||
"/_matrix/key/v2/server/:key_id",
|
||||
"/_matrix/key/v2/server/{key_id}",
|
||||
get(server::get_server_keys_deprecated_route),
|
||||
)
|
||||
.ruma_route(&server::get_public_rooms_route)
|
||||
@@ -232,9 +232,9 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
.route("/_continuwuity/local_user_count", get(client::conduwuit_local_user_count));
|
||||
} else {
|
||||
router = router
|
||||
.route("/_matrix/federation/*path", any(federation_disabled))
|
||||
.route("/_matrix/federation/{*path}", any(federation_disabled))
|
||||
.route("/.well-known/matrix/server", any(federation_disabled))
|
||||
.route("/_matrix/key/*path", any(federation_disabled))
|
||||
.route("/_matrix/key/{*path}", any(federation_disabled))
|
||||
.route("/_conduwuit/local_user_count", any(federation_disabled))
|
||||
.route("/_continuwuity/local_user_count", any(federation_disabled));
|
||||
}
|
||||
@@ -253,27 +253,27 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
|
||||
get(client::get_media_preview_legacy_legacy_route),
|
||||
)
|
||||
.route(
|
||||
"/_matrix/media/v1/download/:server_name/:media_id",
|
||||
"/_matrix/media/v1/download/{server_name}/{media_id}",
|
||||
get(client::get_content_legacy_legacy_route),
|
||||
)
|
||||
.route(
|
||||
"/_matrix/media/v1/download/:server_name/:media_id/:file_name",
|
||||
"/_matrix/media/v1/download/{server_name}/{media_id}/{file_name}",
|
||||
get(client::get_content_as_filename_legacy_legacy_route),
|
||||
)
|
||||
.route(
|
||||
"/_matrix/media/v1/thumbnail/:server_name/:media_id",
|
||||
"/_matrix/media/v1/thumbnail/{server_name}/{media_id}",
|
||||
get(client::get_content_thumbnail_legacy_legacy_route),
|
||||
);
|
||||
} else {
|
||||
router = router
|
||||
.route("/_matrix/media/v1/*path", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v1/{*path}", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/config", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/download/*path", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/thumbnail/*path", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/download/{*path}", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/thumbnail/{*path}", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/v3/preview_url", any(redirect_legacy_preview))
|
||||
.route("/_matrix/media/r0/config", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/r0/download/*path", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/r0/thumbnail/*path", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/r0/download/{*path}", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/r0/thumbnail/{*path}", any(legacy_media_disabled))
|
||||
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use std::{mem, ops::Deref};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{body::Body, extract::FromRequest};
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use conduwuit::{Error, Result, debug, debug_warn, err, trace, utils::string::EMPTY};
|
||||
@@ -79,7 +78,6 @@ impl<T> Deref for Args<T>
|
||||
fn deref(&self) -> &Self::Target { &self.body }
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<T> FromRequest<State, Body> for Args<T>
|
||||
where
|
||||
T: IncomingRequest + Send + Sync + 'static,
|
||||
|
||||
@@ -54,7 +54,8 @@ pub(super) async fn auth(
|
||||
json_body: Option<&CanonicalJsonValue>,
|
||||
metadata: &Metadata,
|
||||
) -> Result<Auth> {
|
||||
let bearer: Option<TypedHeader<Authorization<Bearer>>> = request.parts.extract().await?;
|
||||
let bearer: Option<TypedHeader<Authorization<Bearer>>> =
|
||||
request.parts.extract().await.unwrap_or(None);
|
||||
let token = match &bearer {
|
||||
| Some(TypedHeader(Authorization(bearer))) => Some(bearer.token()),
|
||||
| None => request.query.access_token.as_deref(),
|
||||
|
||||
@@ -24,6 +24,9 @@ conduwuit_mods = [
|
||||
gzip_compression = [
|
||||
"reqwest/gzip",
|
||||
]
|
||||
http3 = [
|
||||
"reqwest/http3",
|
||||
]
|
||||
hardened_malloc = [
|
||||
"dep:hardened_malloc-rs"
|
||||
]
|
||||
|
||||
@@ -1244,12 +1244,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
pub rocksdb_repair: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub rocksdb_read_only: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub rocksdb_secondary: bool,
|
||||
|
||||
/// Enables idle CPU priority for compaction thread. This is not enabled by
|
||||
/// default to prevent compaction from falling too far behind on busy
|
||||
/// systems.
|
||||
@@ -1309,26 +1303,33 @@ pub struct Config {
|
||||
|
||||
/// Allow local (your server only) presence updates/requests.
|
||||
///
|
||||
/// Note that presence on continuwuity is very fast unlike Synapse's. If
|
||||
/// using outgoing presence, this MUST be enabled.
|
||||
/// Local presence must be enabled for outgoing presence to function.
|
||||
///
|
||||
/// Note that local presence is not as heavy on the CPU as federated
|
||||
/// presence, but will still become more expensive the more local users you
|
||||
/// have.
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_local_presence: bool,
|
||||
|
||||
/// Allow incoming federated presence updates/requests.
|
||||
/// Allow incoming federated presence updates.
|
||||
///
|
||||
/// This option receives presence updates from other servers, but does not
|
||||
/// send any unless `allow_outgoing_presence` is true. Note that presence on
|
||||
/// continuwuity is very fast unlike Synapse's.
|
||||
/// This option enables processing inbound presence updates from other
|
||||
/// servers. Without it, remote users will appear as if they are always
|
||||
/// offline to your local users. This does not affect typing indicators or
|
||||
/// read receipts.
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_incoming_presence: bool,
|
||||
|
||||
/// Allow outgoing presence updates/requests.
|
||||
///
|
||||
/// This option sends presence updates to other servers, but does not
|
||||
/// receive any unless `allow_incoming_presence` is true. Note that presence
|
||||
/// on continuwuity is very fast unlike Synapse's. If using outgoing
|
||||
/// presence, you MUST enable `allow_local_presence` as well.
|
||||
#[serde(default = "true_fn")]
|
||||
/// This option sends presence updates to other servers, and requires that
|
||||
/// `allow_local_presence` is also enabled.
|
||||
///
|
||||
/// Note that outgoing presence is very heavy on the CPU and network, and
|
||||
/// will typically cause extreme strain and slowdowns for no real benefit.
|
||||
/// There are only a few clients that even implement presence, so you
|
||||
/// probably don't want to enable this.
|
||||
#[serde(default)]
|
||||
pub allow_outgoing_presence: bool,
|
||||
|
||||
/// How many seconds without presence updates before you become idle.
|
||||
@@ -1366,6 +1367,10 @@ pub struct Config {
|
||||
pub allow_incoming_read_receipts: bool,
|
||||
|
||||
/// Allow sending read receipts to remote servers.
|
||||
///
|
||||
/// Note that sending read receipts to remote servers in large rooms with
|
||||
/// lots of other homeservers may cause additional strain on the CPU and
|
||||
/// network.
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_outgoing_read_receipts: bool,
|
||||
|
||||
@@ -1377,6 +1382,10 @@ pub struct Config {
|
||||
pub allow_local_typing: bool,
|
||||
|
||||
/// Allow outgoing typing updates to federation.
|
||||
///
|
||||
/// Note that sending typing indicators to remote servers in large rooms
|
||||
/// with lots of other homeservers may cause additional strain on the CPU
|
||||
/// and network.
|
||||
#[serde(default = "true_fn")]
|
||||
pub allow_outgoing_typing: bool,
|
||||
|
||||
|
||||
@@ -1,552 +0,0 @@
|
||||
#[cfg(conduwuit_bench)]
|
||||
extern crate test;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
collections::{HashMap, HashSet},
|
||||
sync::atomic::{AtomicU64, Ordering::SeqCst},
|
||||
};
|
||||
|
||||
use futures::{future, future::ready};
|
||||
use maplit::{btreemap, hashmap, hashset};
|
||||
use ruma::{
|
||||
EventId, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, RoomVersionId, Signatures, UserId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
},
|
||||
int, room_id, uint, user_id,
|
||||
};
|
||||
use serde_json::{
|
||||
json,
|
||||
value::{RawValue as RawJsonValue, to_raw_value as to_raw_json_value},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
matrix::{Event, Pdu, pdu::EventHash},
|
||||
state_res::{self as state_res, Error, Result, StateMap},
|
||||
};
|
||||
|
||||
static SERVER_TIMESTAMP: AtomicU64 = AtomicU64::new(0);
|
||||
|
||||
#[cfg(conduwuit_bench)]
|
||||
#[cfg_attr(conduwuit_bench, bench)]
|
||||
fn lexico_topo_sort(c: &mut test::Bencher) {
|
||||
let graph = hashmap! {
|
||||
event_id("l") => hashset![event_id("o")],
|
||||
event_id("m") => hashset![event_id("n"), event_id("o")],
|
||||
event_id("n") => hashset![event_id("o")],
|
||||
event_id("o") => hashset![], // "o" has zero outgoing edges but 4 incoming edges
|
||||
event_id("p") => hashset![event_id("o")],
|
||||
};
|
||||
|
||||
c.iter(|| {
|
||||
let _ = state_res::lexicographical_topological_sort(&graph, &|_| {
|
||||
future::ok((int!(0), MilliSecondsSinceUnixEpoch(uint!(0))))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(conduwuit_bench)]
|
||||
#[cfg_attr(conduwuit_bench, bench)]
|
||||
fn resolution_shallow_auth_chain(c: &mut test::Bencher) {
|
||||
let mut store = TestStore(hashmap! {});
|
||||
|
||||
// build up the DAG
|
||||
let (state_at_bob, state_at_charlie, _) = store.set_up();
|
||||
|
||||
c.iter(|| async {
|
||||
let ev_map = store.0.clone();
|
||||
let state_sets = [&state_at_bob, &state_at_charlie];
|
||||
let fetch = |id: OwnedEventId| ready(ev_map.get(&id).map(ToOwned::to_owned));
|
||||
let exists = |id: OwnedEventId| ready(ev_map.get(&id).is_some());
|
||||
let auth_chain_sets: Vec<HashSet<_>> = state_sets
|
||||
.iter()
|
||||
.map(|map| {
|
||||
store
|
||||
.auth_event_ids(room_id(), map.values().cloned().collect())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let _ = match state_res::resolve(
|
||||
&RoomVersionId::V6,
|
||||
state_sets.into_iter(),
|
||||
&auth_chain_sets,
|
||||
&fetch,
|
||||
&exists,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(e) => panic!("{e}"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(conduwuit_bench)]
|
||||
#[cfg_attr(conduwuit_bench, bench)]
|
||||
fn resolve_deeper_event_set(c: &mut test::Bencher) {
|
||||
let mut inner = INITIAL_EVENTS();
|
||||
let ban = BAN_STATE_SET();
|
||||
|
||||
inner.extend(ban);
|
||||
let store = TestStore(inner.clone());
|
||||
|
||||
let state_set_a = [
|
||||
inner.get(&event_id("CREATE")).unwrap(),
|
||||
inner.get(&event_id("IJR")).unwrap(),
|
||||
inner.get(&event_id("IMA")).unwrap(),
|
||||
inner.get(&event_id("IMB")).unwrap(),
|
||||
inner.get(&event_id("IMC")).unwrap(),
|
||||
inner.get(&event_id("MB")).unwrap(),
|
||||
inner.get(&event_id("PA")).unwrap(),
|
||||
]
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let state_set_b = [
|
||||
inner.get(&event_id("CREATE")).unwrap(),
|
||||
inner.get(&event_id("IJR")).unwrap(),
|
||||
inner.get(&event_id("IMA")).unwrap(),
|
||||
inner.get(&event_id("IMB")).unwrap(),
|
||||
inner.get(&event_id("IMC")).unwrap(),
|
||||
inner.get(&event_id("IME")).unwrap(),
|
||||
inner.get(&event_id("PA")).unwrap(),
|
||||
]
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
c.iter(|| async {
|
||||
let state_sets = [&state_set_a, &state_set_b];
|
||||
let auth_chain_sets: Vec<HashSet<_>> = state_sets
|
||||
.iter()
|
||||
.map(|map| {
|
||||
store
|
||||
.auth_event_ids(room_id(), map.values().cloned().collect())
|
||||
.unwrap()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let fetch = |id: OwnedEventId| ready(inner.get(&id).map(ToOwned::to_owned));
|
||||
let exists = |id: OwnedEventId| ready(inner.get(&id).is_some());
|
||||
let _ = match state_res::resolve(
|
||||
&RoomVersionId::V6,
|
||||
state_sets.into_iter(),
|
||||
&auth_chain_sets,
|
||||
&fetch,
|
||||
&exists,
|
||||
)
|
||||
.await
|
||||
{
|
||||
| Ok(state) => state,
|
||||
| Err(_) => panic!("resolution failed during benchmarking"),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
//*/////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// IMPLEMENTATION DETAILS AHEAD
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////*/
|
||||
struct TestStore<E: Event>(HashMap<OwnedEventId, E>);
|
||||
|
||||
#[allow(unused)]
|
||||
impl<E: Event + Clone> TestStore<E> {
|
||||
fn get_event(&self, room_id: &RoomId, event_id: &EventId) -> Result<E> {
|
||||
self.0
|
||||
.get(event_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| Error::NotFound(format!("{} not found", event_id)))
|
||||
}
|
||||
|
||||
/// Returns the events that correspond to the `event_ids` sorted in the same
|
||||
/// order.
|
||||
fn get_events(&self, room_id: &RoomId, event_ids: &[OwnedEventId]) -> Result<Vec<E>> {
|
||||
let mut events = vec![];
|
||||
for id in event_ids {
|
||||
events.push(self.get_event(room_id, id)?);
|
||||
}
|
||||
Ok(events)
|
||||
}
|
||||
|
||||
/// Returns a Vec of the related auth events to the given `event`.
|
||||
fn auth_event_ids(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_ids: Vec<OwnedEventId>,
|
||||
) -> Result<HashSet<OwnedEventId>> {
|
||||
let mut result = HashSet::new();
|
||||
let mut stack = event_ids;
|
||||
|
||||
// DFS for auth event chain
|
||||
while !stack.is_empty() {
|
||||
let ev_id = stack.pop().unwrap();
|
||||
if result.contains(&ev_id) {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.insert(ev_id.clone());
|
||||
|
||||
let event = self.get_event(room_id, ev_id.borrow())?;
|
||||
|
||||
stack.extend(event.auth_events().map(ToOwned::to_owned));
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Returns a vector representing the difference in auth chains of the given
|
||||
/// `events`.
|
||||
fn auth_chain_diff(
|
||||
&self,
|
||||
room_id: &RoomId,
|
||||
event_ids: Vec<Vec<OwnedEventId>>,
|
||||
) -> Result<Vec<OwnedEventId>> {
|
||||
let mut auth_chain_sets = vec![];
|
||||
for ids in event_ids {
|
||||
// TODO state store `auth_event_ids` returns self in the event ids list
|
||||
// when an event returns `auth_event_ids` self is not contained
|
||||
let chain = self
|
||||
.auth_event_ids(room_id, ids)?
|
||||
.into_iter()
|
||||
.collect::<HashSet<_>>();
|
||||
auth_chain_sets.push(chain);
|
||||
}
|
||||
|
||||
if let Some(first) = auth_chain_sets.first().cloned() {
|
||||
let common = auth_chain_sets
|
||||
.iter()
|
||||
.skip(1)
|
||||
.fold(first, |a, b| a.intersection(b).cloned().collect::<HashSet<_>>());
|
||||
|
||||
Ok(auth_chain_sets
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter(|id| !common.contains(id))
|
||||
.collect())
|
||||
} else {
|
||||
Ok(vec![])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestStore<Pdu> {
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn set_up(
|
||||
&mut self,
|
||||
) -> (StateMap<OwnedEventId>, StateMap<OwnedEventId>, StateMap<OwnedEventId>) {
|
||||
let create_event = to_pdu_event::<&EventId>(
|
||||
"CREATE",
|
||||
alice(),
|
||||
TimelineEventType::RoomCreate,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
|
||||
&[],
|
||||
&[],
|
||||
);
|
||||
let cre = create_event.event_id().to_owned();
|
||||
self.0.insert(cre.clone(), create_event.clone());
|
||||
|
||||
let alice_mem = to_pdu_event(
|
||||
"IMA",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().to_string().as_str()),
|
||||
member_content_join(),
|
||||
&[cre.clone()],
|
||||
&[cre.clone()],
|
||||
);
|
||||
self.0
|
||||
.insert(alice_mem.event_id().to_owned(), alice_mem.clone());
|
||||
|
||||
let join_rules = to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
|
||||
&[cre.clone(), alice_mem.event_id().to_owned()],
|
||||
&[alice_mem.event_id().to_owned()],
|
||||
);
|
||||
self.0
|
||||
.insert(join_rules.event_id().to_owned(), join_rules.clone());
|
||||
|
||||
// Bob and Charlie join at the same time, so there is a fork
|
||||
// this will be represented in the state_sets when we resolve
|
||||
let bob_mem = to_pdu_event(
|
||||
"IMB",
|
||||
bob(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(bob().to_string().as_str()),
|
||||
member_content_join(),
|
||||
&[cre.clone(), join_rules.event_id().to_owned()],
|
||||
&[join_rules.event_id().to_owned()],
|
||||
);
|
||||
self.0
|
||||
.insert(bob_mem.event_id().to_owned(), bob_mem.clone());
|
||||
|
||||
let charlie_mem = to_pdu_event(
|
||||
"IMC",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().to_string().as_str()),
|
||||
member_content_join(),
|
||||
&[cre, join_rules.event_id().to_owned()],
|
||||
&[join_rules.event_id().to_owned()],
|
||||
);
|
||||
self.0
|
||||
.insert(charlie_mem.event_id().to_owned(), charlie_mem.clone());
|
||||
|
||||
let state_at_bob = [&create_event, &alice_mem, &join_rules, &bob_mem]
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let state_at_charlie = [&create_event, &alice_mem, &join_rules, &charlie_mem]
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let expected = [&create_event, &alice_mem, &join_rules, &bob_mem, &charlie_mem]
|
||||
.iter()
|
||||
.map(|ev| {
|
||||
(
|
||||
(ev.event_type().clone().into(), ev.state_key().unwrap().into()),
|
||||
ev.event_id().to_owned(),
|
||||
)
|
||||
})
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
(state_at_bob, state_at_charlie, expected)
|
||||
}
|
||||
}
|
||||
|
||||
fn event_id(id: &str) -> OwnedEventId {
|
||||
if id.contains('$') {
|
||||
return id.try_into().unwrap();
|
||||
}
|
||||
format!("${}:foo", id).try_into().unwrap()
|
||||
}
|
||||
|
||||
fn alice() -> &'static UserId { user_id!("@alice:foo") }
|
||||
|
||||
fn bob() -> &'static UserId { user_id!("@bob:foo") }
|
||||
|
||||
fn charlie() -> &'static UserId { user_id!("@charlie:foo") }
|
||||
|
||||
fn ella() -> &'static UserId { user_id!("@ella:foo") }
|
||||
|
||||
fn room_id() -> &'static RoomId { room_id!("!test:foo") }
|
||||
|
||||
fn member_content_ban() -> Box<RawJsonValue> {
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Ban)).unwrap()
|
||||
}
|
||||
|
||||
fn member_content_join() -> Box<RawJsonValue> {
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap()
|
||||
}
|
||||
|
||||
fn to_pdu_event<S>(
|
||||
id: &str,
|
||||
sender: &UserId,
|
||||
ev_type: TimelineEventType,
|
||||
state_key: Option<&str>,
|
||||
content: Box<RawJsonValue>,
|
||||
auth_events: &[S],
|
||||
prev_events: &[S],
|
||||
) -> Pdu
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
// We don't care if the addition happens in order just that it is atomic
|
||||
// (each event has its own value)
|
||||
let ts = SERVER_TIMESTAMP.fetch_add(1, SeqCst);
|
||||
let id = if id.contains('$') {
|
||||
id.to_owned()
|
||||
} else {
|
||||
format!("${}:foo", id)
|
||||
};
|
||||
let auth_events = auth_events
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.map(event_id)
|
||||
.collect::<Vec<_>>();
|
||||
let prev_events = prev_events
|
||||
.iter()
|
||||
.map(AsRef::as_ref)
|
||||
.map(event_id)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Pdu {
|
||||
event_id: id.try_into().unwrap(),
|
||||
room_id: Some(room_id().to_owned()),
|
||||
sender: sender.to_owned(),
|
||||
origin_server_ts: ts.try_into().unwrap(),
|
||||
state_key: state_key.map(Into::into),
|
||||
kind: ev_type,
|
||||
content,
|
||||
origin: None,
|
||||
redacts: None,
|
||||
unsigned: None,
|
||||
auth_events,
|
||||
prev_events,
|
||||
depth: uint!(0),
|
||||
hashes: EventHash { sha256: String::new() },
|
||||
signatures: None,
|
||||
}
|
||||
}
|
||||
|
||||
// all graphs start with these input events
|
||||
#[allow(non_snake_case)]
|
||||
fn INITIAL_EVENTS() -> HashMap<OwnedEventId, Pdu> {
|
||||
vec![
|
||||
to_pdu_event::<&EventId>(
|
||||
"CREATE",
|
||||
alice(),
|
||||
TimelineEventType::RoomCreate,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({ "creator": alice() })).unwrap(),
|
||||
&[],
|
||||
&[],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IMA",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE"],
|
||||
&["CREATE"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IPOWER",
|
||||
alice(),
|
||||
TimelineEventType::RoomPowerLevels,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({ "users": { alice(): 100 } })).unwrap(),
|
||||
&["CREATE", "IMA"],
|
||||
&["IMA"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Public)).unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["IPOWER"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IMB",
|
||||
bob(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(bob().to_string().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE", "IJR", "IPOWER"],
|
||||
&["IJR"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IMC",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().to_string().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE", "IJR", "IPOWER"],
|
||||
&["IMB"],
|
||||
),
|
||||
to_pdu_event::<&EventId>(
|
||||
"START",
|
||||
charlie(),
|
||||
TimelineEventType::RoomTopic,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({})).unwrap(),
|
||||
&[],
|
||||
&[],
|
||||
),
|
||||
to_pdu_event::<&EventId>(
|
||||
"END",
|
||||
charlie(),
|
||||
TimelineEventType::RoomTopic,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({})).unwrap(),
|
||||
&[],
|
||||
&[],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|ev| (ev.event_id().to_owned(), ev))
|
||||
.collect()
|
||||
}
|
||||
|
||||
// all graphs start with these input events
|
||||
#[allow(non_snake_case)]
|
||||
fn BAN_STATE_SET() -> HashMap<OwnedEventId, Pdu> {
|
||||
vec![
|
||||
to_pdu_event(
|
||||
"PA",
|
||||
alice(),
|
||||
TimelineEventType::RoomPowerLevels,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"], // auth_events
|
||||
&["START"], // prev_events
|
||||
),
|
||||
to_pdu_event(
|
||||
"PB",
|
||||
alice(),
|
||||
TimelineEventType::RoomPowerLevels,
|
||||
Some(""),
|
||||
to_raw_json_value(&json!({ "users": { alice(): 100, bob(): 50 } })).unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["END"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"MB",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
member_content_ban(),
|
||||
&["CREATE", "IMA", "PB"],
|
||||
&["PA"],
|
||||
),
|
||||
to_pdu_event(
|
||||
"IME",
|
||||
ella(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE", "IJR", "PA"],
|
||||
&["MB"],
|
||||
),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|ev| (ev.event_id().to_owned(), ev))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use ruma::OwnedEventId;
|
||||
use serde_json::Error as JsonError;
|
||||
use thiserror::Error;
|
||||
|
||||
@@ -14,10 +15,28 @@ pub enum Error {
|
||||
Unsupported(String),
|
||||
|
||||
/// The given event was not found.
|
||||
#[error("Not found error: {0}")]
|
||||
#[error("Event not found: {0}")]
|
||||
NotFound(String),
|
||||
|
||||
/// A required event this event depended on could not be fetched,
|
||||
/// either as it was missing, or because it was invalid
|
||||
#[error("Failed to fetch required {0} event: {1}")]
|
||||
DependencyFailed(OwnedEventId, String),
|
||||
|
||||
/// Invalid fields in the given PDU.
|
||||
#[error("Invalid PDU: {0}")]
|
||||
InvalidPdu(String),
|
||||
|
||||
/// This event failed an authorization condition.
|
||||
#[error("Auth check failed: {0}")]
|
||||
AuthConditionFailed(String),
|
||||
|
||||
/// This event contained multiple auth events of the same type and state
|
||||
/// key.
|
||||
#[error("Duplicate auth events: {0}")]
|
||||
DuplicateAuthEvents(String),
|
||||
|
||||
/// This event contains unnecessary auth events.
|
||||
#[error("Unknown or unnecessary auth events present: {0}")]
|
||||
UnselectedAuthEvents(String),
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
238
src/core/matrix/state_res/event_auth/auth_events.rs
Normal file
238
src/core/matrix/state_res/event_auth/auth_events.rs
Normal file
@@ -0,0 +1,238 @@
|
||||
//! Auth checks relevant to any event's `auth_events`.
|
||||
//!
|
||||
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use ruma::{
|
||||
EventId, OwnedEventId, RoomId, UserId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::member::{MembershipState, RoomMemberEventContent, ThirdPartyInvite},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error, warn};
|
||||
|
||||
/// For the given event `kind` what are the relevant auth events that are needed
|
||||
/// to authenticate this `content`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return an error if the supplied `content` is not a JSON
|
||||
/// object.
|
||||
pub fn auth_types_for_event(
|
||||
room_version: &RoomVersion,
|
||||
event_type: &TimelineEventType,
|
||||
state_key: Option<&StateKey>,
|
||||
sender: &UserId,
|
||||
member_content: Option<RoomMemberEventContent>,
|
||||
) -> serde_json::Result<Vec<(StateEventType, StateKey)>> {
|
||||
if event_type == &TimelineEventType::RoomCreate {
|
||||
// Create events never have auth events
|
||||
return Ok(vec![]);
|
||||
}
|
||||
let mut auth_types = if room_version.room_ids_as_hashes {
|
||||
vec![
|
||||
StateEventType::RoomMember.with_state_key(sender.as_str()),
|
||||
StateEventType::RoomPowerLevels.with_state_key(""),
|
||||
]
|
||||
} else {
|
||||
// For room versions that do not use room IDs as hashes, include the
|
||||
// RoomCreate event as an auth event.
|
||||
vec![
|
||||
StateEventType::RoomMember.with_state_key(sender.as_str()),
|
||||
StateEventType::RoomPowerLevels.with_state_key(""),
|
||||
StateEventType::RoomCreate.with_state_key(""),
|
||||
]
|
||||
};
|
||||
|
||||
if event_type == &TimelineEventType::RoomMember {
|
||||
let member_content =
|
||||
member_content.expect("member_content must be provided for RoomMember events");
|
||||
|
||||
// Include the target's membership (if available)
|
||||
auth_types.push((
|
||||
StateEventType::RoomMember,
|
||||
state_key
|
||||
.expect("state_key must be provided for RoomMember events")
|
||||
.to_owned(),
|
||||
));
|
||||
|
||||
if matches!(
|
||||
member_content.membership,
|
||||
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
|
||||
) {
|
||||
// Include the join rules
|
||||
auth_types.push(StateEventType::RoomJoinRules.with_state_key(""));
|
||||
}
|
||||
|
||||
if matches!(member_content.membership, MembershipState::Invite) {
|
||||
// If this is an invite, include the third party invite if it exists
|
||||
if let Some(ThirdPartyInvite { signed, .. }) = member_content.third_party_invite {
|
||||
auth_types
|
||||
.push(StateEventType::RoomThirdPartyInvite.with_state_key(signed.token));
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(member_content.membership, MembershipState::Join)
|
||||
&& room_version.restricted_join_rules
|
||||
{
|
||||
// If this is a restricted join, include the authorizing user's membership
|
||||
if let Some(authorizing_user) = member_content.join_authorized_via_users_server {
|
||||
auth_types
|
||||
.push(StateEventType::RoomMember.with_state_key(authorizing_user.as_str()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(auth_types)
|
||||
}
|
||||
|
||||
/// Checks for duplicate auth events in the `auth_events` field of an event.
|
||||
/// Note: the caller should already have all of the auth events fetched.
|
||||
///
|
||||
/// If there are multiple auth events of the same type and state key, this
|
||||
/// returns an error. Otherwise, it returns a map of (type, state_key) to the
|
||||
/// corresponding auth event.
|
||||
pub async fn check_duplicate_auth_events<FE>(
|
||||
auth_events: &[OwnedEventId],
|
||||
fetch_event: FE,
|
||||
) -> Result<HashMap<(StateEventType, StateKey), Pdu>, Error>
|
||||
where
|
||||
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
let mut seen: HashMap<(StateEventType, StateKey), Pdu> = HashMap::new();
|
||||
|
||||
// Considering all of the event's auth events:
|
||||
for auth_event_id in auth_events {
|
||||
if let Ok(Some(auth_event)) = fetch_event(auth_event_id).await {
|
||||
let event_type = auth_event.kind();
|
||||
// If this is not a state event, reject it.
|
||||
let Some(state_key) = &auth_event.state_key() else {
|
||||
return Err(Error::InvalidPdu(format!(
|
||||
"Auth event {:?} is not a state event",
|
||||
auth_event_id
|
||||
)));
|
||||
};
|
||||
let type_key_pair: (StateEventType, StateKey) =
|
||||
event_type.clone().with_state_key(state_key.clone());
|
||||
|
||||
// If there are duplicate entries for a given type and state_key pair, reject.
|
||||
if seen.contains_key(&type_key_pair) {
|
||||
return Err(Error::DuplicateAuthEvents(format!(
|
||||
"({:?},\"{:?}\")",
|
||||
event_type, state_key
|
||||
)));
|
||||
}
|
||||
seen.insert(type_key_pair, auth_event);
|
||||
} else {
|
||||
return Err(Error::NotFound(auth_event_id.as_str().to_owned()));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(seen)
|
||||
}
|
||||
|
||||
// Checks that the event does not refer to any auth events that it does not need
|
||||
// to.
|
||||
pub fn check_unnecessary_auth_events(
|
||||
auth_events: &HashSet<(StateEventType, StateKey)>,
|
||||
expected: &Vec<(StateEventType, StateKey)>,
|
||||
) -> Result<(), Error> {
|
||||
// If there are entries whose type and state_key don't match those specified by
|
||||
// the auth events selection algorithm described in the server specification,
|
||||
// reject.
|
||||
let remaining = auth_events
|
||||
.iter()
|
||||
.filter(|key| !expected.contains(key))
|
||||
.collect::<HashSet<_>>();
|
||||
if !remaining.is_empty() {
|
||||
return Err(Error::UnselectedAuthEvents(format!("{:?}", remaining)));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Checks that all provided auth events were not rejected previously.
|
||||
//
|
||||
// TODO: this is currently a no-op and always returns Ok(()).
|
||||
pub fn check_all_auth_events_accepted(
|
||||
_auth_events: &HashMap<(StateEventType, StateKey), Pdu>,
|
||||
) -> Result<(), Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Checks that all auth events are from the same room as the event being
|
||||
// validated.
|
||||
pub fn check_auth_same_room(auth_events: &Vec<Pdu>, room_id: &RoomId) -> bool {
|
||||
for auth_event in auth_events {
|
||||
if let Some(auth_room_id) = &auth_event.room_id() {
|
||||
if auth_room_id.as_str() != room_id.as_str() {
|
||||
warn!(
|
||||
auth_event_id=%auth_event.event_id(),
|
||||
"Auth event room id {} does not match expected room id {}",
|
||||
auth_room_id,
|
||||
room_id
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
warn!(auth_event_id=%auth_event.event_id(), "Auth event has no room_id");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Performs all auth event checks for the given event.
|
||||
pub async fn check_auth_events<FE>(
|
||||
event: &Pdu,
|
||||
room_id: &RoomId,
|
||||
room_version: &RoomVersion,
|
||||
fetch_event: &FE,
|
||||
) -> Result<HashMap<(StateEventType, StateKey), Pdu>, Error>
|
||||
where
|
||||
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
// If there are duplicate entries for a given type and state_key pair, reject.
|
||||
let auth_events_map = check_duplicate_auth_events(&event.auth_events, fetch_event).await?;
|
||||
let auth_events_set: HashSet<(StateEventType, StateKey)> =
|
||||
auth_events_map.keys().cloned().collect();
|
||||
|
||||
// If there are entries whose type and state_key don’t match those specified by
|
||||
// the auth events selection algorithm described in the server specification,
|
||||
// reject.
|
||||
let member_event_content = match event.kind() {
|
||||
| TimelineEventType::RoomMember =>
|
||||
Some(event.get_content::<RoomMemberEventContent>().map_err(|e| {
|
||||
Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e))
|
||||
})?),
|
||||
| _ => None,
|
||||
};
|
||||
let expected_auth_events = auth_types_for_event(
|
||||
room_version,
|
||||
event.kind(),
|
||||
event.state_key.as_ref(),
|
||||
event.sender(),
|
||||
member_event_content,
|
||||
)?;
|
||||
if let Err(e) = check_unnecessary_auth_events(&auth_events_set, &expected_auth_events) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// If there are entries which were themselves rejected under the checks
|
||||
// performed on receipt of a PDU, reject.
|
||||
if let Err(e) = check_all_auth_events_accepted(&auth_events_map) {
|
||||
return Err(e);
|
||||
}
|
||||
|
||||
// If any event in auth_events has a room_id which does not match that of the
|
||||
// event being authorised, reject.
|
||||
let auth_event_refs: Vec<Pdu> = auth_events_map.values().cloned().collect();
|
||||
if !check_auth_same_room(&auth_event_refs, room_id) {
|
||||
return Err(Error::InvalidPdu(
|
||||
"One or more auth events are from a different room".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(auth_events_map)
|
||||
}
|
||||
113
src/core/matrix/state_res/event_auth/context.rs
Normal file
113
src/core/matrix/state_res/event_auth/context.rs
Normal file
@@ -0,0 +1,113 @@
|
||||
//! Context for event authorisation checks
|
||||
|
||||
use ruma::{
|
||||
Int, OwnedUserId, UserId,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{create::RoomCreateEventContent, power_levels::RoomPowerLevelsEventContent},
|
||||
},
|
||||
};
|
||||
|
||||
use crate::{Event, EventTypeExt, Pdu, RoomVersion, matrix::StateKey, state_res::Error};
|
||||
|
||||
pub enum UserPower {
|
||||
/// Creator indicates this user should be granted a power level above all.
|
||||
Creator,
|
||||
/// Standard indicates power levels should be used to determine rank.
|
||||
Standard,
|
||||
}
|
||||
|
||||
impl PartialEq for UserPower {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
| (UserPower::Creator, UserPower::Creator) => true,
|
||||
| (UserPower::Standard, UserPower::Standard) => true,
|
||||
| _ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the creators of the room.
|
||||
/// If this room only supports one creator, a vec of one will be returned.
|
||||
/// If multiple creators are supported, all will be returned, with the
|
||||
/// m.room.create sender first.
|
||||
pub async fn calculate_creators<FS>(
|
||||
room_version: &RoomVersion,
|
||||
fetch_state: FS,
|
||||
) -> Result<Vec<OwnedUserId>, Error>
|
||||
where
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
let create_event = fetch_state(StateEventType::RoomCreate.with_state_key(""))
|
||||
.await?
|
||||
.ok_or_else(|| Error::InvalidPdu("Room create event not found".to_owned()))?;
|
||||
let content = create_event
|
||||
.get_content::<RoomCreateEventContent>()
|
||||
.map_err(|e| {
|
||||
Error::InvalidPdu(format!("Room create event has invalid content: {}", e))
|
||||
})?;
|
||||
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
let mut creators = vec![create_event.sender().to_owned()];
|
||||
if let Some(additional) = content.additional_creators {
|
||||
for user_id in additional {
|
||||
if !creators.contains(&user_id) {
|
||||
creators.push(user_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(creators)
|
||||
} else if room_version.use_room_create_sender {
|
||||
Ok(vec![create_event.sender().to_owned()])
|
||||
} else {
|
||||
// Have to check the event content
|
||||
#[allow(deprecated)]
|
||||
if let Some(creator) = content.creator {
|
||||
Ok(vec![creator])
|
||||
} else {
|
||||
Err(Error::InvalidPdu("Room create event missing creator field".to_owned()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rank fetches the creatorship and power level of the target user
|
||||
///
|
||||
/// Returns (UserPower, power_level, Option<RoomPowerLevelsEventContent>)
|
||||
/// If UserPower::Creator is returned, the power_level and
|
||||
/// RoomPowerLevelsEventContent will be meaningless and can be ignored.
|
||||
pub async fn get_rank<FS>(
|
||||
room_version: &RoomVersion,
|
||||
fetch_state: &FS,
|
||||
user_id: &UserId,
|
||||
) -> Result<(UserPower, Int, Option<RoomPowerLevelsEventContent>), Error>
|
||||
where
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
let creators = calculate_creators(room_version, &fetch_state).await?;
|
||||
if creators.contains(&user_id.to_owned()) && room_version.explicitly_privilege_room_creators {
|
||||
return Ok((UserPower::Creator, Int::MAX, None));
|
||||
}
|
||||
|
||||
let power_levels = fetch_state(StateEventType::RoomPowerLevels.with_state_key("")).await?;
|
||||
if let Some(power_levels) = power_levels {
|
||||
let power_levels = power_levels
|
||||
.get_content::<RoomPowerLevelsEventContent>()
|
||||
.map_err(|e| {
|
||||
Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e))
|
||||
})?;
|
||||
Ok((
|
||||
UserPower::Standard,
|
||||
*power_levels
|
||||
.users
|
||||
.get(user_id)
|
||||
.unwrap_or(&power_levels.users_default),
|
||||
Some(power_levels),
|
||||
))
|
||||
} else {
|
||||
// No power levels event, use defaults
|
||||
if creators[0] == user_id {
|
||||
return Ok((UserPower::Creator, Int::MAX, None));
|
||||
}
|
||||
Ok((UserPower::Standard, Int::from(0), None))
|
||||
}
|
||||
}
|
||||
97
src/core/matrix/state_res/event_auth/create_event.rs
Normal file
97
src/core/matrix/state_res/event_auth/create_event.rs
Normal file
@@ -0,0 +1,97 @@
|
||||
//! Auth checks relevant to the `m.room.create` event specifically.
|
||||
//!
|
||||
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
|
||||
|
||||
use ruma::{OwnedUserId, RoomVersionId, events::room::create::RoomCreateEventContent};
|
||||
use serde::Deserialize;
|
||||
use serde_json::from_str;
|
||||
|
||||
use crate::{Event, Pdu, RoomVersion, state_res::Error, trace};
|
||||
|
||||
// A raw representation of the create event content, for initial parsing.
|
||||
// This allows us to extract fields without fully validating the event first.
|
||||
#[derive(Deserialize)]
|
||||
struct RawCreateContent {
|
||||
creator: Option<String>,
|
||||
room_version: Option<String>,
|
||||
additional_creators: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
// Check whether an `m.room.create` event is valid.
|
||||
// This ensures that:
|
||||
//
|
||||
// 1. The event has no `prev_events`
|
||||
// 2. If the version disallows it, the event has no `room_id` present.
|
||||
// 3. If the room version is present and recognised, otherwise assume invalid.
|
||||
// 4. If the room version supports it, `additional_creators` is populated with
|
||||
// valid user IDs.
|
||||
// 5. If the room version supports it, `creator` is populated AND is a valid
|
||||
// user ID.
|
||||
// 6. Otherwise, this event is valid.
|
||||
//
|
||||
// The fully deserialized `RoomCreateEventContent` is returned for further calls
|
||||
// to other checks.
|
||||
pub fn check_room_create(event: &Pdu) -> Result<RoomCreateEventContent, Error> {
|
||||
// Check 1: The event has no `prev_events`
|
||||
if !event.prev_events.is_empty() {
|
||||
return Err(Error::InvalidPdu("m.room.create event has prev_events".to_owned()));
|
||||
}
|
||||
|
||||
let create_content = from_str::<RawCreateContent>(event.content().get())?;
|
||||
|
||||
// Note: Here we attempt to both load the raw room version string and validate
|
||||
// it, and then cast it to the room features. If either step fails, we return
|
||||
// an unsupported error. If the room version is missing, it defaults to "1",
|
||||
// which we also do not support.
|
||||
//
|
||||
// This performs check 3, which then allows us to perform check 2.
|
||||
let room_version = if let Some(raw_room_version) = create_content.room_version {
|
||||
trace!("Parsing and interpreting room version: {}", raw_room_version);
|
||||
let room_version_id = RoomVersionId::try_from(raw_room_version.as_str())
|
||||
.map_err(|_| Error::Unsupported(raw_room_version))?;
|
||||
RoomVersion::new(&room_version_id)
|
||||
.map_err(|_| Error::Unsupported(room_version_id.as_str().to_owned()))?
|
||||
} else {
|
||||
return Err(Error::Unsupported("1".to_owned()));
|
||||
};
|
||||
|
||||
// Check 2: If the version disallows it, the event has no `room_id` present.
|
||||
if room_version.room_ids_as_hashes && event.room_id.is_some() {
|
||||
return Err(Error::InvalidPdu(
|
||||
"m.room.create event has room_id but room version disallows it".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check 4: If the room version supports it, `additional_creators` is populated
|
||||
// with valid user IDs.
|
||||
if room_version.explicitly_privilege_room_creators {
|
||||
if let Some(additional_creators) = create_content.additional_creators {
|
||||
for creator in additional_creators {
|
||||
trace!("Validating additional creator user ID: {}", creator);
|
||||
if OwnedUserId::parse(&creator).is_err() {
|
||||
return Err(Error::InvalidPdu(format!(
|
||||
"Invalid user ID in additional_creators: {creator}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check 5: If the room version supports it, `creator` is populated AND is a
|
||||
// valid user ID.
|
||||
if !room_version.use_room_create_sender {
|
||||
if let Some(creator) = create_content.creator {
|
||||
trace!("Validating creator user ID: {}", creator);
|
||||
if OwnedUserId::parse(&creator).is_err() {
|
||||
return Err(Error::InvalidPdu(format!("Invalid user ID in creator: {creator}")));
|
||||
}
|
||||
} else {
|
||||
return Err(Error::InvalidPdu(
|
||||
"m.room.create event missing creator field".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Deserialise into the full create event for future checks.
|
||||
Ok(from_str::<RoomCreateEventContent>(event.content().get())?)
|
||||
}
|
||||
650
src/core/matrix/state_res/event_auth/iterative_auth_checks.rs
Normal file
650
src/core/matrix/state_res/event_auth/iterative_auth_checks.rs
Normal file
@@ -0,0 +1,650 @@
|
||||
use ruma::{
|
||||
EventId, OwnedUserId, RoomVersionId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{create::RoomCreateEventContent, member::MembershipState},
|
||||
},
|
||||
int,
|
||||
serde::Raw,
|
||||
};
|
||||
use serde::{Deserialize, de::IgnoredAny};
|
||||
use serde_json::from_str as from_json_str;
|
||||
|
||||
use crate::{
|
||||
Event, EventTypeExt, Pdu, RoomVersion, debug, error,
|
||||
matrix::StateKey,
|
||||
state_res::{
|
||||
error::Error,
|
||||
event_auth::{
|
||||
auth_events::check_auth_events,
|
||||
context::{UserPower, calculate_creators, get_rank},
|
||||
create_event::check_room_create,
|
||||
member_event::check_member_event,
|
||||
power_levels::check_power_levels,
|
||||
},
|
||||
},
|
||||
trace, warn,
|
||||
};
|
||||
|
||||
// FIXME: field extracting could be bundled for `content`
|
||||
#[derive(Deserialize)]
|
||||
struct GetMembership {
|
||||
membership: MembershipState,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
struct RoomMemberContentFields {
|
||||
membership: Option<Raw<MembershipState>>,
|
||||
join_authorised_via_users_server: Option<Raw<OwnedUserId>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RoomCreateContentFields {
|
||||
room_version: Option<Raw<RoomVersionId>>,
|
||||
creator: Option<Raw<IgnoredAny>>,
|
||||
additional_creators: Option<Vec<Raw<OwnedUserId>>>,
|
||||
#[serde(rename = "m.federate", default = "ruma::serde::default_true")]
|
||||
federate: bool,
|
||||
}
|
||||
|
||||
/// Authenticate the incoming `event`.
|
||||
///
|
||||
/// The steps of authentication are:
|
||||
///
|
||||
/// * check that the event is being authenticated for the correct room
|
||||
/// * then there are checks for specific event types
|
||||
///
|
||||
/// The `fetch_state` closure should gather state from a state snapshot. We need
|
||||
/// to know if the event passes auth against some state not a recursive
|
||||
/// collection of auth_events fields.
|
||||
#[tracing::instrument(
|
||||
skip_all,
|
||||
fields(
|
||||
event_id = incoming_event.event_id().as_str(),
|
||||
event_type = ?incoming_event.event_type().to_string()
|
||||
)
|
||||
)]
|
||||
#[allow(clippy::suspicious_operation_groupings)]
|
||||
pub async fn auth_check<FE, FS>(
|
||||
room_version: &RoomVersion,
|
||||
incoming_event: &Pdu,
|
||||
fetch_event: &FE,
|
||||
fetch_state: &FS,
|
||||
create_event: Option<&Pdu>,
|
||||
) -> Result<bool, Error>
|
||||
where
|
||||
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
debug!("auth_check beginning");
|
||||
let sender = incoming_event.sender();
|
||||
|
||||
// Since v1, If type is m.room.create:
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomCreate {
|
||||
debug!("start m.room.create check");
|
||||
if let Err(e) = check_room_create(incoming_event) {
|
||||
warn!("m.room.create event has been rejected: {}", e);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug!("m.room.create event was allowed");
|
||||
return Ok(true);
|
||||
}
|
||||
let Some(create_event) = create_event else {
|
||||
error!("no create event provided for auth check");
|
||||
return Err(Error::InvalidPdu("missing create event".to_owned()));
|
||||
};
|
||||
|
||||
// TODO: we need to know if events have previously been rejected or soft failed
|
||||
// For now, we'll just assume the create_event is valid.
|
||||
let create_content = from_json_str::<RoomCreateEventContent>(create_event.content().get())
|
||||
.expect("provided create event must be valid");
|
||||
|
||||
// Since v12, If the event’s room_id is not an event ID for an accepted (not
|
||||
// rejected) m.room.create event, with the sigil ! instead of $, reject.
|
||||
if room_version.room_ids_as_hashes {
|
||||
let calculated_room_id = create_event.event_id().as_str().replace('$', "!");
|
||||
if let Some(claimed_room_id) = create_event.room_id() {
|
||||
if claimed_room_id.as_str() != calculated_room_id {
|
||||
warn!(
|
||||
expected = %calculated_room_id,
|
||||
received = %claimed_room_id,
|
||||
"event's room ID does not match the hash of the m.room.create event ID"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
} else {
|
||||
warn!("event is missing a room ID");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
let room_id = incoming_event.room_id().expect("event must have a room ID");
|
||||
|
||||
let auth_map =
|
||||
match check_auth_events(incoming_event, room_id, &room_version, fetch_event).await {
|
||||
| Ok(map) => map,
|
||||
| Err(e) => {
|
||||
warn!("event's auth events are invalid: {}", e);
|
||||
return Ok(false);
|
||||
},
|
||||
};
|
||||
|
||||
// Considering the event's auth_events
|
||||
|
||||
// Since v1, If the content of the m.room.create event in the room state has the
|
||||
// property m.federate set to false, and the sender domain of the event does
|
||||
// not match the sender domain of the create event, reject.
|
||||
if !create_content.federate {
|
||||
if create_event.sender().server_name() != incoming_event.sender().server_name() {
|
||||
warn!(
|
||||
sender = %incoming_event.sender(),
|
||||
create_sender = %create_event.sender(),
|
||||
"room is not federated and event's sender domain does not match create event's sender domain"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// From v1 to v5, If type is m.room.aliases
|
||||
if room_version.special_case_aliases_auth
|
||||
&& *incoming_event.event_type() == TimelineEventType::RoomAliases
|
||||
{
|
||||
if let Some(state_key) = incoming_event.state_key() {
|
||||
// If sender's domain doesn't matches state_key, reject
|
||||
if state_key != sender.server_name().as_str() {
|
||||
warn!("state_key does not match sender");
|
||||
return Ok(false);
|
||||
}
|
||||
// Otherwise, allow
|
||||
return Ok(true);
|
||||
}
|
||||
// If event has no state_key, reject.
|
||||
warn!("m.room.alias event has no state key");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// From v1, If type is m.room.member
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomMember {
|
||||
if let Err(e) =
|
||||
check_member_event(&room_version, incoming_event, fetch_event, fetch_state).await
|
||||
{
|
||||
warn!("m.room.member event has been rejected: {}", e);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// From v1, If the sender's current membership state is not join, reject
|
||||
let sender_member_event =
|
||||
match auth_map.get(&StateEventType::RoomMember.with_state_key(sender.as_str())) {
|
||||
| Some(ev) => ev,
|
||||
| None => {
|
||||
warn!(
|
||||
%sender,
|
||||
"sender is not joined - no membership event found for sender in auth events"
|
||||
);
|
||||
return Ok(false);
|
||||
},
|
||||
};
|
||||
|
||||
let sender_membership_event_content: RoomMemberContentFields =
|
||||
from_json_str(sender_member_event.content().get())?;
|
||||
let Some(membership_state) = sender_membership_event_content.membership else {
|
||||
warn!(
|
||||
?sender_membership_event_content,
|
||||
"Sender membership event content missing membership field"
|
||||
);
|
||||
return Err(Error::InvalidPdu("Missing membership field".to_owned()));
|
||||
};
|
||||
let membership_state = membership_state.deserialize()?;
|
||||
|
||||
if membership_state != MembershipState::Join {
|
||||
warn!(
|
||||
%sender,
|
||||
?membership_state,
|
||||
"sender cannot send events without being joined to the room"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// From v1, If type is m.room.third_party_invite
|
||||
let (rank, sender_pl, pl_evt) = get_rank(&room_version, fetch_state, sender).await?;
|
||||
|
||||
// Allow if and only if sender's current power level is greater than
|
||||
// or equal to the invite level
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomThirdPartyInvite {
|
||||
if rank == UserPower::Creator {
|
||||
trace!("sender is room creator, allowing m.room.third_party_invite");
|
||||
return Ok(true);
|
||||
}
|
||||
let invite_level = match &pl_evt {
|
||||
| Some(power_levels) => power_levels.invite,
|
||||
| None => int!(0),
|
||||
};
|
||||
|
||||
if sender_pl < invite_level {
|
||||
warn!(
|
||||
%sender,
|
||||
has=%sender_pl,
|
||||
required=%invite_level,
|
||||
"sender cannot send invites in this room"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
debug!("m.room.third_party_invite event was allowed");
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
// Since v1, if the event type’s required power level is greater than the
|
||||
// sender’s power level, reject.
|
||||
let required_level = match &pl_evt {
|
||||
| Some(power_levels) => power_levels
|
||||
.events
|
||||
.get(incoming_event.kind())
|
||||
.unwrap_or_else(|| {
|
||||
if incoming_event.state_key.is_some() {
|
||||
&power_levels.state_default
|
||||
} else {
|
||||
&power_levels.events_default
|
||||
}
|
||||
}),
|
||||
| None => &int!(0),
|
||||
};
|
||||
if rank != UserPower::Creator && sender_pl < *required_level {
|
||||
warn!(
|
||||
%sender,
|
||||
has=%sender_pl,
|
||||
required=%required_level,
|
||||
"sender does not have enough power level to send this event"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
// Since v1, If the event has a state_key that starts with an @ and does not
|
||||
// match the sender, reject.
|
||||
if let Some(state_key) = incoming_event.state_key() {
|
||||
if state_key.starts_with('@') && state_key != sender.as_str() {
|
||||
warn!(
|
||||
%sender,
|
||||
%state_key,
|
||||
"event's state key starts with @ and does not match sender"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// Since v1, If type is m.room.power_levels
|
||||
if *incoming_event.event_type() == TimelineEventType::RoomPowerLevels {
|
||||
let creators = calculate_creators(&room_version, fetch_state).await?;
|
||||
if let Err(e) =
|
||||
check_power_levels(&room_version, incoming_event, pl_evt.as_ref(), creators).await
|
||||
{
|
||||
warn!(
|
||||
%sender,
|
||||
"m.room.power_levels event has been rejected: {}", e
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
||||
// From v1 to v2: If type is m.room.redaction:
|
||||
// If the sender’s power level is greater than or equal to the redact level,
|
||||
// allow.
|
||||
// If the domain of the event_id of the event being redacted is the same as the
|
||||
// domain of the event_id of the m.room.redaction, allow.
|
||||
// Otherwise, reject.
|
||||
if room_version.extra_redaction_checks {
|
||||
// We'll panic here, since while we don't theoretically support the room
|
||||
// versions that require this, we don't want to incorrectly permit an event
|
||||
// that should be rejected in this theoretically impossible scenario.
|
||||
unreachable!(
|
||||
"continuwuity does not support room versions that require extra redaction checks"
|
||||
);
|
||||
}
|
||||
|
||||
debug!("allowing event passed all checks");
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use ruma::events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
join_rules::{
|
||||
AllowRule, JoinRule, Restricted, RoomJoinRulesEventContent, RoomMembership,
|
||||
},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
};
|
||||
use serde_json::value::to_raw_value as to_raw_json_value;
|
||||
|
||||
use crate::{
|
||||
matrix::{Event, EventTypeExt, Pdu as PduEvent},
|
||||
state_res::{
|
||||
RoomVersion, StateMap,
|
||||
event_auth::{
|
||||
iterative_auth_checks::valid_membership_change, valid_membership_change,
|
||||
},
|
||||
test_utils::{
|
||||
INITIAL_EVENTS, INITIAL_EVENTS_CREATE_ROOM, alice, charlie, ella, event_id,
|
||||
member_content_ban, member_content_join, room_id, to_pdu_event,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_ban_pass() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().as_str()),
|
||||
member_content_ban(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = charlie();
|
||||
let sender = alice();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_non_creator() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS_CREATE_ROOM();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(charlie().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE"],
|
||||
&["CREATE"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = charlie();
|
||||
let sender = charlie();
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_join_creator() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS_CREATE_ROOM();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
alice(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().as_str()),
|
||||
member_content_join(),
|
||||
&["CREATE"],
|
||||
&["CREATE"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = alice();
|
||||
let sender = alice();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ban_fail() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let events = INITIAL_EVENTS();
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
charlie(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(alice().as_str()),
|
||||
member_content_ban(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = alice();
|
||||
let sender = charlie();
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V6,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_restricted_join_rule() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let mut events = INITIAL_EVENTS();
|
||||
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Restricted(
|
||||
Restricted::new(vec![AllowRule::RoomMembership(RoomMembership::new(
|
||||
room_id().to_owned(),
|
||||
))]),
|
||||
)))
|
||||
.unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["IPOWER"],
|
||||
);
|
||||
|
||||
let mut member = RoomMemberEventContent::new(MembershipState::Join);
|
||||
member.join_authorized_via_users_server = Some(alice().to_owned());
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
ella(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Join)).unwrap(),
|
||||
&["CREATE", "IJR", "IPOWER", "new"],
|
||||
&["new"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = ella();
|
||||
let sender = ella();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
Some(alice()),
|
||||
&MembershipState::Join,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
assert!(
|
||||
!valid_membership_change(
|
||||
&RoomVersion::V9,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
Some(ella()),
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_knock() {
|
||||
let _ = tracing::subscriber::set_default(
|
||||
tracing_subscriber::fmt().with_test_writer().finish(),
|
||||
);
|
||||
let mut events = INITIAL_EVENTS();
|
||||
*events.get_mut(&event_id("IJR")).unwrap() = to_pdu_event(
|
||||
"IJR",
|
||||
alice(),
|
||||
TimelineEventType::RoomJoinRules,
|
||||
Some(""),
|
||||
to_raw_json_value(&RoomJoinRulesEventContent::new(JoinRule::Knock)).unwrap(),
|
||||
&["CREATE", "IMA", "IPOWER"],
|
||||
&["IPOWER"],
|
||||
);
|
||||
|
||||
let auth_events = events
|
||||
.values()
|
||||
.map(|ev| (ev.event_type().with_state_key(ev.state_key().unwrap()), ev.clone()))
|
||||
.collect::<StateMap<_>>();
|
||||
|
||||
let requester = to_pdu_event(
|
||||
"HELLO",
|
||||
ella(),
|
||||
TimelineEventType::RoomMember,
|
||||
Some(ella().as_str()),
|
||||
to_raw_json_value(&RoomMemberEventContent::new(MembershipState::Knock)).unwrap(),
|
||||
&[],
|
||||
&["IMC"],
|
||||
);
|
||||
|
||||
let fetch_state = |ty, key| auth_events.get(&(ty, key)).cloned();
|
||||
let target_user = ella();
|
||||
let sender = ella();
|
||||
|
||||
assert!(
|
||||
valid_membership_change(
|
||||
&RoomVersion::V7,
|
||||
target_user,
|
||||
fetch_state(StateEventType::RoomMember, target_user.as_str().into()).as_ref(),
|
||||
sender,
|
||||
fetch_state(StateEventType::RoomMember, sender.as_str().into()).as_ref(),
|
||||
&requester,
|
||||
None::<&PduEvent>,
|
||||
fetch_state(StateEventType::RoomPowerLevels, "".into()).as_ref(),
|
||||
fetch_state(StateEventType::RoomJoinRules, "".into()).as_ref(),
|
||||
None,
|
||||
&MembershipState::Leave,
|
||||
&fetch_state(StateEventType::RoomCreate, "".into()).unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
422
src/core/matrix/state_res/event_auth/member_event.rs
Normal file
422
src/core/matrix/state_res/event_auth/member_event.rs
Normal file
@@ -0,0 +1,422 @@
|
||||
//! Auth checks relevant to the `m.room.member` event specifically.
|
||||
//!
|
||||
//! See: https://spec.matrix.org/v1.16/rooms/v12/#authorization-rules
|
||||
|
||||
use ruma::{
|
||||
EventId, OwnedUserId, UserId,
|
||||
events::{
|
||||
StateEventType,
|
||||
room::{
|
||||
join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
third_party_invite::{PublicKey, RoomThirdPartyInviteEventContent},
|
||||
},
|
||||
},
|
||||
serde::Base64,
|
||||
signatures::{PublicKeyMap, PublicKeySet, verify_json},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Event, EventTypeExt, Pdu, RoomVersion,
|
||||
matrix::StateKey,
|
||||
state_res::{
|
||||
Error,
|
||||
event_auth::context::{UserPower, get_rank},
|
||||
},
|
||||
utils::to_canonical_object,
|
||||
};
|
||||
|
||||
#[derive(serde::Deserialize, Default)]
|
||||
struct PartialMembershipObject {
|
||||
membership: Option<String>,
|
||||
join_authorized_via_users_server: Option<OwnedUserId>,
|
||||
third_party_invite: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
/// Fetches the membership *content* of the target.
|
||||
/// If there is not one, an empty leave membership is returned.
|
||||
async fn fetch_membership<FS>(
|
||||
fetch_state: &FS,
|
||||
target: &UserId,
|
||||
) -> Result<PartialMembershipObject, Error>
|
||||
where
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
fetch_state(StateEventType::RoomMember.with_state_key(target.as_str()))
|
||||
.await
|
||||
.map(|pdu| {
|
||||
if let Some(ev) = pdu {
|
||||
ev.get_content::<PartialMembershipObject>().map_err(|e| {
|
||||
Error::InvalidPdu(format!("m.room.member event has invalid content: {}", e))
|
||||
})
|
||||
} else {
|
||||
Ok(PartialMembershipObject {
|
||||
membership: Some("leave".to_owned()),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
async fn check_join_event<FE, FS>(
|
||||
room_version: &RoomVersion,
|
||||
event: &Pdu,
|
||||
membership: &PartialMembershipObject,
|
||||
target: &UserId,
|
||||
fetch_event: &FE,
|
||||
fetch_state: &FS,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
// 3.1: If the only previous event is an m.room.create and the state_key is the
|
||||
// sender of the m.room.create, allow.
|
||||
if event.prev_events.len() == 1 {
|
||||
let only_prev = fetch_event(&event.prev_events[0]).await?;
|
||||
if let Some(prev_event) = only_prev {
|
||||
let k = prev_event.event_type().with_state_key("");
|
||||
if k.0 == StateEventType::RoomCreate && k.1.as_str() == event.sender().as_str() {
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
return Err(Error::DependencyFailed(
|
||||
event.prev_events[0].to_owned(),
|
||||
"Previous event not found when checking join event".to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// 3.2: If the sender does not match state_key, reject.
|
||||
if event.sender() != target {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"m.room.member join event sender does not match state_key".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
let prev_membership = if let Some(ev) =
|
||||
fetch_state(StateEventType::RoomMember.with_state_key(target.as_str())).await?
|
||||
{
|
||||
Some(ev.get_content::<PartialMembershipObject>().map_err(|e| {
|
||||
Error::InvalidPdu(format!("Previous m.room.member event has invalid content: {}", e))
|
||||
})?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let join_rule_content =
|
||||
if let Some(jr) = fetch_state(StateEventType::RoomJoinRules.with_state_key("")).await? {
|
||||
jr.get_content::<RoomJoinRulesEventContent>().map_err(|e| {
|
||||
Error::InvalidPdu(format!("m.room.join_rules event has invalid content: {}", e))
|
||||
})?
|
||||
} else {
|
||||
// Default to invite if no join rules event is present.
|
||||
RoomJoinRulesEventContent { join_rule: JoinRule::Private }
|
||||
};
|
||||
|
||||
// 3.3: If the sender is banned, reject.
|
||||
let prev_member = if let Some(prev_content) = &prev_membership {
|
||||
if let Some(membership) = &prev_content.membership {
|
||||
if membership == "ban" {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"m.room.member join event sender is banned".to_owned(),
|
||||
));
|
||||
}
|
||||
membership
|
||||
} else {
|
||||
"leave"
|
||||
}
|
||||
} else {
|
||||
"leave"
|
||||
};
|
||||
|
||||
// 3.4: If the join_rule is invite or knock then allow if membership
|
||||
// state is invite or join.
|
||||
// 3.5: If the join_rule is restricted or knock_restricted:
|
||||
// 3.5.1: If membership state is join or invite, allow.
|
||||
match join_rule_content.join_rule {
|
||||
| JoinRule::Invite | JoinRule::Knock => {
|
||||
if prev_member == "invite" || prev_member == "join" {
|
||||
return Ok(());
|
||||
}
|
||||
Err(Error::AuthConditionFailed(
|
||||
"m.room.member join event not invited under invite/knock join rule".to_owned(),
|
||||
))
|
||||
},
|
||||
| JoinRule::Restricted(_) | JoinRule::KnockRestricted(_) => {
|
||||
// 3.5.2: If the join_authorised_via_users_server key in content is not a user
|
||||
// with sufficient permission to invite other users or is not a joined
|
||||
// member of the room, reject.
|
||||
if prev_member == "invite" || prev_member == "join" {
|
||||
return Ok(());
|
||||
}
|
||||
let join_authed_by = membership.join_authorized_via_users_server.as_ref();
|
||||
if let Some(user_id) = join_authed_by {
|
||||
let rank = get_rank(&room_version, fetch_state, user_id).await?;
|
||||
if rank.0 == UserPower::Standard {
|
||||
// This user is not a creator, check that they have
|
||||
// sufficient power level
|
||||
if rank.1 < rank.2.unwrap().invite {
|
||||
return Err(Error::InvalidPdu(
|
||||
"m.room.member join event join_authorised_via_users_server does not \
|
||||
have sufficient power level to invite"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
}
|
||||
// Check that the user is a joined member of the room
|
||||
if let Some(state_event) =
|
||||
fetch_state(StateEventType::RoomMember.with_state_key(user_id.as_str()))
|
||||
.await?
|
||||
{
|
||||
let state_content = state_event
|
||||
.get_content::<PartialMembershipObject>()
|
||||
.map_err(|e| {
|
||||
Error::InvalidPdu(format!(
|
||||
"m.room.member event has invalid content: {}",
|
||||
e
|
||||
))
|
||||
})?;
|
||||
if let Some(state_membership) = &state_content.membership {
|
||||
if state_membership == "join" {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"m.room.member join event missing join_authorised_via_users_server"
|
||||
.to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// 3.5.3: Otherwise, allow
|
||||
return Ok(());
|
||||
},
|
||||
| JoinRule::Public => return Ok(()),
|
||||
| _ => Err(Error::AuthConditionFailed(format!(
|
||||
"unknown join rule: {:?}",
|
||||
join_rule_content.join_rule
|
||||
)))?,
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks a third-party invite is valid.
|
||||
async fn check_third_party_invite(
|
||||
target_current_membership: PartialMembershipObject,
|
||||
raw_third_party_invite: &serde_json::Value,
|
||||
target: &UserId,
|
||||
event: &Pdu,
|
||||
fetch_state: impl AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
) -> Result<(), Error> {
|
||||
// 4.1.1: If target user is banned, reject.
|
||||
if target_current_membership
|
||||
.membership
|
||||
.is_some_and(|m| m == "ban")
|
||||
{
|
||||
return Err(Error::AuthConditionFailed("invite target is banned".to_owned()));
|
||||
}
|
||||
// 4.1.2: If content.third_party_invite does not have a signed property, reject.
|
||||
let signed = raw_third_party_invite.get("signed").ok_or_else(|| {
|
||||
Error::AuthConditionFailed(
|
||||
"invite event third_party_invite missing signed property".to_owned(),
|
||||
)
|
||||
})?;
|
||||
// 4.2.3: If signed does not have mxid and token properties, reject.
|
||||
let mxid = signed.get("mxid").and_then(|v| v.as_str()).ok_or_else(|| {
|
||||
Error::AuthConditionFailed(
|
||||
"invite event third_party_invite signed missing/invalid mxid property".to_owned(),
|
||||
)
|
||||
})?;
|
||||
let token = signed
|
||||
.get("token")
|
||||
.and_then(|v| v.as_str())
|
||||
.ok_or_else(|| {
|
||||
Error::AuthConditionFailed(
|
||||
"invite event third_party_invite signed missing token property".to_owned(),
|
||||
)
|
||||
})?;
|
||||
// 4.2.4: If mxid does not match state_key, reject.
|
||||
if mxid != target.as_str() {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"invite event third_party_invite signed mxid does not match state_key".to_owned(),
|
||||
));
|
||||
}
|
||||
// 4.2.5: If there is no m.room.third_party_invite event in the room
|
||||
// state matching the token, reject.
|
||||
let Some(third_party_invite_event) =
|
||||
fetch_state(StateEventType::RoomThirdPartyInvite.with_state_key(token)).await?
|
||||
else {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"invite event third_party_invite token has no matching m.room.third_party_invite"
|
||||
.to_owned(),
|
||||
));
|
||||
};
|
||||
// 4.2.6: If sender does not match sender of the m.room.third_party_invite,
|
||||
// reject.
|
||||
if third_party_invite_event.sender() != event.sender() {
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"invite event sender does not match m.room.third_party_invite sender".to_owned(),
|
||||
));
|
||||
}
|
||||
// 4.2.7: If any signature in signed matches any public key in the
|
||||
// m.room.third_party_invite event, allow. The public keys are in
|
||||
// content of m.room.third_party_invite as:
|
||||
// 1. A single public key in the public_key property.
|
||||
// 2. A list of public keys in the public_keys property.
|
||||
let tpi_content = third_party_invite_event
|
||||
.get_content::<RoomThirdPartyInviteEventContent>()
|
||||
.or_else(|_| {
|
||||
Err(Error::InvalidPdu(
|
||||
"m.room.third_party_invite event has invalid content".to_owned(),
|
||||
))
|
||||
})?;
|
||||
let mut public_keys = tpi_content.public_keys.unwrap_or_default();
|
||||
public_keys.push(PublicKey {
|
||||
public_key: tpi_content.public_key,
|
||||
key_validity_url: None,
|
||||
});
|
||||
|
||||
let signatures = signed
|
||||
.get("signatures")
|
||||
.and_then(|v| v.as_object())
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidPdu(
|
||||
"invite event third_party_invite signed missing/invalid signatures".to_owned(),
|
||||
)
|
||||
})?;
|
||||
let mut public_key_map = PublicKeyMap::new();
|
||||
for (server_name, sig_map) in signatures {
|
||||
let mut pk_set = PublicKeySet::new();
|
||||
if let Some(sig_map) = sig_map.as_object() {
|
||||
for (key_id, sig) in sig_map {
|
||||
let sig_b64 = Base64::parse(sig.as_str().ok_or(Error::InvalidPdu(
|
||||
"invite event third_party_invite signature is not a string".to_owned(),
|
||||
))?)
|
||||
.map_err(|_| {
|
||||
Error::InvalidPdu(
|
||||
"invite event third_party_invite signature is not valid Base64"
|
||||
.to_owned(),
|
||||
)
|
||||
})?;
|
||||
pk_set.insert(key_id.clone(), sig_b64);
|
||||
}
|
||||
}
|
||||
public_key_map.insert(server_name.clone(), pk_set);
|
||||
}
|
||||
verify_json(
|
||||
&public_key_map,
|
||||
to_canonical_object(signed).expect("signed was already validated"),
|
||||
)
|
||||
.map_err(|e| {
|
||||
Error::AuthConditionFailed(format!(
|
||||
"invite event third_party_invite signature verification failed: {e}"
|
||||
))
|
||||
})?;
|
||||
// If there was no error, there was a valid signature, so allow.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn check_invite_event<FS>(
|
||||
room_version: &RoomVersion,
|
||||
event: &Pdu,
|
||||
membership: &PartialMembershipObject,
|
||||
target: &UserId,
|
||||
fetch_state: &FS,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
let target_current_membership = fetch_membership(fetch_state, target).await?;
|
||||
|
||||
// 4.1: If content has a third_party_invite property:
|
||||
if let Some(raw_third_party_invite) = &membership.third_party_invite {
|
||||
return check_third_party_invite(
|
||||
target_current_membership,
|
||||
raw_third_party_invite,
|
||||
target,
|
||||
event,
|
||||
fetch_state,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// 4.2: If the sender’s current membership state is not join, reject.
|
||||
let sender_membership = fetch_membership(fetch_state, event.sender()).await?;
|
||||
if sender_membership.membership.is_none_or(|m| m != "join") {
|
||||
return Err(Error::AuthConditionFailed("invite sender is not joined".to_owned()));
|
||||
}
|
||||
|
||||
// 4.3: If target user’s current membership state is join or ban, reject.
|
||||
if target_current_membership
|
||||
.membership
|
||||
.is_some_and(|m| m == "join" || m == "ban")
|
||||
{
|
||||
return Err(Error::AuthConditionFailed(
|
||||
"invite target is already joined or banned".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// 4.4: If the sender’s power level is greater than or equal to the invite
|
||||
// level, allow.
|
||||
let (rank, pl, pl_evt) = get_rank(&room_version, fetch_state, event.sender()).await?;
|
||||
if rank == UserPower::Creator || pl >= pl_evt.unwrap_or_default().invite {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// 4.5: Otherwise, reject.
|
||||
Err(Error::AuthConditionFailed(
|
||||
"invite sender does not have sufficient power level to invite".to_owned(),
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn check_member_event<FE, FS>(
|
||||
room_version: &RoomVersion,
|
||||
event: &Pdu,
|
||||
fetch_event: FE,
|
||||
fetch_state: FS,
|
||||
) -> Result<(), Error>
|
||||
where
|
||||
FE: AsyncFn(&EventId) -> Result<Option<Pdu>, Error>,
|
||||
FS: AsyncFn((StateEventType, StateKey)) -> Result<Option<Pdu>, Error>,
|
||||
{
|
||||
// 1. If there is no state_key property, or no membership property in content,
|
||||
// reject.
|
||||
if event.state_key.is_none() {
|
||||
return Err(Error::InvalidPdu("m.room.member event missing state_key".to_owned()));
|
||||
}
|
||||
|
||||
let target = UserId::parse(event.state_key().unwrap())
|
||||
.map_err(|_| Error::InvalidPdu("m.room.member event has invalid state_key".to_owned()))?
|
||||
.to_owned();
|
||||
let content = event
|
||||
.get_content::<PartialMembershipObject>()
|
||||
.map_err(|e| {
|
||||
Error::InvalidPdu(format!("m.room.member event has invalid content: {}", e))
|
||||
})?;
|
||||
|
||||
if content.membership.is_none() {
|
||||
return Err(Error::InvalidPdu(
|
||||
"m.room.member event missing membership in content".to_owned(),
|
||||
));
|
||||
}
|
||||
|
||||
// 2: If content has a join_authorised_via_users_server key
|
||||
//
|
||||
// 2.1: If the event is not validly signed by the homeserver of the user ID
|
||||
// denoted by the key, reject.
|
||||
if let Some(_join_auth) = &content.join_authorized_via_users_server {
|
||||
// We need to check the signature here, but don't have the means to do so yet.
|
||||
todo!("Implement join_authorised_via_users_server check");
|
||||
}
|
||||
|
||||
match content.membership.as_deref().unwrap() {
|
||||
| "join" =>
|
||||
check_join_event(room_version, event, &content, &target, &fetch_event, &fetch_state)
|
||||
.await?,
|
||||
| "invite" =>
|
||||
check_invite_event(room_version, event, &content, &target, &fetch_state).await?,
|
||||
| _ => {
|
||||
todo!()
|
||||
},
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
6
src/core/matrix/state_res/event_auth/mod.rs
Normal file
6
src/core/matrix/state_res/event_auth/mod.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
pub mod auth_events;
|
||||
mod context;
|
||||
pub mod create_event;
|
||||
pub mod iterative_auth_checks;
|
||||
pub mod member_event;
|
||||
mod power_levels;
|
||||
157
src/core/matrix/state_res/event_auth/power_levels.rs
Normal file
157
src/core/matrix/state_res/event_auth/power_levels.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use ruma::{OwnedUserId, events::room::power_levels::RoomPowerLevelsEventContent};
|
||||
|
||||
use crate::{
|
||||
Event, Pdu, RoomVersion,
|
||||
state_res::{Error, event_auth::context::UserPower},
|
||||
};
|
||||
|
||||
/// Verifies that a m.room.power_levels event is well-formed according to the
|
||||
/// Matrix specification.
|
||||
///
|
||||
/// Creators must contain the m.room.create sender and any additional creators.
|
||||
pub async fn check_power_levels(
|
||||
room_version: &RoomVersion,
|
||||
event: &Pdu,
|
||||
current_power_levels: Option<&RoomPowerLevelsEventContent>,
|
||||
creators: Vec<OwnedUserId>,
|
||||
) -> Result<(), Error> {
|
||||
let content = event
|
||||
.get_content::<RoomPowerLevelsEventContent>()
|
||||
.map_err(|e| {
|
||||
Error::InvalidPdu(format!("m.room.power_levels event has invalid content: {}", e))
|
||||
})?;
|
||||
|
||||
// If any of the properties users_default, events_default, state_default, ban,
|
||||
// redact, kick, or invite in content are present and not an integer, reject.
|
||||
//
|
||||
// If either of the properties events or notifications in content are present
|
||||
// and not an object with values that are integers, reject.
|
||||
//
|
||||
// NOTE: Deserialisation fails if this is not the case, so we don't need to
|
||||
// check these here.
|
||||
|
||||
// If the users property in content is not an object with keys that are valid
|
||||
// user IDs with values that are integers (or a string that is an integer),
|
||||
// reject.
|
||||
while let Some(user_id) = content.users.keys().next() {
|
||||
// NOTE: Deserialisation fails if the power level is not an integer, so we don't
|
||||
// need to check that here.
|
||||
|
||||
if let Err(e) = user_id.validate_historical() {
|
||||
return Err(Error::InvalidPdu(format!(
|
||||
"m.room.power_levels event has invalid user ID in users map: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
// Since v12, If the users property in content contains the sender of the
|
||||
// m.room.create event or any of the additional_creators array (if present)
|
||||
// from the content of the m.room.create event, reject.
|
||||
if room_version.explicitly_privilege_room_creators && creators.contains(user_id) {
|
||||
return Err(Error::InvalidPdu(
|
||||
"m.room.power_levels event users map contains a room creator".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// If there is no previous m.room.power_levels event in the room, allow.
|
||||
if current_power_levels.is_none() {
|
||||
return Ok(());
|
||||
}
|
||||
let current_power_levels = current_power_levels.unwrap();
|
||||
|
||||
// For the properties users_default, events_default, state_default, ban, redact,
|
||||
// kick, invite check if they were added, changed or removed. For each found
|
||||
// alteration:
|
||||
// If the current value is higher than the sender’s current power level, reject.
|
||||
// If the new value is higher than the sender’s current power level, reject.
|
||||
let sender = event.sender();
|
||||
let rank = if room_version.explicitly_privilege_room_creators {
|
||||
if creators.contains(&sender.to_owned()) {
|
||||
UserPower::Creator
|
||||
} else {
|
||||
UserPower::Standard
|
||||
}
|
||||
} else {
|
||||
UserPower::Standard
|
||||
};
|
||||
let sender_pl = current_power_levels
|
||||
.users
|
||||
.get(sender)
|
||||
.unwrap_or(¤t_power_levels.users_default);
|
||||
|
||||
if rank != UserPower::Creator {
|
||||
let checks = [
|
||||
("users_default", current_power_levels.users_default, content.users_default),
|
||||
("events_default", current_power_levels.events_default, content.events_default),
|
||||
("state_default", current_power_levels.state_default, content.state_default),
|
||||
("ban", current_power_levels.ban, content.ban),
|
||||
("redact", current_power_levels.redact, content.redact),
|
||||
("kick", current_power_levels.kick, content.kick),
|
||||
("invite", current_power_levels.invite, content.invite),
|
||||
];
|
||||
|
||||
for (name, old_value, new_value) in checks.iter() {
|
||||
if old_value != new_value {
|
||||
if *old_value > *sender_pl {
|
||||
return Err(Error::AuthConditionFailed(format!(
|
||||
"sender cannot change level for {}",
|
||||
name
|
||||
)));
|
||||
}
|
||||
if *new_value > *sender_pl {
|
||||
return Err(Error::AuthConditionFailed(format!(
|
||||
"sender cannot raise level for {} to {}",
|
||||
name, new_value
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each entry being changed in, or removed from, the events
|
||||
// property:
|
||||
// If the current value is greater than the sender’s current power level,
|
||||
// reject.
|
||||
for (event_type, new_value) in content.events.iter() {
|
||||
let old_value = current_power_levels.events.get(event_type);
|
||||
if old_value != Some(new_value) {
|
||||
let old_pl = old_value.unwrap_or(¤t_power_levels.events_default);
|
||||
if *old_pl > *sender_pl {
|
||||
return Err(Error::AuthConditionFailed(format!(
|
||||
"sender cannot change event level for {}",
|
||||
event_type
|
||||
)));
|
||||
}
|
||||
if *new_value > *sender_pl {
|
||||
return Err(Error::AuthConditionFailed(format!(
|
||||
"sender cannot raise event level for {} to {}",
|
||||
event_type, new_value
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For each entry being changed in, or removed from, the events or
|
||||
// notifications properties:
|
||||
// If the current value is greater than the sender’s current power
|
||||
// level, reject.
|
||||
// If the new value is greater than the sender’s current power level,
|
||||
// reject.
|
||||
// TODO after making ruwuma's notifications value a BTreeMap
|
||||
|
||||
// For each entry being added to, or changed in, the users property:
|
||||
// If the new value is greater than the sender’s current power level, reject.
|
||||
for (user_id, new_value) in content.users.iter() {
|
||||
let old_value = current_power_levels.users.get(user_id);
|
||||
if old_value != Some(new_value) {
|
||||
if *new_value > *sender_pl {
|
||||
return Err(Error::AuthConditionFailed(format!(
|
||||
"sender cannot raise user level for {} to {}",
|
||||
user_id, new_value
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -8,9 +8,6 @@
|
||||
#[cfg(test)]
|
||||
mod test_utils;
|
||||
|
||||
#[cfg(test)]
|
||||
mod benches;
|
||||
|
||||
use std::{
|
||||
borrow::Borrow,
|
||||
cmp::{Ordering, Reverse},
|
||||
@@ -18,30 +15,31 @@
|
||||
hash::{BuildHasher, Hash},
|
||||
};
|
||||
|
||||
use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt, future};
|
||||
use futures::{Future, FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
use ruma::{
|
||||
EventId, Int, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::member::{MembershipState, RoomMemberEventContent},
|
||||
},
|
||||
int,
|
||||
room::member::{MembershipState, RoomMemberEventContent}, StateEventType,
|
||||
TimelineEventType,
|
||||
}, int, EventId, Int, MilliSecondsSinceUnixEpoch,
|
||||
OwnedEventId,
|
||||
RoomVersionId,
|
||||
};
|
||||
use serde_json::from_str as from_json_str;
|
||||
|
||||
pub(crate) use self::error::Error;
|
||||
use self::power_levels::PowerLevelsContentFields;
|
||||
pub use self::{
|
||||
event_auth::{auth_check, auth_types_for_event},
|
||||
room_version::RoomVersion,
|
||||
};
|
||||
pub use self::{event_auth::iterative_auth_checks::auth_check, room_version::RoomVersion};
|
||||
use crate::utils::TryFutureExtExt;
|
||||
use crate::{
|
||||
debug, debug_error, err,
|
||||
matrix::{Event, StateKey},
|
||||
state_res::room_version::StateResolutionVersion,
|
||||
debug, err, error as log_error, matrix::{Event, StateKey},
|
||||
state_res::{
|
||||
event_auth::auth_events::auth_types_for_event, room_version::StateResolutionVersion,
|
||||
},
|
||||
trace,
|
||||
utils::stream::{BroadbandExt, IterStream, ReadyExt, TryBroadbandExt, WidebandExt},
|
||||
warn,
|
||||
Pdu,
|
||||
};
|
||||
|
||||
/// A mapping of event type and state_key to some value `T`, usually an
|
||||
@@ -75,23 +73,20 @@
|
||||
/// event is part of the same room.
|
||||
//#[tracing::instrument(level = "debug", skip(state_sets, auth_chain_sets,
|
||||
//#[tracing::instrument(level event_fetch))]
|
||||
pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, ExistsFut>(
|
||||
pub async fn resolve<'a, Sets, SetIter, Hasher, FE, FR, Exists>(
|
||||
room_version: &RoomVersionId,
|
||||
state_sets: Sets,
|
||||
auth_chain_sets: &'a [HashSet<OwnedEventId, Hasher>],
|
||||
event_fetch: &Fetch,
|
||||
event_fetch: &FE,
|
||||
event_exists: &Exists,
|
||||
) -> Result<StateMap<OwnedEventId>>
|
||||
where
|
||||
Fetch: Fn(OwnedEventId) -> FetchFut + Sync,
|
||||
FetchFut: Future<Output = Option<Pdu>> + Send,
|
||||
Exists: Fn(OwnedEventId) -> ExistsFut + Sync,
|
||||
ExistsFut: Future<Output = bool> + Send,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
Exists: AsyncFn(OwnedEventId) -> bool + Sync,
|
||||
Sets: IntoIterator<IntoIter = SetIter> + Send,
|
||||
SetIter: Iterator<Item = &'a StateMap<OwnedEventId>> + Clone + Send,
|
||||
Hasher: BuildHasher + Send + Sync,
|
||||
Pdu: Event + Clone + Send + Sync,
|
||||
for<'b> &'b Pdu: Event + Send,
|
||||
{
|
||||
use RoomVersionId::*;
|
||||
let stateres_version = match room_version {
|
||||
@@ -169,7 +164,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
|
||||
// Sequentially auth check each control event.
|
||||
let resolved_control = iterative_auth_check(
|
||||
&room_version,
|
||||
sorted_control_levels.iter().stream().map(AsRef::as_ref),
|
||||
sorted_control_levels.iter().stream().map(ToOwned::to_owned),
|
||||
initial_state,
|
||||
&event_fetch,
|
||||
)
|
||||
@@ -209,7 +204,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
|
||||
|
||||
let mut resolved_state = iterative_auth_check(
|
||||
&room_version,
|
||||
sorted_left_events.iter().stream().map(AsRef::as_ref),
|
||||
sorted_left_events.iter().stream(),
|
||||
resolved_control, // The control events are added to the final resolved state
|
||||
&event_fetch,
|
||||
)
|
||||
@@ -273,14 +268,12 @@ fn separate<'a, Id>(
|
||||
}
|
||||
|
||||
/// Calculate the conflicted subgraph
|
||||
async fn calculate_conflicted_subgraph<F, Fut, E>(
|
||||
async fn calculate_conflicted_subgraph<FE>(
|
||||
conflicted: &StateMap<Vec<OwnedEventId>>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) -> Option<HashSet<OwnedEventId>>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
FE: AsyncFn(OwnedEventId) -> Result<Option<Pdu>> + Sync,
|
||||
{
|
||||
let conflicted_events: HashSet<_> = conflicted.values().flatten().cloned().collect();
|
||||
let mut subgraph: HashSet<OwnedEventId> = HashSet::new();
|
||||
@@ -312,7 +305,17 @@ async fn calculate_conflicted_subgraph<F, Fut, E>(
|
||||
continue;
|
||||
}
|
||||
trace!(event_id = event_id.as_str(), "fetching event for its auth events");
|
||||
let evt = fetch_event(event_id.clone()).await;
|
||||
let evt = fetch_event(event_id.clone())
|
||||
.await
|
||||
.inspect_err(|e| {
|
||||
log_error!(
|
||||
"error fetching event {} for conflicted state subgraph: {}",
|
||||
event_id,
|
||||
e
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
if evt.is_none() {
|
||||
err!("could not fetch event {} to calculate conflicted subgraph", event_id);
|
||||
path.pop();
|
||||
@@ -359,15 +362,14 @@ fn get_auth_chain_diff<Id, Hasher>(
|
||||
/// The power level is negative because a higher power level is equated to an
|
||||
/// earlier (further back in time) origin server timestamp.
|
||||
#[tracing::instrument(level = "debug", skip_all)]
|
||||
async fn reverse_topological_power_sort<E, F, Fut>(
|
||||
async fn reverse_topological_power_sort<FE, FR>(
|
||||
events_to_sort: Vec<OwnedEventId>,
|
||||
auth_diff: &HashSet<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) -> Result<Vec<OwnedEventId>>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
debug!("reverse topological sort of power events");
|
||||
|
||||
@@ -404,8 +406,8 @@ async fn reverse_topological_power_sort<E, F, Fut>(
|
||||
.get(&event_id)
|
||||
.ok_or_else(|| Error::NotFound(String::new()))?;
|
||||
|
||||
let ev = fetch_event(event_id)
|
||||
.await
|
||||
let ev = fetch_event(&event_id)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(String::new()))?;
|
||||
|
||||
Ok((pl, ev.origin_server_ts()))
|
||||
@@ -544,18 +546,14 @@ fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other))
|
||||
/// Do NOT use this any where but topological sort, we find the power level for
|
||||
/// the eventId at the eventId's generation (we walk backwards to `EventId`s
|
||||
/// most recent previous power level event).
|
||||
async fn get_power_level_for_sender<E, F, Fut>(
|
||||
event_id: &EventId,
|
||||
fetch_event: &F,
|
||||
) -> serde_json::Result<Int>
|
||||
async fn get_power_level_for_sender<FE, FR>(event_id: &EventId, fetch_event: &FE) -> Result<Int>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
debug!("fetch event ({event_id}) senders power level");
|
||||
|
||||
let event = fetch_event(event_id.to_owned()).await;
|
||||
let event = fetch_event(event_id).await?;
|
||||
|
||||
let auth_events = event.as_ref().map(Event::auth_events);
|
||||
|
||||
@@ -563,7 +561,7 @@ async fn get_power_level_for_sender<E, F, Fut>(
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.stream()
|
||||
.broadn_filter_map(5, |aid| fetch_event(aid.to_owned()))
|
||||
.broad_filter_map(|aid| fetch_event(aid).unwrap_or_default())
|
||||
.ready_find(|aev| is_type_and_key(aev, &TimelineEventType::RoomPowerLevels, ""))
|
||||
.await;
|
||||
|
||||
@@ -594,27 +592,24 @@ async fn get_power_level_for_sender<E, F, Fut>(
|
||||
/// the the `fetch_event` closure and verify each event using the
|
||||
/// `event_auth::auth_check` function.
|
||||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
async fn iterative_auth_check<FE, FR, S>(
|
||||
room_version: &RoomVersion,
|
||||
events_to_check: S,
|
||||
unconflicted_state: StateMap<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) -> Result<StateMap<OwnedEventId>>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
S: Stream<Item = &'a EventId> + Send + 'a,
|
||||
E: Event + Clone + Send + Sync,
|
||||
for<'b> &'b E: Event + Send,
|
||||
FE: Fn(&EventId) -> FR,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send + Sync,
|
||||
S: Stream<Item = OwnedEventId> + Send,
|
||||
{
|
||||
debug!("starting iterative auth check");
|
||||
|
||||
let events_to_check: Vec<_> = events_to_check
|
||||
.map(Result::Ok)
|
||||
.broad_and_then(async |event_id| {
|
||||
fetch_event(event_id.to_owned())
|
||||
.await
|
||||
.ok_or_else(|| Error::NotFound(format!("Failed to find {event_id}")))
|
||||
.map(Ok::<OwnedEventId, Error>)
|
||||
.broad_and_then(async |event_id| match fetch_event(&event_id).await {
|
||||
| Ok(Some(e)) => Ok(e),
|
||||
| _ => Err(Error::NotFound(format!("could not find {event_id}")))?,
|
||||
})
|
||||
.try_collect()
|
||||
.boxed()
|
||||
@@ -627,16 +622,20 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
|
||||
let auth_event_ids: HashSet<OwnedEventId> = events_to_check
|
||||
.iter()
|
||||
.flat_map(|event: &E| event.auth_events().map(ToOwned::to_owned))
|
||||
.flat_map(|event: &Pdu| event.auth_events().map(ToOwned::to_owned))
|
||||
.collect();
|
||||
|
||||
trace!(set = ?auth_event_ids, "auth event IDs to fetch");
|
||||
|
||||
let auth_events: HashMap<OwnedEventId, E> = auth_event_ids
|
||||
let auth_events: HashMap<OwnedEventId, Pdu> = auth_event_ids
|
||||
.into_iter()
|
||||
.stream()
|
||||
.broad_filter_map(fetch_event)
|
||||
.map(|auth_event| (auth_event.event_id().to_owned(), auth_event))
|
||||
.broad_filter_map(async |event_id| {
|
||||
fetch_event(&event_id)
|
||||
.await
|
||||
.map(|ev_opt| ev_opt.map(|ev| (event_id.clone(), ev)))
|
||||
.unwrap_or_default()
|
||||
})
|
||||
.collect()
|
||||
.boxed()
|
||||
.await;
|
||||
@@ -655,29 +654,23 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
.state_key()
|
||||
.ok_or_else(|| Error::InvalidPdu("State event had no state key".to_owned()))?;
|
||||
|
||||
let member_event_content = match event.kind() {
|
||||
| TimelineEventType::RoomMember =>
|
||||
Some(event.get_content::<RoomMemberEventContent>().map_err(|e| {
|
||||
Error::InvalidPdu(format!("Failed to parse m.room.member content: {}", e))
|
||||
})?),
|
||||
| _ => None,
|
||||
};
|
||||
let auth_types = auth_types_for_event(
|
||||
event.event_type(),
|
||||
event.sender(),
|
||||
Some(state_key),
|
||||
event.content(),
|
||||
room_version,
|
||||
event.kind(),
|
||||
event.state_key().map(StateKey::from_str).as_ref(),
|
||||
event.sender(),
|
||||
member_event_content,
|
||||
)?;
|
||||
trace!(list = ?auth_types, event_id = event.event_id().as_str(), "auth types for event");
|
||||
|
||||
let mut auth_state = StateMap::new();
|
||||
if room_version.room_ids_as_hashes {
|
||||
trace!("room version uses hashed IDs, manually fetching create event");
|
||||
let create_event_id_raw = event.room_id_or_hash().as_str().replace('!', "$");
|
||||
let create_event_id = EventId::parse(&create_event_id_raw).map_err(|e| {
|
||||
Error::InvalidPdu(format!(
|
||||
"Failed to parse create event ID from room ID/hash: {e}"
|
||||
))
|
||||
})?;
|
||||
let create_event = fetch_event(create_event_id.into())
|
||||
.await
|
||||
.ok_or_else(|| Error::NotFound("Failed to find create event".into()))?;
|
||||
auth_state.insert(create_event.event_type().with_state_key(""), create_event);
|
||||
}
|
||||
let mut auth_state = StateMap::with_capacity(event.auth_events.len());
|
||||
for aid in event.auth_events() {
|
||||
if let Some(ev) = auth_events.get(aid) {
|
||||
//TODO: synapse checks "rejected_reason" which is most likely related to
|
||||
@@ -703,7 +696,13 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
if let Some(event) = auth_events.get(ev_id) {
|
||||
Some((key, event.clone()))
|
||||
} else {
|
||||
Some((key, fetch_event(ev_id.clone()).await?))
|
||||
match fetch_event(ev_id).await {
|
||||
| Ok(Some(event)) => Some((key, event)),
|
||||
| _ => {
|
||||
warn!(event_id = ev_id.as_str(), "unable to fetch auth event");
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
.ready_for_each(|(key, event)| {
|
||||
@@ -715,30 +714,16 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
|
||||
debug!(event_id = event.event_id().as_str(), "Running auth checks");
|
||||
|
||||
// The key for this is (eventType + a state_key of the signed token not sender)
|
||||
// so search for it
|
||||
let current_third_party = auth_state.iter().find_map(|(_, pdu)| {
|
||||
(*pdu.event_type() == TimelineEventType::RoomThirdPartyInvite).then_some(pdu)
|
||||
});
|
||||
|
||||
let fetch_state = |ty: &StateEventType, key: &str| {
|
||||
future::ready(
|
||||
auth_state
|
||||
.get(&ty.with_state_key(key))
|
||||
.map(ToOwned::to_owned),
|
||||
)
|
||||
let fetch_state = async |t: (StateEventType, StateKey)| {
|
||||
Ok(auth_state
|
||||
.get(&t.0.with_state_key(t.1.as_str()))
|
||||
.map(ToOwned::to_owned))
|
||||
};
|
||||
|
||||
let auth_result = auth_check(
|
||||
room_version,
|
||||
&event,
|
||||
current_third_party,
|
||||
fetch_state,
|
||||
&fetch_state(&StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.expect("create event must exist"),
|
||||
)
|
||||
.await;
|
||||
let create_event = fetch_state((StateEventType::RoomCreate, StateKey::new())).await?;
|
||||
let auth_result =
|
||||
auth_check(room_version, &event, fetch_event, &fetch_state, create_event.as_ref())
|
||||
.await;
|
||||
|
||||
match auth_result {
|
||||
| Ok(true) => {
|
||||
@@ -758,7 +743,7 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
warn!("event {} failed the authentication check", event.event_id());
|
||||
},
|
||||
| Err(e) => {
|
||||
debug_error!("event {} failed the authentication check: {e}", event.event_id());
|
||||
log_error!("event {} failed the authentication check: {e}", event.event_id());
|
||||
return Err(e);
|
||||
},
|
||||
}
|
||||
@@ -777,15 +762,14 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
/// after the most recent are depth 0, the events before (with the first power
|
||||
/// level as a parent) will be marked as depth 1. depth 1 is "older" than depth
|
||||
/// 0.
|
||||
async fn mainline_sort<E, F, Fut>(
|
||||
async fn mainline_sort<FE, FR>(
|
||||
to_sort: &[OwnedEventId],
|
||||
resolved_power_level: Option<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) -> Result<Vec<OwnedEventId>>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Clone + Send + Sync,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
debug!("mainline sort of events");
|
||||
|
||||
@@ -799,14 +783,14 @@ async fn mainline_sort<E, F, Fut>(
|
||||
while let Some(p) = pl {
|
||||
mainline.push(p.clone());
|
||||
|
||||
let event = fetch_event(p.clone())
|
||||
.await
|
||||
let event = fetch_event(&p)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Failed to find {p}")))?;
|
||||
|
||||
pl = None;
|
||||
for aid in event.auth_events() {
|
||||
let ev = fetch_event(aid.to_owned())
|
||||
.await
|
||||
let ev = fetch_event(aid)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
|
||||
|
||||
if is_type_and_key(&ev, &TimelineEventType::RoomPowerLevels, "") {
|
||||
@@ -827,7 +811,11 @@ async fn mainline_sort<E, F, Fut>(
|
||||
.iter()
|
||||
.stream()
|
||||
.broad_filter_map(async |ev_id| {
|
||||
fetch_event(ev_id.clone()).await.map(|event| (event, ev_id))
|
||||
fetch_event(ev_id)
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.map(|event| (event, ev_id))
|
||||
})
|
||||
.broad_filter_map(|(event, ev_id)| {
|
||||
get_mainline_depth(Some(event.clone()), &mainline_map, fetch_event)
|
||||
@@ -849,15 +837,14 @@ async fn mainline_sort<E, F, Fut>(
|
||||
|
||||
/// Get the mainline depth from the `mainline_map` or finds a power_level event
|
||||
/// that has an associated mainline depth.
|
||||
async fn get_mainline_depth<E, F, Fut>(
|
||||
mut event: Option<E>,
|
||||
async fn get_mainline_depth<FE, FR>(
|
||||
mut event: Option<Pdu>,
|
||||
mainline_map: &HashMap<OwnedEventId, usize>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) -> Result<usize>
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
while let Some(sort_ev) = event {
|
||||
debug!(event_id = sort_ev.event_id().as_str(), "mainline");
|
||||
@@ -869,8 +856,8 @@ async fn get_mainline_depth<E, F, Fut>(
|
||||
|
||||
event = None;
|
||||
for aid in sort_ev.auth_events() {
|
||||
let aev = fetch_event(aid.to_owned())
|
||||
.await
|
||||
let aev = fetch_event(aid)
|
||||
.await?
|
||||
.ok_or_else(|| Error::NotFound(format!("Failed to find {aid}")))?;
|
||||
|
||||
if is_type_and_key(&aev, &TimelineEventType::RoomPowerLevels, "") {
|
||||
@@ -883,20 +870,19 @@ async fn get_mainline_depth<E, F, Fut>(
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
async fn add_event_and_auth_chain_to_graph<E, F, Fut>(
|
||||
async fn add_event_and_auth_chain_to_graph<FE, FR>(
|
||||
graph: &mut HashMap<OwnedEventId, HashSet<OwnedEventId>>,
|
||||
event_id: OwnedEventId,
|
||||
auth_diff: &HashSet<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
fetch_event: &FE,
|
||||
) where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send + Sync,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
let mut state = vec![event_id];
|
||||
while let Some(eid) = state.pop() {
|
||||
graph.entry(eid.clone()).or_default();
|
||||
let event = fetch_event(eid.clone()).await;
|
||||
let event = fetch_event(&eid).await.ok().flatten();
|
||||
let auth_events = event.as_ref().map(Event::auth_events).into_iter().flatten();
|
||||
|
||||
// Prefer the store to event as the store filters dedups the events
|
||||
@@ -915,14 +901,13 @@ async fn add_event_and_auth_chain_to_graph<E, F, Fut>(
|
||||
}
|
||||
}
|
||||
|
||||
async fn is_power_event_id<E, F, Fut>(event_id: &EventId, fetch: &F) -> bool
|
||||
async fn is_power_event_id<FE, FR>(event_id: &EventId, fetch: &FE) -> bool
|
||||
where
|
||||
F: Fn(OwnedEventId) -> Fut + Sync,
|
||||
Fut: Future<Output = Option<E>> + Send,
|
||||
E: Event + Send,
|
||||
FE: Fn(&EventId) -> FR + Sync,
|
||||
FR: Future<Output = Result<Option<Pdu>, Error>> + Send,
|
||||
{
|
||||
match fetch(event_id.to_owned()).await.as_ref() {
|
||||
| Some(state) => is_power_event(state),
|
||||
match fetch(event_id).await.as_ref() {
|
||||
| Ok(Some(state)) => is_power_event(state),
|
||||
| _ => false,
|
||||
}
|
||||
}
|
||||
@@ -979,26 +964,27 @@ fn with_state_key(self, state_key: impl Into<StateKey>) -> (StateEventType, Stat
|
||||
mod tests {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use itertools::Itertools;
|
||||
use maplit::{hashmap, hashset};
|
||||
use rand::seq::SliceRandom;
|
||||
use ruma::{
|
||||
MilliSecondsSinceUnixEpoch, OwnedEventId, RoomVersionId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent},
|
||||
},
|
||||
int, uint,
|
||||
room::join_rules::{JoinRule, RoomJoinRulesEventContent}, StateEventType,
|
||||
TimelineEventType,
|
||||
}, int, uint,
|
||||
MilliSecondsSinceUnixEpoch,
|
||||
OwnedEventId, RoomVersionId,
|
||||
};
|
||||
use serde_json::{json, value::to_raw_value as to_raw_json_value};
|
||||
|
||||
use super::{
|
||||
StateMap, is_power_event,
|
||||
room_version::RoomVersion,
|
||||
is_power_event, room_version::RoomVersion,
|
||||
test_utils::{
|
||||
INITIAL_EVENTS, TestStore, alice, bob, charlie, do_check, ella, event_id,
|
||||
member_content_ban, member_content_join, room_id, to_init_pdu_event, to_pdu_event,
|
||||
zara,
|
||||
alice, bob, charlie, do_check, ella, event_id, member_content_ban, member_content_join,
|
||||
room_id, to_init_pdu_event, to_pdu_event, zara, TestStore,
|
||||
INITIAL_EVENTS,
|
||||
},
|
||||
StateMap,
|
||||
};
|
||||
use crate::{
|
||||
debug,
|
||||
@@ -1028,13 +1014,13 @@ async fn test_event_sort() {
|
||||
.map(|pdu| pdu.event_id.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let fetcher = |id| ready(events.get(&id).cloned());
|
||||
let fetcher = |id| ready(Ok(events.get(id).cloned()));
|
||||
let sorted_power_events =
|
||||
super::reverse_topological_power_sort(power_events, &auth_chain, &fetcher)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resolved_power = super::iterative_auth_check(
|
||||
let resolved_power = super::auth_check(
|
||||
&RoomVersion::V6,
|
||||
sorted_power_events.iter().map(AsRef::as_ref).stream(),
|
||||
HashMap::new(), // unconflicted events
|
||||
|
||||
@@ -33,8 +33,6 @@ pub struct Engine {
|
||||
pub(crate) db: Db,
|
||||
pub(crate) pool: Arc<Pool>,
|
||||
pub(crate) ctx: Arc<Context>,
|
||||
pub(super) read_only: bool,
|
||||
pub(super) secondary: bool,
|
||||
pub(crate) checksums: bool,
|
||||
corks: AtomicU32,
|
||||
}
|
||||
@@ -129,14 +127,6 @@ pub fn current_sequence(&self) -> u64 {
|
||||
|
||||
sequence
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_read_only(&self) -> bool { self.secondary || self.read_only }
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_secondary(&self) -> bool { self.secondary }
|
||||
}
|
||||
|
||||
impl Drop for Engine {
|
||||
|
||||
@@ -12,9 +12,8 @@ pub fn backup(&self) -> Result {
|
||||
let mut engine = self.backup_engine()?;
|
||||
let config = &self.ctx.server.config;
|
||||
if config.database_backups_to_keep > 0 {
|
||||
let flush = !self.is_read_only();
|
||||
engine
|
||||
.create_new_backup_flush(&self.db, flush)
|
||||
.create_new_backup_flush(&self.db, true)
|
||||
.map_err(map_err)?;
|
||||
|
||||
let engine_info = engine.get_backup_info();
|
||||
|
||||
@@ -35,14 +35,7 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
|
||||
}
|
||||
|
||||
debug!("Opening database...");
|
||||
let db = if config.rocksdb_read_only {
|
||||
Db::open_cf_descriptors_read_only(&db_opts, path, cfds, false)
|
||||
} else if config.rocksdb_secondary {
|
||||
Db::open_cf_descriptors_as_secondary(&db_opts, path, path, cfds)
|
||||
} else {
|
||||
Db::open_cf_descriptors(&db_opts, path, cfds)
|
||||
}
|
||||
.or_else(or_else)?;
|
||||
let db = Db::open_cf_descriptors(&db_opts, path, cfds).or_else(or_else)?;
|
||||
|
||||
info!(
|
||||
columns = num_cfds,
|
||||
@@ -55,8 +48,6 @@ pub(crate) async fn open(ctx: Arc<Context>, desc: &[Descriptor]) -> Result<Arc<S
|
||||
db,
|
||||
pool: ctx.pool.clone(),
|
||||
ctx: ctx.clone(),
|
||||
read_only: config.rocksdb_read_only,
|
||||
secondary: config.rocksdb_secondary,
|
||||
checksums: config.rocksdb_checksums,
|
||||
corks: AtomicU32::new(0),
|
||||
}))
|
||||
|
||||
@@ -74,14 +74,6 @@ pub fn iter(&self) -> impl Iterator<Item = (&MapsKey, &MapsVal)> + Send + '_ {
|
||||
|
||||
#[inline]
|
||||
pub fn keys(&self) -> impl Iterator<Item = &MapsKey> + Send + '_ { self.maps.keys() }
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_read_only(&self) -> bool { self.db.is_read_only() }
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn is_secondary(&self) -> bool { self.db.is_secondary() }
|
||||
}
|
||||
|
||||
impl Index<&str> for Database {
|
||||
|
||||
@@ -99,6 +99,11 @@ gzip_compression = [
|
||||
hardened_malloc = [
|
||||
"conduwuit-core/hardened_malloc",
|
||||
]
|
||||
http3 = [
|
||||
"conduwuit-api/http3",
|
||||
"conduwuit-core/http3",
|
||||
"conduwuit-service/http3",
|
||||
]
|
||||
io_uring = [
|
||||
"conduwuit-database/io_uring",
|
||||
]
|
||||
|
||||
@@ -27,10 +27,6 @@ pub struct Args {
|
||||
#[arg(long, short('O'))]
|
||||
pub option: Vec<String>,
|
||||
|
||||
/// Run in a stricter read-only --maintenance mode.
|
||||
#[arg(long)]
|
||||
pub read_only: bool,
|
||||
|
||||
/// Run in maintenance mode while refusing connections.
|
||||
#[arg(long)]
|
||||
pub maintenance: bool,
|
||||
@@ -143,11 +139,7 @@ pub(crate) fn parse() -> Args { Args::parse() }
|
||||
|
||||
/// Synthesize any command line options with configuration file options.
|
||||
pub(crate) fn update(mut config: Figment, args: &Args) -> Result<Figment> {
|
||||
if args.read_only {
|
||||
config = config.join(("rocksdb_read_only", true));
|
||||
}
|
||||
|
||||
if args.maintenance || args.read_only {
|
||||
if args.maintenance {
|
||||
config = config.join(("startup_netburst", false));
|
||||
config = config.join(("listening", false));
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ gzip_compression = [
|
||||
"conduwuit-core/gzip_compression",
|
||||
"reqwest/gzip",
|
||||
]
|
||||
http3 = [
|
||||
"conduwuit-core/http3",
|
||||
]
|
||||
io_uring = [
|
||||
"conduwuit-database/io_uring",
|
||||
]
|
||||
|
||||
@@ -37,10 +37,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
}
|
||||
|
||||
async fn worker(self: Arc<Self>) -> Result {
|
||||
if self.services.globals.is_read_only() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.services.config.ldap.enable {
|
||||
warn!("emergency password feature not available with LDAP enabled.");
|
||||
return Ok(());
|
||||
|
||||
@@ -156,7 +156,4 @@ pub fn user_is_local(&self, user_id: &UserId) -> bool {
|
||||
pub fn server_is_ours(&self, server_name: &ServerName) -> bool {
|
||||
server_name == self.server_name()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn is_read_only(&self) -> bool { self.db.db.is_read_only() }
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use conduwuit::{
|
||||
Err, Event, Result, Server, err,
|
||||
Err, Event, Result, err,
|
||||
utils::{ReadyExt, stream::TryIgnore},
|
||||
};
|
||||
use database::{Deserialized, Ignore, Interfix, Map};
|
||||
@@ -30,12 +30,12 @@ struct Data {
|
||||
}
|
||||
|
||||
struct Services {
|
||||
server: Arc<Server>,
|
||||
admin: Dep<admin::Service>,
|
||||
appservice: Dep<appservice::Service>,
|
||||
globals: Dep<globals::Service>,
|
||||
sending: Dep<sending::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
@@ -47,13 +47,13 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
aliasid_alias: args.db["aliasid_alias"].clone(),
|
||||
},
|
||||
services: Services {
|
||||
server: args.server.clone(),
|
||||
admin: args.depend::<admin::Service>("admin"),
|
||||
appservice: args.depend::<appservice::Service>("appservice"),
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
sending: args.depend::<sending::Service>("sending"),
|
||||
state_accessor: args
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||
},
|
||||
}))
|
||||
}
|
||||
@@ -117,6 +117,9 @@ pub async fn remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Resul
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolves the given room ID or alias, returning the resolved room ID.
|
||||
/// Unlike resolve_with_servers (the underlying call), potential resident
|
||||
/// servers are not returned
|
||||
#[inline]
|
||||
pub async fn resolve(&self, room: &RoomOrAliasId) -> Result<OwnedRoomId> {
|
||||
self.resolve_with_servers(room, None)
|
||||
@@ -124,6 +127,14 @@ pub async fn resolve(&self, room: &RoomOrAliasId) -> Result<OwnedRoomId> {
|
||||
.map(|(room_id, _)| room_id)
|
||||
}
|
||||
|
||||
/// Resolves the given room ID or alias, returning the resolved room ID, and
|
||||
/// any servers that might be able to assist in fetching room data.
|
||||
///
|
||||
/// If the input is a room ID, this simply returns it and <servers>.
|
||||
/// If the input is an alias, this attempts to resolve it locally, then via
|
||||
/// appservices, and finally remotely if the alias is not local.
|
||||
/// If the alias is successfully resolved, the room ID and an empty list of
|
||||
/// servers is returned.
|
||||
pub async fn resolve_with_servers(
|
||||
&self,
|
||||
room: &RoomOrAliasId,
|
||||
@@ -134,28 +145,26 @@ pub async fn resolve_with_servers(
|
||||
Ok((room_id.to_owned(), servers.unwrap_or_default()))
|
||||
} else {
|
||||
let alias: &RoomAliasId = room.try_into().expect("valid RoomAliasId");
|
||||
self.resolve_alias(alias, servers).await
|
||||
self.resolve_alias(alias).await
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves the given room alias, returning the resolved room ID and any
|
||||
/// servers that might be in the room.
|
||||
#[tracing::instrument(skip(self), name = "resolve")]
|
||||
pub async fn resolve_alias(
|
||||
&self,
|
||||
room_alias: &RoomAliasId,
|
||||
servers: Option<Vec<OwnedServerName>>,
|
||||
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
|
||||
let server_name = room_alias.server_name();
|
||||
let server_is_ours = self.services.globals.server_is_ours(server_name);
|
||||
let servers_contains_ours = || {
|
||||
servers
|
||||
.as_ref()
|
||||
.is_some_and(|servers| servers.contains(&self.services.server.name))
|
||||
};
|
||||
let server_is_ours = self
|
||||
.services
|
||||
.globals
|
||||
.server_is_ours(room_alias.server_name());
|
||||
|
||||
if !server_is_ours && !servers_contains_ours() {
|
||||
return self
|
||||
.remote_resolve(room_alias, servers.unwrap_or_default())
|
||||
.await;
|
||||
if !server_is_ours {
|
||||
// TODO: The spec advises servers may cache remote room aliases temporarily.
|
||||
// We might want to look at doing that.
|
||||
return self.remote_resolve(room_alias).await;
|
||||
}
|
||||
|
||||
let room_id = match self.resolve_local_alias(room_alias).await {
|
||||
@@ -163,10 +172,18 @@ pub async fn resolve_alias(
|
||||
| Err(_) => self.resolve_appservice_alias(room_alias).await?,
|
||||
};
|
||||
|
||||
room_id.map_or_else(
|
||||
|| Err!(Request(NotFound("Room with alias not found."))),
|
||||
|room_id| Ok((room_id, Vec::new())),
|
||||
)
|
||||
if let Some(room_id) = room_id {
|
||||
let servers: Vec<OwnedServerName> = self
|
||||
.services
|
||||
.state_cache
|
||||
.room_servers(&room_id)
|
||||
.map(ToOwned::to_owned)
|
||||
.collect()
|
||||
.await;
|
||||
return Ok((room_id, servers));
|
||||
}
|
||||
|
||||
Err!(Request(NotFound("Alias does not exist.")))
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
@@ -206,12 +223,12 @@ async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) ->
|
||||
|
||||
// The creator of an alias can remove it
|
||||
if self
|
||||
.who_created_alias(alias).await
|
||||
.is_ok_and(|user| user == user_id)
|
||||
// Server admins can remove any local alias
|
||||
|| self.services.admin.user_is_admin(user_id).await
|
||||
// Always allow the server service account to remove the alias, since there may not be an admin room
|
||||
|| server_user == user_id
|
||||
.who_created_alias(alias).await
|
||||
.is_ok_and(|user| user == user_id)
|
||||
// Server admins can remove any local alias
|
||||
|| self.services.admin.user_is_admin(user_id).await
|
||||
// Always allow the server service account to remove the alias, since there may not be an admin room
|
||||
|| server_user == user_id
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use std::iter::once;
|
||||
|
||||
use conduwuit::{Result, debug, debug_error, err, implement};
|
||||
use conduwuit::{Result, debug, error, implement};
|
||||
use federation::query::get_room_information::v1::Response;
|
||||
use ruma::{OwnedRoomId, OwnedServerName, RoomAliasId, ServerName, api::federation};
|
||||
|
||||
@@ -8,40 +6,21 @@
|
||||
pub(super) async fn remote_resolve(
|
||||
&self,
|
||||
room_alias: &RoomAliasId,
|
||||
servers: Vec<OwnedServerName>,
|
||||
) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
|
||||
debug!(?room_alias, servers = ?servers, "resolve");
|
||||
let servers = once(room_alias.server_name())
|
||||
.map(ToOwned::to_owned)
|
||||
.chain(servers.into_iter());
|
||||
|
||||
let mut resolved_servers = Vec::new();
|
||||
let mut resolved_room_id: Option<OwnedRoomId> = None;
|
||||
for server in servers {
|
||||
match self.remote_request(room_alias, &server).await {
|
||||
| Err(e) => debug_error!("Failed to query for {room_alias:?} from {server}: {e}"),
|
||||
| Ok(Response { room_id, servers }) => {
|
||||
debug!(
|
||||
"Server {server} answered with {room_id:?} for {room_alias:?} servers: \
|
||||
{servers:?}"
|
||||
);
|
||||
|
||||
resolved_room_id.get_or_insert(room_id);
|
||||
add_server(&mut resolved_servers, server);
|
||||
|
||||
if !servers.is_empty() {
|
||||
add_servers(&mut resolved_servers, servers);
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
debug!("Asking {} to resolve {room_alias:?}", room_alias.server_name());
|
||||
match self
|
||||
.remote_request(room_alias, room_alias.server_name())
|
||||
.await
|
||||
{
|
||||
| Err(e) => {
|
||||
error!("Unable to resolve remote room alias {}: {e}", room_alias);
|
||||
Err(e)
|
||||
},
|
||||
| Ok(Response { room_id, servers }) => {
|
||||
debug!("Remote resolved {room_alias:?} to {room_id:?} with servers {servers:?}");
|
||||
Ok((room_id, servers))
|
||||
},
|
||||
}
|
||||
|
||||
resolved_room_id
|
||||
.map(|room_id| (room_id, resolved_servers))
|
||||
.ok_or_else(|| {
|
||||
err!(Request(NotFound("No servers could assist in resolving the room alias")))
|
||||
})
|
||||
}
|
||||
|
||||
#[implement(super::Service)]
|
||||
@@ -59,15 +38,3 @@ async fn remote_request(
|
||||
.send_federation_request(server, request)
|
||||
.await
|
||||
}
|
||||
|
||||
fn add_servers(servers: &mut Vec<OwnedServerName>, new: Vec<OwnedServerName>) {
|
||||
for server in new {
|
||||
add_server(servers, server);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_server(servers: &mut Vec<OwnedServerName>, server: OwnedServerName) {
|
||||
if !servers.contains(&server) {
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,12 @@ pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Re
|
||||
})
|
||||
.boxed();
|
||||
|
||||
let mut federated_room = false;
|
||||
|
||||
while let Some(ref backfill_server) = servers.next().await {
|
||||
if !self.services.globals.server_is_ours(backfill_server) {
|
||||
federated_room = true;
|
||||
}
|
||||
info!("Asking {backfill_server} for backfill in {room_id}");
|
||||
let response = self
|
||||
.services
|
||||
@@ -168,7 +173,9 @@ pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Re
|
||||
}
|
||||
}
|
||||
|
||||
warn!("No servers could backfill, but backfill was needed in room {room_id}");
|
||||
if federated_room {
|
||||
warn!("No servers could backfill, but backfill was needed in room {room_id}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -139,7 +139,7 @@ pub async fn start(self: &Arc<Self>) -> Result<Arc<Self>> {
|
||||
|
||||
// reset dormant online/away statuses to offline, and set the server user as
|
||||
// online
|
||||
if self.server.config.allow_local_presence && !self.db.is_read_only() {
|
||||
if self.server.config.allow_local_presence {
|
||||
self.presence.unset_all_presence().await;
|
||||
_ = self
|
||||
.presence
|
||||
@@ -156,7 +156,7 @@ pub async fn stop(&self) {
|
||||
info!("Shutting down services...");
|
||||
|
||||
// set the server user as offline
|
||||
if self.server.config.allow_local_presence && !self.db.is_read_only() {
|
||||
if self.server.config.allow_local_presence {
|
||||
_ = self
|
||||
.presence
|
||||
.ping_presence(&self.globals.server_user, &ruma::presence::PresenceState::Offline)
|
||||
|
||||
Reference in New Issue
Block a user