Merge branch 'next' into 'master'

Merge remote-tracking branch 'origin/next'

See merge request famedly/conduit!538
This commit is contained in:
Timo Kösters 2023-08-10 17:01:56 +00:00
commit 0c2cfda3ae
93 changed files with 6911 additions and 2870 deletions

1
.gitignore vendored
View File

@ -61,6 +61,7 @@ conduit.db
# Etc.
**/*.rs.bk
cached_target
# Nix artifacts
/result*

View File

@ -16,16 +16,16 @@ variables:
.docker-shared-settings:
stage: "build docker image"
image: jdrouet/docker-with-buildx:20.10.21-0.9.1
needs: []
tags: ["docker"]
tags: [ "docker" ]
variables:
# Docker in Docker:
DOCKER_HOST: tcp://docker:2375/
DOCKER_TLS_CERTDIR: ""
DOCKER_DRIVER: overlay2
DOCKER_BUILDKIT: 1
image:
name: docker.io/docker
services:
- docker:dind
- name: docker.io/docker:dind
alias: docker
script:
- apk add openssh-client
- eval $(ssh-agent -s)
@ -41,6 +41,7 @@ variables:
--pull
--tag "$CI_REGISTRY_IMAGE/temporary-ci-images:$CI_JOB_ID"
--push
--provenance=false
--file "Dockerfile" .
# Build multiplatform image to deb stage and extract their .deb files:
- >
@ -48,6 +49,7 @@ variables:
--platform "linux/arm/v7,linux/arm64,linux/amd64"
--target "packager-result"
--output="type=local,dest=/tmp/build-output"
--provenance=false
--file "Dockerfile" .
# Build multiplatform image to binary stage and extract their binaries:
- >
@ -55,6 +57,7 @@ variables:
--platform "linux/arm/v7,linux/arm64,linux/amd64"
--target "builder-result"
--output="type=local,dest=/tmp/build-output"
--provenance=false
--file "Dockerfile" .
# Copy to GitLab container registry:
- >
@ -100,13 +103,20 @@ docker:tags:
TAG: "matrix-conduit:$CI_COMMIT_TAG"
docker build debugging:
extends: .docker-shared-settings
rules:
- if: "$CI_MERGE_REQUEST_TITLE =~ /.*[Dd]ocker.*/"
variables:
TAG: "matrix-conduit-docker-tests:latest"
# --------------------------------------------------------------------- #
# Run tests #
# --------------------------------------------------------------------- #
cargo check:
stage: test
image: docker.io/rust:1.64.0-bullseye
image: docker.io/rust:1.70.0-bullseye
needs: []
interruptible: true
before_script:
@ -131,11 +141,7 @@ test:cargo:
- apt-get update && apt-get -y --no-install-recommends install libclang-dev # dependency for rocksdb
script:
- rustc --version && cargo --version # Print version info for debugging
- "cargo test --color always --workspace --verbose --locked --no-fail-fast -- -Z unstable-options --format json | gitlab-report -p test > $CI_PROJECT_DIR/report.xml"
artifacts:
when: always
reports:
junit: report.xml
- "cargo test --color always --workspace --verbose --locked --no-fail-fast"
test:clippy:
extends: .test-shared-settings

134
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,134 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
* Focusing on what is best not just for us as individuals, but for the overall
community
Examples of unacceptable behavior include:
* The use of sexualized language or imagery, and sexual attention or advances of
any kind
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email address,
without their explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement over email at
coc@koesters.xyz or over Matrix at @timo:conduit.rs.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series of
actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within the
community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

1427
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -6,98 +6,109 @@ authors = ["timokoesters <timo@koesters.xyz>"]
homepage = "https://conduit.rs"
repository = "https://gitlab.com/famedly/conduit"
readme = "README.md"
version = "0.5.0"
rust-version = "1.64"
version = "0.6.0"
edition = "2021"
# When changing this, make sure to update the `flake.lock` file by running
# `nix flake update`. If you don't have Nix installed or otherwise don't know
# how to do this, ping `@charles:computer.surgery` or `@dusk:gaze.systems` in
# the matrix room.
rust-version = "1.70.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
# Web framework
axum = { version = "0.5.17", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path"], optional = true }
axum-server = { version = "0.4.0", features = ["tls-rustls"] }
tower = { version = "0.4.8", features = ["util"] }
tower-http = { version = "0.3.4", features = ["add-extension", "cors", "compression-full", "sensitive-headers", "trace", "util"] }
axum = { version = "0.6.18", default-features = false, features = ["form", "headers", "http1", "http2", "json", "matched-path"], optional = true }
axum-server = { version = "0.5.1", features = ["tls-rustls"] }
tower = { version = "0.4.13", features = ["util"] }
tower-http = { version = "0.4.1", features = ["add-extension", "cors", "sensitive-headers", "trace", "util"] }
# Used for matrix spec type definitions and helpers
#ruma = { version = "0.4.0", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "67d0f3cc04a8d1dc4a8a1ec947519967ce11ce26", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "50c1db7e0a3a21fc794b0cce3b64285a4c750c71", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-pre-spec", "unstable-exhaustive-types"] }
ruma = { git = "https://github.com/ruma/ruma", rev = "3bd58e3c899457c2d55c45268dcb8a65ae682d54", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { git = "https://github.com/timokoesters/ruma", rev = "4ec9c69bb7e09391add2382b3ebac97b6e8f4c64", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
#ruma = { path = "../ruma/crates/ruma", features = ["compat", "rand", "appservice-api-c", "client-api", "federation-api", "push-gateway-api-c", "state-res", "unstable-msc2448", "unstable-msc3575", "unstable-exhaustive-types", "ring-compat", "unstable-unspecified" ] }
# Async runtime and utilities
tokio = { version = "1.11.0", features = ["fs", "macros", "signal", "sync"] }
tokio = { version = "1.28.1", features = ["fs", "macros", "signal", "sync"] }
# Used for storing data permanently
#sled = { version = "0.34.7", features = ["compression", "no_metrics"], optional = true }
#sled = { git = "https://github.com/spacejam/sled.git", rev = "e4640e0773595229f398438886f19bca6f7326a2", features = ["compression"] }
persy = { version = "1.0.0", optional = true, features = ["background_ops"] }
persy = { version = "1.4.4", optional = true, features = ["background_ops"] }
# Used for the http request / response body type for Ruma endpoints used with reqwest
bytes = "1.1.0"
http = "0.2.4"
bytes = "1.4.0"
http = "0.2.9"
# Used to find data directory for default db path
directories = "4.0.0"
directories = "4.0.1"
# Used for ruma wrapper
serde_json = { version = "1.0.68", features = ["raw_value"] }
serde_json = { version = "1.0.96", features = ["raw_value"] }
# Used for appservice registration files
serde_yaml = "0.9.13"
serde_yaml = "0.9.21"
# Used for pdu definition
serde = { version = "1.0.130", features = ["rc"] }
serde = { version = "1.0.163", features = ["rc"] }
# Used for secure identifiers
rand = "0.8.4"
rand = "0.8.5"
# Used to hash passwords
rust-argon2 = "1.0.0"
# Used to send requests
reqwest = { default-features = false, features = ["rustls-tls-native-roots", "socks"], git = "https://github.com/timokoesters/reqwest", rev = "57b7cf4feb921573dfafad7d34b9ac6e44ead0bd" }
# Used for conduit::Error type
thiserror = "1.0.29"
thiserror = "1.0.40"
# Used to generate thumbnails for images
image = { version = "0.24.4", default-features = false, features = ["jpeg", "png", "gif"] }
image = { version = "0.24.6", default-features = false, features = ["jpeg", "png", "gif"] }
# Used to encode server public key
base64 = "0.13.0"
base64 = "0.21.2"
# Used when hashing the state
ring = "0.16.20"
# Used when querying the SRV record of other servers
trust-dns-resolver = "0.22.0"
# Used to find matching events for appservices
regex = "1.5.4"
regex = "1.8.1"
# jwt jsonwebtokens
jsonwebtoken = "8.1.1"
jsonwebtoken = "8.3.0"
# Performance measurements
tracing = { version = "0.1.27", features = [] }
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
tracing = { version = "0.1.37", features = [] }
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
tracing-flame = "0.2.0"
opentelemetry = { version = "0.18.0", features = ["rt-tokio"] }
opentelemetry-jaeger = { version = "0.17.0", features = ["rt-tokio"] }
tracing-opentelemetry = "0.18.0"
lru-cache = "0.1.2"
rusqlite = { version = "0.28.0", optional = true, features = ["bundled"] }
rusqlite = { version = "0.29.0", optional = true, features = ["bundled"] }
parking_lot = { version = "0.12.1", optional = true }
crossbeam = { version = "0.8.1", optional = true }
num_cpus = "1.13.0"
crossbeam = { version = "0.8.2", optional = true }
num_cpus = "1.15.0"
threadpool = "1.8.1"
heed = { git = "https://github.com/timokoesters/heed.git", rev = "f6f825da7fb2c758867e05ad973ef800a6fe1d5d", optional = true }
rocksdb = { version = "0.17.0", default-features = true, features = ["multi-threaded-cf", "zstd"], optional = true }
# Used for ruma wrapper
serde_html_form = "0.2.0"
thread_local = "1.1.3"
rocksdb = { version = "0.21.0", default-features = true, features = ["multi-threaded-cf", "zstd"], optional = true }
thread_local = "1.1.7"
# used for TURN server authentication
hmac = "0.12.1"
sha-1 = "0.10.0"
sha-1 = "0.10.1"
# used for conduit's CLI and admin room command parsing
clap = { version = "4.0.11", default-features = false, features = ["std", "derive", "help", "usage", "error-context"] }
futures-util = { version = "0.3.17", default-features = false }
clap = { version = "4.3.0", default-features = false, features = ["std", "derive", "help", "usage", "error-context"] }
futures-util = { version = "0.3.28", default-features = false }
# Used for reading the configuration from conduit.toml & environment variables
figment = { version = "0.10.6", features = ["env", "toml"] }
figment = { version = "0.10.8", features = ["env", "toml"] }
tikv-jemalloc-ctl = { version = "0.5.0", features = ["use_std"], optional = true }
tikv-jemallocator = { version = "0.5.0", features = ["unprefixed_malloc_on_supported_platforms"], optional = true }
lazy_static = "1.4.0"
async-trait = "0.1.57"
async-trait = "0.1.68"
sd-notify = { version = "0.4.1", optional = true }
[target.'cfg(unix)'.dependencies]
nix = { version = "0.26.2", features = ["resource"] }
[features]
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "jemalloc", "systemd"]
default = ["conduit_bin", "backend_sqlite", "backend_rocksdb", "systemd"]
#backend_sled = ["sled"]
backend_persy = ["persy", "parking_lot"]
backend_sqlite = ["sqlite"]
@ -129,7 +140,7 @@ instead of a server that has high scalability."""
section = "net"
priority = "optional"
assets = [
["debian/README.Debian", "usr/share/doc/matrix-conduit/", "644"],
["debian/README.md", "usr/share/doc/matrix-conduit/README.Debian", "644"],
["README.md", "usr/share/doc/matrix-conduit/", "644"],
["target/release/conduit", "usr/sbin/matrix-conduit", "755"],
]

View File

@ -1,23 +0,0 @@
[build.env]
# CI uses an S3 endpoint to store sccache artifacts, so their config needs to
# be available in the cross container as well
passthrough = [
"RUSTC_WRAPPER",
"AWS_ACCESS_KEY_ID",
"AWS_SECRET_ACCESS_KEY",
"SCCACHE_BUCKET",
"SCCACHE_ENDPOINT",
"SCCACHE_S3_USE_SSL",
]
[target.aarch64-unknown-linux-musl]
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-aarch64-unknown-linux-musl:latest"
[target.arm-unknown-linux-musleabihf]
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-arm-unknown-linux-musleabihf:latest"
[target.armv7-unknown-linux-musleabihf]
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-armv7-unknown-linux-musleabihf:latest"
[target.x86_64-unknown-linux-musl]
image = "registry.gitlab.com/jfowl/conduit-containers/rust-cross-x86_64-unknown-linux-musl@sha256:b6d689e42f0236c8a38b961bca2a12086018b85ed20e0826310421daf182e2bb"

View File

@ -7,10 +7,10 @@
## Installing Conduit
Although you might be able to compile Conduit for Windows, we do recommend running it on a linux server. We therefore
Although you might be able to compile Conduit for Windows, we do recommend running it on a Linux server. We therefore
only offer Linux binaries.
You may simply download the binary that fits your machine. Run `uname -m` to see what you need. Now copy the right url:
You may simply download the binary that fits your machine. Run `uname -m` to see what you need. Now copy the appropriate url:
| CPU Architecture | Download stable version | Download development version |
| ------------------------------------------- | --------------------------------------------------------------- | ----------------------------------------------------------- |
@ -19,7 +19,7 @@ You may simply download the binary that fits your machine. Run `uname -m` to see
| armv8 / aarch64 | [Binary][armv8-glibc-master] / [.deb][armv8-glibc-master-deb] | [Binary][armv8-glibc-next] / [.deb][armv8-glibc-next-deb] |
These builds were created on and linked against the glibc version shipped with Debian bullseye.
If you use a system with an older glibc version, you might need to compile Conduit yourself.
If you use a system with an older glibc version (e.g. RHEL8), you might need to compile Conduit yourself.
[x84_64-glibc-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_amd64/conduit?job=docker:master
[armv7-glibc-master]: https://gitlab.com/famedly/conduit/-/jobs/artifacts/master/raw/build-output/linux_arm_v7/conduit?job=docker:master
@ -39,12 +39,16 @@ $ sudo wget -O /usr/local/bin/matrix-conduit <url>
$ sudo chmod +x /usr/local/bin/matrix-conduit
```
Alternatively, you may compile the binary yourself
Alternatively, you may compile the binary yourself. First, install any dependencies:
```bash
# Debian
$ sudo apt install libclang-dev build-essential
```
# RHEL
$ sudo dnf install clang
```
Then, `cd` into the source tree of conduit-next and run:
```bash
$ cargo build --release
```
@ -74,10 +78,10 @@ cross build --release --no-default-features --features conduit_bin,backend_rocks
While Conduit can run as any user it is usually better to use dedicated users for different services. This also allows
you to make sure that the file permissions are correctly set up.
In Debian you can use this command to create a Conduit user:
In Debian or RHEL, you can use this command to create a Conduit user:
```bash
sudo adduser --system conduit --no-create-home
sudo adduser --system conduit --group --disabled-login --no-create-home
```
## Forwarding ports in the firewall or the router
@ -86,6 +90,19 @@ Conduit uses the ports 443 and 8448 both of which need to be open in the firewal
If Conduit runs behind a router or in a container and has a different public IP address than the host system these public ports need to be forwarded directly or indirectly to the port mentioned in the config.
## Optional: Avoid port 8448
If Conduit runs behind Cloudflare reverse proxy, which doesn't support port 8448 on free plans, [delegation](https://matrix-org.github.io/synapse/latest/delegate.html) can be set up to have federation traffic routed to port 443:
```apache
# .well-known delegation on Apache
<Files "/.well-known/matrix/server">
ErrorDocument 200 '{"m.server": "your.server.name:443"}'
Header always set Content-Type application/json
Header always set Access-Control-Allow-Origin *
</Files>
```
[SRV DNS record](https://spec.matrix.org/latest/server-server-api/#resolving-server-names) delegation is also [possible](https://www.cloudflare.com/en-gb/learning/dns/dns-records/dns-srv-record/).
## Setting up a systemd service
Now we'll set up a systemd service for Conduit, so it's easy to start/stop Conduit and set it to autostart when your
@ -100,7 +117,7 @@ After=network.target
[Service]
Environment="CONDUIT_CONFIG=/etc/matrix-conduit/conduit.toml"
User=conduit
Group=nogroup
Group=conduit
Restart=always
ExecStart=/usr/local/bin/matrix-conduit
@ -155,7 +172,9 @@ max_request_size = 20_000_000 # in bytes
allow_registration = true
allow_federation = true
allow_check_for_updates = true
# Server to get public keys from. You probably shouldn't change this
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time
@ -168,7 +187,7 @@ address = "127.0.0.1" # This makes sure Conduit can only be reached using the re
## Setting the correct file permissions
As we are using a Conduit specific user we need to allow it to read the config. To do that you can run this command on
Debian:
Debian or RHEL:
```bash
sudo chown -R root:root /etc/matrix-conduit
@ -179,7 +198,7 @@ If you use the default database path you also need to run this:
```bash
sudo mkdir -p /var/lib/matrix-conduit/
sudo chown -R conduit:nogroup /var/lib/matrix-conduit/
sudo chown -R conduit:conduit /var/lib/matrix-conduit/
sudo chmod 700 /var/lib/matrix-conduit/
```
@ -192,6 +211,11 @@ This depends on whether you use Apache, Caddy, Nginx or another web server.
Create `/etc/apache2/sites-enabled/050-conduit.conf` and copy-and-paste this:
```apache
# Requires mod_proxy and mod_proxy_http
#
# On Apache instance compiled from source,
# paste into httpd-ssl.conf or httpd.conf
Listen 8448
<VirtualHost *:443 *:8448>
@ -199,7 +223,7 @@ Listen 8448
ServerName your.server.name # EDIT THIS
AllowEncodedSlashes NoDecode
ProxyPass /_matrix/ http://127.0.0.1:6167/_matrix/ nocanon
ProxyPass /_matrix/ http://127.0.0.1:6167/_matrix/ timeout=300 nocanon
ProxyPassReverse /_matrix/ http://127.0.0.1:6167/_matrix/
</VirtualHost>
@ -208,7 +232,11 @@ ProxyPassReverse /_matrix/ http://127.0.0.1:6167/_matrix/
**You need to make some edits again.** When you are done, run
```bash
# Debian
$ sudo systemctl reload apache2
# Installed from source
$ sudo apachectl -k graceful
```
### Caddy
@ -241,12 +269,14 @@ server {
merge_slashes off;
# Nginx defaults to only allow 1MB uploads
# Increase this to allow posting large files such as videos
client_max_body_size 20M;
location /_matrix/ {
proxy_pass http://127.0.0.1:6167$request_uri;
proxy_set_header Host $http_host;
proxy_buffering off;
proxy_read_timeout 5m;
}
ssl_certificate /etc/letsencrypt/live/your.server.name/fullchain.pem; # EDIT THIS
@ -266,11 +296,19 @@ $ sudo systemctl reload nginx
If you chose Caddy as your web proxy SSL certificates are handled automatically and you can skip this step.
The easiest way to get an SSL certificate, if you don't have one already, is to install `certbot` and run this:
The easiest way to get an SSL certificate, if you don't have one already, is to [install](https://certbot.eff.org/instructions) `certbot` and run this:
```bash
# To use ECC for the private key,
# paste into /etc/letsencrypt/cli.ini:
# key-type = ecdsa
# elliptic-curve = secp384r1
$ sudo certbot -d your.server.name
```
[Automated renewal](https://eff-certbot.readthedocs.io/en/stable/using.html#automated-renewals) is usually preconfigured.
If using Cloudflare, configure instead the edge and origin certificates in dashboard. In case youre already running a website on the same Apache server, you can just copy-and-paste the SSL configuration from your main virtual host on port 443 into the above-mentioned vhost.
## You're done!
@ -294,6 +332,8 @@ You can also use these commands as a quick health check.
```bash
$ curl https://your.server.name/_matrix/client/versions
# If using port 8448
$ curl https://your.server.name:8448/_matrix/client/versions
```

View File

@ -1,5 +1,7 @@
# syntax=docker/dockerfile:1
FROM docker.io/rust:1.64-bullseye AS builder
FROM docker.io/rust:1.70-bullseye AS base
FROM base AS builder
WORKDIR /usr/src/conduit
# Install required packages to build Conduit and it's dependencies
@ -37,7 +39,7 @@ COPY --from=builder /usr/src/conduit/target/release/conduit /conduit
# ---------------------------------------------------------------------------------------------------------------
# Build cargo-deb, a tool to package up rust binaries into .deb packages for Debian/Ubuntu based systems:
# ---------------------------------------------------------------------------------------------------------------
FROM docker.io/rust:1.64-bullseye AS build-cargo-deb
FROM base AS build-cargo-deb
RUN apt-get update && \
apt-get install -y --no-install-recommends \
@ -57,7 +59,7 @@ WORKDIR /usr/src/conduit
COPY ./LICENSE ./LICENSE
COPY ./README.md ./README.md
COPY debian/README.Debian ./debian/
COPY debian ./debian
COPY --from=build-cargo-deb /usr/local/cargo/bin/cargo-deb /usr/local/cargo/bin/cargo-deb
# --no-build makes cargo-deb reuse already compiled project

View File

@ -16,10 +16,7 @@ friends or company.
#### Can I try it out?
Yes! You can test our Conduit instance by opening a Matrix client (<https://app.element.io> or Element Android for
example) and registering on the `conduit.rs` homeserver.
*Registration is currently disabled because of scammers. For an account please
message us (see contact section below).*
example) and registering on the `conduit.rs` homeserver. The registration token is "for_testing_only". Don't share personal information.
Server hosting for conduit.rs is donated by the Matrix.org Foundation.
@ -39,7 +36,7 @@ Check out the [Conduit 1.0 Release Milestone](https://gitlab.com/famedly/conduit
#### How can I deploy my own?
- Simple install (this was tested the most): [DEPLOY.md](DEPLOY.md)
- Debian package: [debian/README.Debian](debian/README.Debian)
- Debian package: [debian/README.md](debian/README.md)
- Nix/NixOS: [nix/README.md](nix/README.md)
- Docker: [docker/README.md](docker/README.md)
@ -68,7 +65,7 @@ Thanks to the contributors to Conduit and all libraries we use, for example:
If you run into any question, feel free to
- Ask us in `#conduit:fachschaften.org` on Matrix
- Write an E-Mail to `conduit@koesters.xyz`
- Send an direct message to `timo@fachschaften.org` on Matrix
- Send an direct message to `timokoesters@fachschaften.org` on Matrix
- [Open an issue on GitLab](https://gitlab.com/famedly/conduit/-/issues/new)
#### Donate

48
complement/Dockerfile Normal file
View File

@ -0,0 +1,48 @@
# For use in our CI only. This requires a build artifact created by a previous run pipline stage to be placed in cached_target/release/conduit
FROM registry.gitlab.com/jfowl/conduit-containers/rust-with-tools:commit-16a08e9b as builder
#FROM rust:latest as builder
WORKDIR /workdir
ARG RUSTC_WRAPPER
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG SCCACHE_BUCKET
ARG SCCACHE_ENDPOINT
ARG SCCACHE_S3_USE_SSL
COPY . .
RUN mkdir -p target/release
RUN test -e cached_target/release/conduit && cp cached_target/release/conduit target/release/conduit || cargo build --release
## Actual image
FROM debian:bullseye
WORKDIR /workdir
# Install caddy
RUN apt-get update && apt-get install -y debian-keyring debian-archive-keyring apt-transport-https curl && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/testing/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-testing-archive-keyring.gpg && curl -1sLf 'https://dl.cloudsmith.io/public/caddy/testing/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-testing.list && apt-get update && apt-get install -y caddy
COPY conduit-example.toml conduit.toml
COPY complement/caddy.json caddy.json
ENV SERVER_NAME=localhost
ENV CONDUIT_CONFIG=/workdir/conduit.toml
RUN sed -i "s/port = 6167/port = 8008/g" conduit.toml
RUN echo "allow_federation = true" >> conduit.toml
RUN echo "allow_check_for_updates = true" >> conduit.toml
RUN echo "allow_encryption = true" >> conduit.toml
RUN echo "allow_registration = true" >> conduit.toml
RUN echo "log = \"warn,_=off,sled=off\"" >> conduit.toml
RUN sed -i "s/address = \"127.0.0.1\"/address = \"0.0.0.0\"/g" conduit.toml
COPY --from=builder /workdir/target/release/conduit /workdir/conduit
RUN chmod +x /workdir/conduit
EXPOSE 8008 8448
CMD uname -a && \
sed -i "s/#server_name = \"your.server.name\"/server_name = \"${SERVER_NAME}\"/g" conduit.toml && \
sed -i "s/your.server.name/${SERVER_NAME}/g" caddy.json && \
caddy start --config caddy.json > /dev/null && \
/workdir/conduit

13
complement/README.md Normal file
View File

@ -0,0 +1,13 @@
# Running Conduit on Complement
This assumes that you're familiar with complement, if not, please readme
[their readme](https://github.com/matrix-org/complement#running).
Complement works with "base images", this directory (and Dockerfile) helps build the conduit complement-ready docker
image.
To build, `cd` to the base directory of the workspace, and run this:
`docker build -t complement-conduit:dev -f complement/Dockerfile .`
Then use `complement-conduit:dev` as a base image for running complement tests.

72
complement/caddy.json Normal file
View File

@ -0,0 +1,72 @@
{
"logging": {
"logs": {
"default": {
"level": "WARN"
}
}
},
"apps": {
"http": {
"https_port": 8448,
"servers": {
"srv0": {
"listen": [":8448"],
"routes": [{
"match": [{
"host": ["your.server.name"]
}],
"handle": [{
"handler": "subroute",
"routes": [{
"handle": [{
"handler": "reverse_proxy",
"upstreams": [{
"dial": "127.0.0.1:8008"
}]
}]
}]
}],
"terminal": true
}],
"tls_connection_policies": [{
"match": {
"sni": ["your.server.name"]
}
}]
}
}
},
"pki": {
"certificate_authorities": {
"local": {
"name": "Complement CA",
"root": {
"certificate": "/complement/ca/ca.crt",
"private_key": "/complement/ca/ca.key"
},
"intermediate": {
"certificate": "/complement/ca/ca.crt",
"private_key": "/complement/ca/ca.key"
}
}
}
},
"tls": {
"automation": {
"policies": [{
"subjects": ["your.server.name"],
"issuers": [{
"module": "internal"
}],
"on_demand": true
}, {
"issuers": [{
"module": "internal",
"ca": "local"
}]
}]
}
}
}
}

View File

@ -39,10 +39,15 @@ max_request_size = 20_000_000 # in bytes
allow_registration = true
allow_federation = true
allow_check_for_updates = true
# Enable the display name lightning bolt on registration.
enable_lightning_bolt = true
# Servers listed here will be used to gather public keys of other servers.
# Generally, copying this exactly should be enough. (Currently, Conduit doesn't
# support batched key requests, so this list should only contain Synapse
# servers.)
trusted_servers = ["matrix.org"]
#max_concurrent_requests = 100 # How many requests Conduit sends to other servers at the same time

View File

@ -1,28 +1,36 @@
Conduit for Debian
==================
Installation
------------
Information about downloading, building and deploying the Debian package, see
the "Installing Conduit" section in [DEPLOY.md](../DEPLOY.md).
All following sections until "Setting up the Reverse Proxy" be ignored because
this is handled automatically by the packaging.
Configuration
-------------
When installed, Debconf generates the configuration of the homeserver
(host)name, the address and port it listens on. This configuration ends up in
/etc/matrix-conduit/conduit.toml.
`/etc/matrix-conduit/conduit.toml`.
You can tweak more detailed settings by uncommenting and setting the variables
in /etc/matrix-conduit/conduit.toml. This involves settings such as the maximum
in `/etc/matrix-conduit/conduit.toml`. This involves settings such as the maximum
file size for download/upload, enabling federation, etc.
Running
-------
The package uses the matrix-conduit.service systemd unit file to start and
The package uses the `matrix-conduit.service` systemd unit file to start and
stop Conduit. It loads the configuration file mentioned above to set up the
environment before running the server.
This package assumes by default that Conduit will be placed behind a reverse
proxy such as Apache or nginx. This default deployment entails just listening
on 127.0.0.1 and the free port 6167 and is reachable via a client using the URL
http://localhost:6167.
on `127.0.0.1` and the free port `6167` and is reachable via a client using the URL
<http://localhost:6167>.
At a later stage this packaging may support also setting up TLS and running
stand-alone. In this case, however, you need to set up some certificates and

11
debian/postinst vendored
View File

@ -19,11 +19,11 @@ case "$1" in
_matrix-conduit
fi
# Create the database path if it does not exist yet.
if [ ! -d "$CONDUIT_DATABASE_PATH" ]; then
mkdir -p "$CONDUIT_DATABASE_PATH"
chown _matrix-conduit "$CONDUIT_DATABASE_PATH"
fi
# Create the database path if it does not exist yet and fix up ownership
# and permissions.
mkdir -p "$CONDUIT_DATABASE_PATH"
chown _matrix-conduit "$CONDUIT_DATABASE_PATH"
chmod 700 "$CONDUIT_DATABASE_PATH"
if [ ! -e "$CONDUIT_CONFIG_FILE" ]; then
# Write the debconf values in the config.
@ -73,6 +73,7 @@ max_request_size = 20_000_000 # in bytes
allow_registration = true
allow_federation = true
allow_check_for_updates = true
trusted_servers = ["matrix.org"]

View File

@ -4,7 +4,36 @@
## Docker
### Build & Dockerfile
To run Conduit with Docker you can either build the image yourself or pull it from a registry.
### Use a registry
OCI images for Conduit are available in the registries listed below. We recommend using the image tagged as `latest` from GitLab's own registry.
| Registry | Image | Size | Notes |
| --------------- | --------------------------------------------------------------- | ----------------------------- | ---------------------- |
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:latest][gl] | ![Image Size][shield-latest] | Stable image. |
| Docker Hub | [docker.io/matrixconduit/matrix-conduit:latest][dh] | ![Image Size][shield-latest] | Stable image. |
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:next][gl] | ![Image Size][shield-next] | Development version. |
| Docker Hub | [docker.io/matrixconduit/matrix-conduit:next][dh] | ![Image Size][shield-next] | Development version. |
[dh]: https://hub.docker.com/r/matrixconduit/matrix-conduit
[gl]: https://gitlab.com/famedly/conduit/container_registry/2497937
[shield-latest]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/latest
[shield-next]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/next
Use
```bash
docker image pull <link>
```
to pull it to your machine.
### Build using a dockerfile
The Dockerfile provided by Conduit has two stages, each of which creates an image.
@ -19,9 +48,11 @@ docker build --tag matrixconduit/matrix-conduit:latest .
which also will tag the resulting image as `matrixconduit/matrix-conduit:latest`.
### Run
After building the image you can simply run it with
When you have the image you can simply run it with
```bash
docker run -d -p 8448:6167 \
@ -34,19 +65,10 @@ docker run -d -p 8448:6167 \
-e CONDUIT_TRUSTED_SERVERS="[\"matrix.org\"]" \
-e CONDUIT_MAX_CONCURRENT_REQUESTS="100" \
-e CONDUIT_LOG="warn,rocket=off,_=off,sled=off" \
--name conduit matrixconduit/matrix-conduit:latest
--name conduit <link>
```
or you can skip the build step and pull the image from one of the following registries:
| Registry | Image | Size |
| --------------- | --------------------------------------------------------------- | --------------------- |
| Docker Hub | [matrixconduit/matrix-conduit:latest][dh] | ![Image Size][shield] |
| GitLab Registry | [registry.gitlab.com/famedly/conduit/matrix-conduit:latest][gl] | ![Image Size][shield] |
[dh]: https://hub.docker.com/r/matrixconduit/matrix-conduit
[gl]: https://gitlab.com/famedly/conduit/container_registry/2497937
[shield]: https://img.shields.io/docker/image-size/matrixconduit/matrix-conduit/latest
or you can use [docker-compose](#docker-compose).
The `-d` flag lets the container run in detached mode. You now need to supply a `conduit.toml` config file, an example can be found [here](../conduit-example.toml).
You can pass in different env vars to change config values on the fly. You can even configure Conduit completely by using env vars, but for that you need
@ -95,7 +117,7 @@ As a container user, you probably know about Traefik. It is a easy to use revers
containerized app and services available through the web. With the two provided files,
[`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and
[`docker-compose.override.yml`](docker-compose.override.traefik.yml), it is equally easy to deploy
[`docker-compose.override.yml`](docker-compose.override.yml), it is equally easy to deploy
and use Conduit, with a little caveat. If you already took a look at the files, then you should have
seen the `well-known` service, and that is the little caveat. Traefik is simply a proxy and
loadbalancer and is not able to serve any kind of content, but for Conduit to federate, we need to
@ -106,7 +128,8 @@ With the service `well-known` we use a single `nginx` container that will serve
So...step by step:
1. Copy [`docker-compose.traefik.yml`](docker-compose.traefik.yml) and [`docker-compose.override.traefik.yml`](docker-compose.override.traefik.yml) from the repository and remove `.traefik` from the filenames.
1. Copy [`docker-compose.for-traefik.yml`](docker-compose.for-traefik.yml) (or
[`docker-compose.with-traefik.yml`](docker-compose.with-traefik.yml)) and [`docker-compose.override.yml`](docker-compose.override.yml) from the repository and remove `.for-traefik` (or `.with-traefik`) from the filename.
2. Open both files and modify/adjust them to your needs. Meaning, change the `CONDUIT_SERVER_NAME` and the volume host mappings according to your needs.
3. Create the `conduit.toml` config file, an example can be found [here](../conduit-example.toml), or set `CONDUIT_CONFIG=""` and configure Conduit per env vars.
4. Uncomment the `element-web` service if you want to host your own Element Web Client and create a `element_config.json`.

View File

@ -29,6 +29,7 @@ services:
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUIT_ALLOW_REGISTRATION: 'true'
CONDUIT_ALLOW_FEDERATION: 'true'
CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
#CONDUIT_LOG: warn,rocket=off,_=off,sled=off

View File

@ -35,8 +35,9 @@ services:
# Available levels are: error, warn, info, debug, trace - more info at: https://docs.rs/env_logger/*/env_logger/#enabling-logging
# CONDUIT_LOG: info # default is: "warn,_=off,sled=off"
# CONDUIT_ALLOW_JAEGER: 'false'
# CONDUIT_ALLOW_ENCRYPTION: 'false'
# CONDUIT_ALLOW_FEDERATION: 'false'
# CONDUIT_ALLOW_ENCRYPTION: 'true'
# CONDUIT_ALLOW_FEDERATION: 'true'
# CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
# CONDUIT_DATABASE_PATH: /srv/conduit/.local/share/conduit
# CONDUIT_WORKERS: 10
# CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB

View File

@ -29,6 +29,7 @@ services:
CONDUIT_MAX_REQUEST_SIZE: 20_000_000 # in bytes, ~20 MB
CONDUIT_ALLOW_REGISTRATION: 'true'
CONDUIT_ALLOW_FEDERATION: 'true'
CONDUIT_ALLOW_CHECK_FOR_UPDATES: 'true'
CONDUIT_TRUSTED_SERVERS: '["matrix.org"]'
#CONDUIT_MAX_CONCURRENT_REQUESTS: 100
#CONDUIT_LOG: warn,rocket=off,_=off,sled=off

View File

@ -6,9 +6,14 @@ if [ -z "${CONDUIT_PORT}" ]; then
CONDUIT_PORT=$(ss -tlpn | grep conduit | grep -m1 -o ':[0-9]*' | grep -m1 -o '[0-9]*')
fi
# If CONDUIT_ADDRESS is not set try to get the address from the process list
if [ -z "${CONDUIT_ADDRESS}" ]; then
CONDUIT_ADDRESS=$(ss -tlpn | awk -F ' +|:' '/conduit/ { print $4 }')
fi
# The actual health check.
# We try to first get a response on HTTP and when that fails on HTTPS and when that fails, we exit with code 1.
# TODO: Change this to a single wget call. Do we have a config value that we can check for that?
wget --no-verbose --tries=1 --spider "http://localhost:${CONDUIT_PORT}/_matrix/client/versions" || \
wget --no-verbose --tries=1 --spider "https://localhost:${CONDUIT_PORT}/_matrix/client/versions" || \
wget --no-verbose --tries=1 --spider "http://${CONDUIT_ADDRESS}:${CONDUIT_PORT}/_matrix/client/versions" || \
wget --no-verbose --tries=1 --spider "https://${CONDUIT_ADDRESS}:${CONDUIT_PORT}/_matrix/client/versions" || \
exit 1

View File

@ -1,5 +1,30 @@
{
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": "rust-overlay"
},
"locked": {
"lastModified": 1688772518,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"fenix": {
"inputs": {
"nixpkgs": [
@ -8,11 +33,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1665815894,
"narHash": "sha256-Vboo1L4NMGLKZKVLnOPi9OHlae7uoNyfgvyIUm+SVXE=",
"lastModified": 1689488573,
"narHash": "sha256-diVASflKCCryTYv0djvMnP2444mFsIG0ge5pa7ahauQ=",
"owner": "nix-community",
"repo": "fenix",
"rev": "2348450241a5f945f0ba07e44ecbfac2f541d7f4",
"rev": "39096fe3f379036ff4a5fa198950b8e79defe939",
"type": "github"
},
"original": {
@ -21,72 +46,72 @@
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"naersk": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
"systems": "systems"
},
"locked": {
"lastModified": 1662220400,
"narHash": "sha256-9o2OGQqu4xyLZP9K6kNe1pTHnyPz0Wr3raGYnr9AIgY=",
"owner": "nix-community",
"repo": "naersk",
"rev": "6944160c19cb591eb85bbf9b2f2768a935623ed3",
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "naersk",
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1665856037,
"narHash": "sha256-/RvIWnGKdTSoIq5Xc2HwPIL0TzRslzU6Rqk4Img6UNg=",
"lastModified": 1689444953,
"narHash": "sha256-0o56bfb2LC38wrinPdCGLDScd77LVcr7CrH1zK7qvDg=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "c95ebc5125ffffcd431df0ad8620f0926b8125b8",
"rev": "8acef304efe70152463a6399f73e636bcc363813",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"crane": "crane",
"fenix": "fenix",
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs"
}
},
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1665765556,
"narHash": "sha256-w9L5j0TIB5ay4aRwzGCp8mgvGsu5dVJQvbEFutwr6xE=",
"lastModified": 1689441253,
"narHash": "sha256-4MSDZaFI4DOfsLIZYPMBl0snzWhX1/OqR/QHir382CY=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "018b8429cf3fa9d8aed916704e41dfedeb0f4f78",
"rev": "996e054f1eb1dbfc8455ecabff0f6ff22ba7f7c8",
"type": "github"
},
"original": {
@ -95,6 +120,46 @@
"repo": "rust-analyzer",
"type": "github"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": [
"crane",
"flake-utils"
],
"nixpkgs": [
"crane",
"nixpkgs"
]
},
"locked": {
"lastModified": 1688351637,
"narHash": "sha256-CLTufJ29VxNOIZ8UTg0lepsn3X03AmopmaLTTeHDCL4=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "f9b92316727af9e6c7fee4a761242f7f46880329",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@ -1,15 +1,16 @@
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
fenix = {
url = "github:nix-community/fenix";
inputs.nixpkgs.follows = "nixpkgs";
};
naersk = {
url = "github:nix-community/naersk";
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs";
inputs.flake-utils.follows = "flake-utils";
};
};
@ -19,11 +20,17 @@
, flake-utils
, fenix
, naersk
, crane
}: flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
# Use mold on Linux
stdenv = if pkgs.stdenv.isLinux then
pkgs.stdenvAdapters.useMoldLinker pkgs.stdenv
else
pkgs.stdenv;
# Nix-accessible `Cargo.toml`
cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
@ -32,33 +39,44 @@
# Use the Rust version defined in `Cargo.toml`
channel = cargoToml.package.rust-version;
# This will need to be updated when `package.rust-version` is changed in
# `Cargo.toml`
sha256 = "sha256-KXx+ID0y4mg2B3LHp7IyaiMrdexF6octADnAtFIOjrY=";
# THE rust-version HASH
sha256 = "sha256-gdYqng0y9iHYzYPAdkC/ka3DRny3La/S5G8ASj0Ayyc=";
};
builder = (pkgs.callPackage naersk {
inherit (toolchain) rustc cargo;
}).buildPackage;
# The system's RocksDB
ROCKSDB_INCLUDE_DIR = "${pkgs.rocksdb}/include";
ROCKSDB_LIB_DIR = "${pkgs.rocksdb}/lib";
# Shared between the package and the devShell
nativeBuildInputs = (with pkgs.rustPlatform; [
bindgenHook
]);
builder =
((crane.mkLib pkgs).overrideToolchain toolchain.toolchain).buildPackage;
in
{
packages.default = builder {
src = ./.;
nativeBuildInputs = (with pkgs.rustPlatform; [
bindgenHook
]);
inherit
stdenv
nativeBuildInputs
ROCKSDB_INCLUDE_DIR
ROCKSDB_LIB_DIR;
};
devShells.default = pkgs.mkShell {
devShells.default = (pkgs.mkShell.override { inherit stdenv; }) {
# Rust Analyzer needs to be able to find the path to default crate
# sources, and it can read this environment variable to do so
RUST_SRC_PATH = "${toolchain.rust-src}/lib/rustlib/src/rust/library";
inherit
ROCKSDB_INCLUDE_DIR
ROCKSDB_LIB_DIR;
# Development tools
nativeBuildInputs = (with pkgs.rustPlatform; [
bindgenHook
]) ++ (with toolchain; [
nativeBuildInputs = nativeBuildInputs ++ (with toolchain; [
cargo
clippy
rust-src

View File

@ -107,7 +107,7 @@ in
recommendedProxySettings = true;
virtualHosts = {
"${server_name}" = {
"${matrix_hostname}" = {
forceSSL = true;
enableACME = true;
@ -118,20 +118,21 @@ in
ssl = true;
}
{
addr = "[::]";
port = 443;
ssl = true;
} {
addr = "0.0.0.0";
port = 8448;
ssl = true;
}
{
addr = "[::]";
port = 8448;
ssl = true;
}
];
extraConfig = ''
merge_slashes off;
'';
"${matrix_hostname}" = {
forceSSL = true;
enableACME = true;
locations."/_matrix/" = {
proxyPass = "http://backend_conduit$request_uri";
proxyWebsockets = true;
@ -141,6 +142,15 @@ in
'';
};
extraConfig = ''
merge_slashes off;
'';
};
"${server_name}" = {
forceSSL = true;
enableACME = true;
locations."=/.well-known/matrix/server" = {
# Use the contents of the derivation built previously
alias = "${well_known_server}";
@ -169,7 +179,7 @@ in
upstreams = {
"backend_conduit" = {
servers = {
"localhost:${toString config.services.matrix-conduit.settings.global.port}" = { };
"[::1]:${toString config.services.matrix-conduit.settings.global.port}" = { };
};
};
};

View File

@ -18,7 +18,7 @@ where
let mut http_request = request
.try_into_http_request::<BytesMut>(
destination,
SendAccessToken::IfRequired(""),
SendAccessToken::IfRequired(hs_token),
&[MatrixVersion::V1_0],
)
.unwrap()

View File

@ -74,7 +74,10 @@ pub async fn get_register_available_route(
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and access_token
pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<register::v3::Response> {
if !services().globals.allow_registration() && !body.from_appservice {
if !services().globals.allow_registration()
&& !body.from_appservice
&& services().globals.config.registration_token.is_none()
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Registration has been disabled.",
@ -121,7 +124,11 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
// UIAA
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow {
stages: vec![AuthType::Dummy],
stages: if services().globals.config.registration_token.is_some() {
vec![AuthType::RegistrationToken]
} else {
vec![AuthType::Dummy]
},
}],
completed: Vec::new(),
params: Default::default(),
@ -129,7 +136,7 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
auth_error: None,
};
if !body.from_appservice {
if !body.from_appservice && !is_guest {
if let Some(auth) = &body.auth {
let (worked, uiaainfo) = services().uiaa.try_auth(
&UserId::parse_with_server_name("", services().globals.server_name())
@ -222,11 +229,13 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
)?;
info!("New user {} registered on this server.", user_id);
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user {user_id} registered on this server."
)));
if !body.from_appservice && !is_guest {
services()
.admin
.send_message(RoomMessageEventContent::notice_plain(format!(
"New user {user_id} registered on this server."
)));
}
// If this is the first real user, grant them admin privileges
// Note: the server user, @conduit:servername, is generated first

View File

@ -1,4 +1,5 @@
use crate::{services, Error, Result, Ruma};
use rand::seq::SliceRandom;
use regex::Regex;
use ruma::{
api::{
@ -90,10 +91,10 @@ pub(crate) async fn get_alias_helper(
)
.await?;
return Ok(get_alias::v3::Response::new(
response.room_id,
response.servers,
));
let mut servers = response.servers;
servers.shuffle(&mut rand::thread_rng());
return Ok(get_alias::v3::Response::new(response.room_id, servers));
}
let mut room_id = None;

View File

@ -75,7 +75,7 @@ pub async fn get_global_account_data_route(
let event: Box<RawJsonValue> = services()
.account_data
.get(None, sender_user, body.event_type.clone().into())?
.get(None, sender_user, body.event_type.to_string().into())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
let account_data = serde_json::from_str::<ExtractGlobalEventContent>(event.get())
@ -95,11 +95,7 @@ pub async fn get_room_account_data_route(
let event: Box<RawJsonValue> = services()
.account_data
.get(
Some(&body.room_id),
sender_user,
body.event_type.clone().into(),
)?
.get(Some(&body.room_id), sender_user, body.event_type.clone())?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Data not found."))?;
let account_data = serde_json::from_str::<ExtractRoomEventContent>(event.get())

View File

@ -3,7 +3,7 @@ use ruma::{
api::client::{context::get_context, error::ErrorKind, filter::LazyLoadOptions},
events::StateEventType,
};
use std::{collections::HashSet, convert::TryFrom};
use std::collections::HashSet;
use tracing::error;
/// # `GET /_matrix/client/r0/rooms/{roomId}/context`
@ -27,36 +27,35 @@ pub async fn get_context_route(
let mut lazy_loaded = HashSet::new();
let base_pdu_id = services()
let base_token = services()
.rooms
.timeline
.get_pdu_id(&body.event_id)?
.get_pdu_count(&body.event_id)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"Base event id not found.",
))?;
let base_token = services().rooms.timeline.pdu_count(&base_pdu_id)?;
let base_event = services()
.rooms
.timeline
.get_pdu_from_id(&base_pdu_id)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"Base event not found.",
))?;
let base_event =
services()
.rooms
.timeline
.get_pdu(&body.event_id)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"Base event not found.",
))?;
let room_id = base_event.room_id.clone();
if !services()
.rooms
.state_cache
.is_joined(sender_user, &room_id)?
.state_accessor
.user_can_see_event(sender_user, &room_id, &body.event_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You don't have permission to view this room.",
"You don't have permission to view this event.",
));
}
@ -70,19 +69,24 @@ pub async fn get_context_route(
lazy_loaded.insert(base_event.sender.as_str().to_owned());
}
// Use limit with maximum 100
let limit = u64::from(body.limit).min(100) as usize;
let base_event = base_event.to_room_event();
let events_before: Vec<_> = services()
.rooms
.timeline
.pdus_until(sender_user, &room_id, base_token)?
.take(
u32::try_from(body.limit).map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Limit value is invalid.")
})? as usize
/ 2,
)
.take(limit / 2)
.filter_map(|r| r.ok()) // Remove buggy events
.filter(|(_, pdu)| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.collect();
for (_, event) in &events_before {
@ -99,8 +103,8 @@ pub async fn get_context_route(
let start_token = events_before
.last()
.and_then(|(pdu_id, _)| services().rooms.timeline.pdu_count(pdu_id).ok())
.map(|count| count.to_string());
.map(|(count, _)| count.stringify())
.unwrap_or_else(|| base_token.stringify());
let events_before: Vec<_> = events_before
.into_iter()
@ -111,13 +115,15 @@ pub async fn get_context_route(
.rooms
.timeline
.pdus_after(sender_user, &room_id, base_token)?
.take(
u32::try_from(body.limit).map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Limit value is invalid.")
})? as usize
/ 2,
)
.take(limit / 2)
.filter_map(|r| r.ok()) // Remove buggy events
.filter(|(_, pdu)| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.collect();
for (_, event) in &events_after {
@ -153,8 +159,8 @@ pub async fn get_context_route(
let end_token = events_after
.last()
.and_then(|(pdu_id, _)| services().rooms.timeline.pdu_count(pdu_id).ok())
.map(|count| count.to_string());
.map(|(count, _)| count.stringify())
.unwrap_or_else(|| base_token.stringify());
let events_after: Vec<_> = events_after
.into_iter()
@ -191,8 +197,8 @@ pub async fn get_context_route(
}
let resp = get_context::v3::Response {
start: start_token,
end: end_token,
start: Some(start_token),
end: Some(end_token),
events_before,
event: Some(base_event),
events_after,

View File

@ -20,7 +20,6 @@ use ruma::{
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{JoinRule, RoomJoinRulesEventContent},
name::RoomNameEventContent,
topic::RoomTopicEventContent,
},
StateEventType,
@ -203,17 +202,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
Error::bad_database("Invalid canonical alias event in database.")
})
})?,
name: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomName, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomNameEventContent| c.name)
.map_err(|_| {
Error::bad_database("Invalid room name event in database.")
})
})?,
name: services().rooms.state_accessor.get_name(&room_id)?,
num_joined_members: services()
.rooms
.state_cache
@ -232,6 +221,7 @@ pub(crate) async fn get_public_rooms_filtered_helper(
serde_json::from_str(s.content.get())
.map(|c: RoomTopicEventContent| Some(c.topic))
.map_err(|_| {
error!("Invalid room topic event in database for room {}", room_id);
Error::bad_database("Invalid room topic event in database.")
})
})?,

View File

@ -132,6 +132,7 @@ pub async fn upload_signing_keys_route(
master_key,
&body.self_signing_key,
&body.user_signing_key,
true, // notify so that other users see the new keys
)?;
}
@ -151,18 +152,6 @@ pub async fn upload_signatures_route(
let key = serde_json::to_value(key)
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid key JSON"))?;
let is_signed_key = match key.get("usage") {
Some(usage) => usage
.as_array()
.map(|usage| !usage.contains(&json!("master")))
.unwrap_or(false),
None => true,
};
if !is_signed_key {
continue;
}
for signature in key
.get("signatures")
.ok_or(Error::BadRequest(
@ -323,15 +312,17 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
}
}
if let Some(master_key) = services()
.users
.get_master_key(user_id, &allowed_signatures)?
if let Some(master_key) =
services()
.users
.get_master_key(sender_user, user_id, &allowed_signatures)?
{
master_keys.insert(user_id.to_owned(), master_key);
}
if let Some(self_signing_key) = services()
.users
.get_self_signing_key(user_id, &allowed_signatures)?
if let Some(self_signing_key) =
services()
.users
.get_self_signing_key(sender_user, user_id, &allowed_signatures)?
{
self_signing_keys.insert(user_id.to_owned(), self_signing_key);
}
@ -369,7 +360,29 @@ pub(crate) async fn get_keys_helper<F: Fn(&UserId) -> bool>(
while let Some((server, response)) = futures.next().await {
match response {
Ok(response) => {
master_keys.extend(response.master_keys);
for (user, masterkey) in response.master_keys {
let (master_key_id, mut master_key) =
services().users.parse_master_key(&user, &masterkey)?;
if let Some(our_master_key) = services().users.get_key(
&master_key_id,
sender_user,
&user,
&allowed_signatures,
)? {
let (_, our_master_key) =
services().users.parse_master_key(&user, &our_master_key)?;
master_key.signatures.extend(our_master_key.signatures);
}
let json = serde_json::to_value(master_key).expect("to_value always works");
let raw = serde_json::from_value(json).expect("Raw::from_value always works");
services().users.add_cross_signing_keys(
&user, &raw, &None, &None,
false, // Dont notify. A notification would trigger another key request resulting in an endless loop
)?;
master_keys.insert(user, raw);
}
self_signing_keys.extend(response.self_signing_keys);
device_keys.extend(response.device_keys);
}

View File

@ -1,3 +1,5 @@
use std::time::Duration;
use crate::{service::media::FileMeta, services, utils, Error, Result, Ruma};
use ruma::api::client::{
error::ErrorKind,
@ -67,6 +69,8 @@ pub async fn get_remote_content(
allow_remote: false,
server_name: server_name.to_owned(),
media_id,
timeout_ms: Duration::from_secs(20),
allow_redirect: false,
},
)
.await?;
@ -194,6 +198,8 @@ pub async fn get_content_thumbnail_route(
method: body.method.clone(),
server_name: body.server_name.clone(),
media_id: body.media_id.clone(),
timeout_ms: Duration::from_secs(20),
allow_redirect: false,
},
)
.await?;

View File

@ -17,7 +17,7 @@ use ruma::{
member::{MembershipState, RoomMemberEventContent},
power_levels::RoomPowerLevelsEventContent,
},
RoomEventType, StateEventType,
StateEventType, TimelineEventType,
},
serde::Base64,
state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
@ -29,7 +29,7 @@ use std::{
sync::{Arc, RwLock},
time::{Duration, Instant},
};
use tracing::{debug, error, warn};
use tracing::{debug, error, info, warn};
use crate::{
service::pdu::{gen_event_id_canonical_json, PduBuilder},
@ -106,12 +106,13 @@ pub async fn join_room_by_id_or_alias_route(
);
servers.push(room_id.server_name().to_owned());
(servers, room_id)
}
Err(room_alias) => {
let response = get_alias_helper(room_alias).await?;
(response.servers.into_iter().collect(), response.room_id)
(response.servers, response.room_id)
}
};
@ -209,7 +210,7 @@ pub async fn kick_user_route(
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -273,7 +274,7 @@ pub async fn ban_user_route(body: Ruma<ban_user::v3::Request>) -> Result<ban_use
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -331,7 +332,7 @@ pub async fn unban_user_route(
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -396,11 +397,10 @@ pub async fn get_member_events_route(
) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// TODO: check history visibility?
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
.state_accessor
.user_can_see_state_events(&sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
@ -434,12 +434,12 @@ pub async fn joined_members_route(
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
.state_accessor
.user_can_see_state_events(&sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You aren't a member of the room.",
"You don't have permission to view this room.",
));
}
@ -491,9 +491,13 @@ async fn join_room_by_id_helper(
.state_cache
.server_in_room(services().globals.server_name(), room_id)?
{
info!("Joining {room_id} over federation.");
let (make_join_response, remote_server) =
make_join_request(sender_user, room_id, servers).await?;
info!("make_join finished");
let room_version_id = match make_join_response.room_version {
Some(room_version)
if services()
@ -578,6 +582,7 @@ async fn join_room_by_id_helper(
// It has enough fields to be called a proper event now
let mut join_event = join_event_stub;
info!("Asking {remote_server} for send_join");
let send_join_response = services()
.sending
.send_federation_request(
@ -586,11 +591,15 @@ async fn join_room_by_id_helper(
room_id: room_id.to_owned(),
event_id: event_id.to_owned(),
pdu: PduEvent::convert_to_outgoing_federation_event(join_event.clone()),
omit_members: false,
},
)
.await?;
info!("send_join finished");
if let Some(signed_raw) = &send_join_response.room_state.event {
info!("There is a signed event. This room is probably using restricted joins. Adding signature to our event");
let (signed_event_id, signed_value) =
match gen_event_id_canonical_json(signed_raw, &room_version_id) {
Ok(t) => t,
@ -630,24 +639,29 @@ async fn join_room_by_id_helper(
.expect("we created a valid pdu")
.insert(remote_server.to_string(), signature.clone());
} else {
warn!("Server {} sent invalid sendjoin event", remote_server);
warn!(
"Server {remote_server} sent invalid signature in sendjoin signatures for event {signed_value:?}",
);
}
}
services().rooms.short.get_or_create_shortroomid(room_id)?;
info!("Parsing join event");
let parsed_join_pdu = PduEvent::from_id_val(event_id, join_event.clone())
.map_err(|_| Error::BadServerResponse("Invalid join event PDU."))?;
let mut state = HashMap::new();
let pub_key_map = RwLock::new(BTreeMap::new());
info!("Fetching join signing keys");
services()
.rooms
.event_handler
.fetch_join_signing_keys(&send_join_response, &room_version_id, &pub_key_map)
.await?;
info!("Going through send_join response room_state");
for result in send_join_response
.room_state
.state
@ -660,7 +674,7 @@ async fn join_room_by_id_helper(
};
let pdu = PduEvent::from_id_val(&event_id, value.clone()).map_err(|e| {
warn!("{:?}: {}", value, e);
warn!("Invalid PDU in send_join response: {} {:?}", e, value);
Error::BadServerResponse("Invalid PDU in send_join response.")
})?;
@ -677,6 +691,7 @@ async fn join_room_by_id_helper(
}
}
info!("Going through send_join response auth_chain");
for result in send_join_response
.room_state
.auth_chain
@ -694,6 +709,7 @@ async fn join_room_by_id_helper(
.add_pdu_outlier(&event_id, &value)?;
}
info!("Running send_join auth check");
if !state_res::event_auth::auth_check(
&state_res::RoomVersion::new(&room_version_id).expect("room version is supported"),
&parsed_join_pdu,
@ -714,25 +730,30 @@ async fn join_room_by_id_helper(
.ok()?
},
)
.map_err(|_e| Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed"))?
{
.map_err(|e| {
warn!("Auth check failed: {e}");
Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed")
})? {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Auth check failed",
));
}
info!("Saving state from send_join");
let (statehash_before_join, new, removed) = services().rooms.state_compressor.save_state(
room_id,
state
.into_iter()
.map(|(k, id)| {
services()
.rooms
.state_compressor
.compress_state_event(k, &id)
})
.collect::<Result<_>>()?,
Arc::new(
state
.into_iter()
.map(|(k, id)| {
services()
.rooms
.state_compressor
.compress_state_event(k, &id)
})
.collect::<Result<_>>()?,
),
)?;
services()
@ -741,12 +762,14 @@ async fn join_room_by_id_helper(
.force_state(room_id, statehash_before_join, new, removed, &state_lock)
.await?;
info!("Updating joined counts for new room");
services().rooms.state_cache.update_joined_count(room_id)?;
// We append to state before appending the pdu, so we don't have a moment in time with the
// pdu without it's state. This is okay because append_pdu can't fail.
let statehash_after_join = services().rooms.state.append_to_state(&parsed_join_pdu)?;
info!("Appending new room join event");
services().rooms.timeline.append_pdu(
&parsed_join_pdu,
join_event,
@ -754,6 +777,7 @@ async fn join_room_by_id_helper(
&state_lock,
)?;
info!("Setting final room state for new room");
// We set the room state after inserting the pdu, so that we never have a moment in time
// where events in the current room state do not exist
services()
@ -761,6 +785,8 @@ async fn join_room_by_id_helper(
.state
.set_room_state(room_id, statehash_after_join, &state_lock)?;
} else {
info!("We can join locally");
let join_rules_event = services().rooms.state_accessor.room_state_get(
room_id,
&StateEventType::RoomJoinRules,
@ -864,7 +890,7 @@ async fn join_room_by_id_helper(
// Try normal join first
let error = match services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(sender_user.to_string()),
@ -878,9 +904,16 @@ async fn join_room_by_id_helper(
Err(e) => e,
};
if !restriction_rooms.is_empty() {
// We couldn't do the join locally, maybe federation can help to satisfy the restricted
// join requirements
if !restriction_rooms.is_empty()
&& servers
.iter()
.filter(|s| *s != services().globals.server_name())
.count()
> 0
{
info!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted join requirements"
);
let (make_join_response, remote_server) =
make_join_request(sender_user, room_id, servers).await?;
@ -973,6 +1006,7 @@ async fn join_room_by_id_helper(
room_id: room_id.to_owned(),
event_id: event_id.to_owned(),
pdu: PduEvent::convert_to_outgoing_federation_event(join_event.clone()),
omit_members: false,
},
)
.await?;
@ -1038,6 +1072,7 @@ async fn make_join_request(
if remote_server == services().globals.server_name() {
continue;
}
info!("Asking {remote_server} for make_join");
let make_join_response = services()
.sending
.send_federation_request(
@ -1162,7 +1197,7 @@ pub(crate) async fn invite_helper<'a>(
let (pdu, pdu_json) = services().rooms.timeline.create_hash_and_sign_event(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content,
unsigned: None,
state_key: Some(user_id.to_string()),
@ -1271,7 +1306,7 @@ pub(crate) async fn invite_helper<'a>(
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Invite,
displayname: services().users.displayname(user_id)?,
@ -1396,7 +1431,7 @@ pub async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<Strin
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&event).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some(user_id.to_string()),

View File

@ -1,10 +1,13 @@
use crate::{service::pdu::PduBuilder, services, utils, Error, Result, Ruma};
use crate::{
service::{pdu::PduBuilder, rooms::timeline::PduCount},
services, utils, Error, Result, Ruma,
};
use ruma::{
api::client::{
error::ErrorKind,
message::{get_message_events, send_message_event},
},
events::{RoomEventType, StateEventType},
events::{StateEventType, TimelineEventType},
};
use std::{
collections::{BTreeMap, HashSet},
@ -36,7 +39,7 @@ pub async fn send_message_event_route(
let state_lock = mutex_state.lock().await;
// Forbid m.room.encrypted if encryption is disabled
if RoomEventType::RoomEncrypted == body.event_type.to_string().into()
if TimelineEventType::RoomEncrypted == body.event_type.to_string().into()
&& !services().globals.allow_encryption()
{
return Err(Error::BadRequest(
@ -110,29 +113,18 @@ pub async fn get_message_events_route(
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You don't have permission to view this room.",
));
}
let from = match body.from.clone() {
Some(from) => from
.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid `from` value."))?,
Some(from) => PduCount::try_from_string(&from)?,
None => match body.dir {
ruma::api::client::Direction::Forward => 0,
ruma::api::client::Direction::Backward => u64::MAX,
ruma::api::Direction::Forward => PduCount::min(),
ruma::api::Direction::Backward => PduCount::max(),
},
};
let to = body.to.as_ref().map(|t| t.parse());
let to = body
.to
.as_ref()
.and_then(|t| PduCount::try_from_string(&t).ok());
services().rooms.lazy_loading.lazy_load_confirm_delivery(
sender_user,
@ -141,8 +133,7 @@ pub async fn get_message_events_route(
from,
)?;
// Use limit or else 10
let limit = body.limit.try_into().map_or(10_usize, |l: u32| l as usize);
let limit = u64::from(body.limit).min(100) as usize;
let next_token;
@ -151,22 +142,21 @@ pub async fn get_message_events_route(
let mut lazy_loaded = HashSet::new();
match body.dir {
ruma::api::client::Direction::Forward => {
ruma::api::Direction::Forward => {
let events_after: Vec<_> = services()
.rooms
.timeline
.pdus_after(sender_user, &body.room_id, from)?
.take(limit)
.filter_map(|r| r.ok()) // Filter out buggy events
.filter_map(|(pdu_id, pdu)| {
.filter(|(_, pdu)| {
services()
.rooms
.timeline
.pdu_count(&pdu_id)
.map(|pdu_count| (pdu_count, pdu))
.ok()
.state_accessor
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
.unwrap_or(false)
})
.take_while(|&(k, _)| Some(Ok(k)) != to) // Stop at `to`
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.collect();
for (_, event) in &events_after {
@ -192,26 +182,30 @@ pub async fn get_message_events_route(
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = from.to_string();
resp.end = next_token.map(|count| count.to_string());
resp.start = from.stringify();
resp.end = next_token.map(|count| count.stringify());
resp.chunk = events_after;
}
ruma::api::client::Direction::Backward => {
ruma::api::Direction::Backward => {
services()
.rooms
.timeline
.backfill_if_required(&body.room_id, from)
.await?;
let events_before: Vec<_> = services()
.rooms
.timeline
.pdus_until(sender_user, &body.room_id, from)?
.take(limit)
.filter_map(|r| r.ok()) // Filter out buggy events
.filter_map(|(pdu_id, pdu)| {
.filter(|(_, pdu)| {
services()
.rooms
.timeline
.pdu_count(&pdu_id)
.map(|pdu_count| (pdu_count, pdu))
.ok()
.state_accessor
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
.unwrap_or(false)
})
.take_while(|&(k, _)| Some(Ok(k)) != to) // Stop at `to`
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.collect();
for (_, event) in &events_before {
@ -237,8 +231,8 @@ pub async fn get_message_events_route(
.map(|(_, pdu)| pdu.to_room_event())
.collect();
resp.start = from.to_string();
resp.end = next_token.map(|count| count.to_string());
resp.start = from.stringify();
resp.end = next_token.map(|count| count.stringify());
resp.chunk = events_before;
}
}

View File

@ -16,14 +16,17 @@ mod profile;
mod push;
mod read_marker;
mod redact;
mod relations;
mod report;
mod room;
mod search;
mod session;
mod space;
mod state;
mod sync;
mod tag;
mod thirdparty;
mod threads;
mod to_device;
mod typing;
mod unversioned;
@ -48,14 +51,17 @@ pub use profile::*;
pub use push::*;
pub use read_marker::*;
pub use redact::*;
pub use relations::*;
pub use report::*;
pub use room::*;
pub use search::*;
pub use session::*;
pub use space::*;
pub use state::*;
pub use sync::*;
pub use tag::*;
pub use thirdparty::*;
pub use threads::*;
pub use to_device::*;
pub use typing::*;
pub use unversioned::*;

View File

@ -9,7 +9,7 @@ use ruma::{
},
federation::{self, query::get_profile_information::v1::ProfileField},
},
events::{room::member::RoomMemberEventContent, RoomEventType, StateEventType},
events::{room::member::RoomMemberEventContent, StateEventType, TimelineEventType},
};
use serde_json::value::to_raw_value;
use std::sync::Arc;
@ -37,7 +37,7 @@ pub async fn set_displayname_route(
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
displayname: body.displayname.clone(),
..serde_json::from_str(
@ -172,7 +172,7 @@ pub async fn set_avatar_url_route(
.map(|room_id| {
Ok::<_, Error>((
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
avatar_url: body.avatar_url.clone(),
..serde_json::from_str(

View File

@ -5,11 +5,11 @@ use ruma::{
push::{
delete_pushrule, get_pushers, get_pushrule, get_pushrule_actions, get_pushrule_enabled,
get_pushrules_all, set_pusher, set_pushrule, set_pushrule_actions,
set_pushrule_enabled, RuleKind, RuleScope,
set_pushrule_enabled, RuleScope,
},
},
events::{push_rules::PushRulesEvent, GlobalAccountDataEventType},
push::{ConditionalPushRuleInit, NewPushRule, PatternedPushRuleInit, SimplePushRuleInit},
push::{InsertPushRuleError, RemovePushRuleError},
};
/// # `GET /_matrix/client/r0/pushrules`
@ -65,30 +65,10 @@ pub async fn get_pushrule_route(
.map_err(|_| Error::bad_database("Invalid account data event in db."))?
.content;
let global = account_data.global;
let rule = match body.kind {
RuleKind::Override => global
.override_
.get(body.rule_id.as_str())
.map(|rule| rule.clone().into()),
RuleKind::Underride => global
.underride
.get(body.rule_id.as_str())
.map(|rule| rule.clone().into()),
RuleKind::Sender => global
.sender
.get(body.rule_id.as_str())
.map(|rule| rule.clone().into()),
RuleKind::Room => global
.room
.get(body.rule_id.as_str())
.map(|rule| rule.clone().into()),
RuleKind::Content => global
.content
.get(body.rule_id.as_str())
.map(|rule| rule.clone().into()),
_ => None,
};
let rule = account_data
.global
.get(body.kind.clone(), &body.rule_id)
.map(Into::into);
if let Some(rule) = rule {
Ok(get_pushrule::v3::Response { rule })
@ -131,66 +111,36 @@ pub async fn set_pushrule_route(
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = &mut account_data.content.global;
match body.rule {
NewPushRule::Override(rule) => {
global.override_.replace(
ConditionalPushRuleInit {
actions: rule.actions,
default: false,
enabled: true,
rule_id: rule.rule_id,
conditions: rule.conditions,
}
.into(),
);
}
NewPushRule::Underride(rule) => {
global.underride.replace(
ConditionalPushRuleInit {
actions: rule.actions,
default: false,
enabled: true,
rule_id: rule.rule_id,
conditions: rule.conditions,
}
.into(),
);
}
NewPushRule::Sender(rule) => {
global.sender.replace(
SimplePushRuleInit {
actions: rule.actions,
default: false,
enabled: true,
rule_id: rule.rule_id,
}
.into(),
);
}
NewPushRule::Room(rule) => {
global.room.replace(
SimplePushRuleInit {
actions: rule.actions,
default: false,
enabled: true,
rule_id: rule.rule_id,
}
.into(),
);
}
NewPushRule::Content(rule) => {
global.content.replace(
PatternedPushRuleInit {
actions: rule.actions,
default: false,
enabled: true,
rule_id: rule.rule_id,
pattern: rule.pattern,
}
.into(),
);
}
if let Err(error) = account_data.content.global.insert(
body.rule.clone(),
body.after.as_deref(),
body.before.as_deref(),
) {
let err = match error {
InsertPushRuleError::ServerDefaultRuleId => Error::BadRequest(
ErrorKind::InvalidParam,
"Rule IDs starting with a dot are reserved for server-default rules.",
),
InsertPushRuleError::InvalidRuleId => Error::BadRequest(
ErrorKind::InvalidParam,
"Rule ID containing invalid characters.",
),
InsertPushRuleError::RelativeToServerDefaultRule => Error::BadRequest(
ErrorKind::InvalidParam,
"Can't place a push rule relatively to a server-default rule.",
),
InsertPushRuleError::UnknownRuleId => Error::BadRequest(
ErrorKind::NotFound,
"The before or after rule could not be found.",
),
InsertPushRuleError::BeforeHigherThanAfter => Error::BadRequest(
ErrorKind::InvalidParam,
"The before rule has a higher priority than the after rule.",
),
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
};
return Err(err);
}
services().account_data.update(
@ -235,33 +185,15 @@ pub async fn get_pushrule_actions_route(
.content;
let global = account_data.global;
let actions = match body.kind {
RuleKind::Override => global
.override_
.get(body.rule_id.as_str())
.map(|rule| rule.actions.clone()),
RuleKind::Underride => global
.underride
.get(body.rule_id.as_str())
.map(|rule| rule.actions.clone()),
RuleKind::Sender => global
.sender
.get(body.rule_id.as_str())
.map(|rule| rule.actions.clone()),
RuleKind::Room => global
.room
.get(body.rule_id.as_str())
.map(|rule| rule.actions.clone()),
RuleKind::Content => global
.content
.get(body.rule_id.as_str())
.map(|rule| rule.actions.clone()),
_ => None,
};
let actions = global
.get(body.kind.clone(), &body.rule_id)
.map(|rule| rule.actions().to_owned())
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"Push rule not found.",
))?;
Ok(get_pushrule_actions::v3::Response {
actions: actions.unwrap_or_default(),
})
Ok(get_pushrule_actions::v3::Response { actions })
}
/// # `PUT /_matrix/client/r0/pushrules/{scope}/{kind}/{ruleId}/actions`
@ -294,40 +226,17 @@ pub async fn set_pushrule_actions_route(
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = &mut account_data.content.global;
match body.kind {
RuleKind::Override => {
if let Some(mut rule) = global.override_.get(body.rule_id.as_str()).cloned() {
rule.actions = body.actions.clone();
global.override_.replace(rule);
}
}
RuleKind::Underride => {
if let Some(mut rule) = global.underride.get(body.rule_id.as_str()).cloned() {
rule.actions = body.actions.clone();
global.underride.replace(rule);
}
}
RuleKind::Sender => {
if let Some(mut rule) = global.sender.get(body.rule_id.as_str()).cloned() {
rule.actions = body.actions.clone();
global.sender.replace(rule);
}
}
RuleKind::Room => {
if let Some(mut rule) = global.room.get(body.rule_id.as_str()).cloned() {
rule.actions = body.actions.clone();
global.room.replace(rule);
}
}
RuleKind::Content => {
if let Some(mut rule) = global.content.get(body.rule_id.as_str()).cloned() {
rule.actions = body.actions.clone();
global.content.replace(rule);
}
}
_ => {}
};
if account_data
.content
.global
.set_actions(body.kind.clone(), &body.rule_id, body.actions.clone())
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::NotFound,
"Push rule not found.",
));
}
services().account_data.update(
None,
@ -370,34 +279,13 @@ pub async fn get_pushrule_enabled_route(
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = account_data.content.global;
let enabled = match body.kind {
RuleKind::Override => global
.override_
.iter()
.find(|rule| rule.rule_id == body.rule_id)
.map_or(false, |rule| rule.enabled),
RuleKind::Underride => global
.underride
.iter()
.find(|rule| rule.rule_id == body.rule_id)
.map_or(false, |rule| rule.enabled),
RuleKind::Sender => global
.sender
.iter()
.find(|rule| rule.rule_id == body.rule_id)
.map_or(false, |rule| rule.enabled),
RuleKind::Room => global
.room
.iter()
.find(|rule| rule.rule_id == body.rule_id)
.map_or(false, |rule| rule.enabled),
RuleKind::Content => global
.content
.iter()
.find(|rule| rule.rule_id == body.rule_id)
.map_or(false, |rule| rule.enabled),
_ => false,
};
let enabled = global
.get(body.kind.clone(), &body.rule_id)
.map(|r| r.enabled())
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"Push rule not found.",
))?;
Ok(get_pushrule_enabled::v3::Response { enabled })
}
@ -432,44 +320,16 @@ pub async fn set_pushrule_enabled_route(
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = &mut account_data.content.global;
match body.kind {
RuleKind::Override => {
if let Some(mut rule) = global.override_.get(body.rule_id.as_str()).cloned() {
global.override_.remove(&rule);
rule.enabled = body.enabled;
global.override_.insert(rule);
}
}
RuleKind::Underride => {
if let Some(mut rule) = global.underride.get(body.rule_id.as_str()).cloned() {
global.underride.remove(&rule);
rule.enabled = body.enabled;
global.underride.insert(rule);
}
}
RuleKind::Sender => {
if let Some(mut rule) = global.sender.get(body.rule_id.as_str()).cloned() {
global.sender.remove(&rule);
rule.enabled = body.enabled;
global.sender.insert(rule);
}
}
RuleKind::Room => {
if let Some(mut rule) = global.room.get(body.rule_id.as_str()).cloned() {
global.room.remove(&rule);
rule.enabled = body.enabled;
global.room.insert(rule);
}
}
RuleKind::Content => {
if let Some(mut rule) = global.content.get(body.rule_id.as_str()).cloned() {
global.content.remove(&rule);
rule.enabled = body.enabled;
global.content.insert(rule);
}
}
_ => {}
if account_data
.content
.global
.set_enabled(body.kind.clone(), &body.rule_id, body.enabled)
.is_err()
{
return Err(Error::BadRequest(
ErrorKind::NotFound,
"Push rule not found.",
));
}
services().account_data.update(
@ -512,34 +372,23 @@ pub async fn delete_pushrule_route(
let mut account_data = serde_json::from_str::<PushRulesEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))?;
let global = &mut account_data.content.global;
match body.kind {
RuleKind::Override => {
if let Some(rule) = global.override_.get(body.rule_id.as_str()).cloned() {
global.override_.remove(&rule);
if let Err(error) = account_data
.content
.global
.remove(body.kind.clone(), &body.rule_id)
{
let err = match error {
RemovePushRuleError::ServerDefault => Error::BadRequest(
ErrorKind::InvalidParam,
"Cannot delete a server-default pushrule.",
),
RemovePushRuleError::NotFound => {
Error::BadRequest(ErrorKind::NotFound, "Push rule not found.")
}
}
RuleKind::Underride => {
if let Some(rule) = global.underride.get(body.rule_id.as_str()).cloned() {
global.underride.remove(&rule);
}
}
RuleKind::Sender => {
if let Some(rule) = global.sender.get(body.rule_id.as_str()).cloned() {
global.sender.remove(&rule);
}
}
RuleKind::Room => {
if let Some(rule) = global.room.get(body.rule_id.as_str()).cloned() {
global.room.remove(&rule);
}
}
RuleKind::Content => {
if let Some(rule) = global.content.get(body.rule_id.as_str()).cloned() {
global.content.remove(&rule);
}
}
_ => {}
_ => Error::BadRequest(ErrorKind::InvalidParam, "Invalid data."),
};
return Err(err);
}
services().account_data.update(

View File

@ -1,4 +1,4 @@
use crate::{services, Error, Result, Ruma};
use crate::{service::rooms::timeline::PduCount, services, Error, Result, Ruma};
use ruma::{
api::client::{error::ErrorKind, read_marker::set_read_marker, receipt::create_receipt},
events::{
@ -42,18 +42,28 @@ pub async fn set_read_marker_route(
}
if let Some(event) = &body.private_read_receipt {
services().rooms.edus.read_receipt.private_read_set(
&body.room_id,
sender_user,
services()
.rooms
.timeline
.get_pdu_count(event)?
.ok_or(Error::BadRequest(
let count = services()
.rooms
.timeline
.get_pdu_count(event)?
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Event does not exist.",
))?;
let count = match count {
PduCount::Backfilled(_) => {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Event does not exist.",
))?,
)?;
"Read receipt is in backfilled timeline",
))
}
PduCount::Normal(c) => c,
};
services()
.rooms
.edus
.read_receipt
.private_read_set(&body.room_id, sender_user, count)?;
}
if let Some(event) = &body.read_receipt {
@ -142,17 +152,27 @@ pub async fn create_receipt_route(
)?;
}
create_receipt::v3::ReceiptType::ReadPrivate => {
let count = services()
.rooms
.timeline
.get_pdu_count(&body.event_id)?
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Event does not exist.",
))?;
let count = match count {
PduCount::Backfilled(_) => {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Read receipt is in backfilled timeline",
))
}
PduCount::Normal(c) => c,
};
services().rooms.edus.read_receipt.private_read_set(
&body.room_id,
sender_user,
services()
.rooms
.timeline
.get_pdu_count(&body.event_id)?
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Event does not exist.",
))?,
count,
)?;
}
_ => return Err(Error::bad_database("Unsupported receipt type")),

View File

@ -3,7 +3,7 @@ use std::sync::Arc;
use crate::{service::pdu::PduBuilder, services, Result, Ruma};
use ruma::{
api::client::redact::redact_event,
events::{room::redaction::RoomRedactionEventContent, RoomEventType},
events::{room::redaction::RoomRedactionEventContent, TimelineEventType},
};
use serde_json::value::to_raw_value;
@ -32,7 +32,7 @@ pub async fn redact_event_route(
let event_id = services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomRedaction,
event_type: TimelineEventType::RoomRedaction,
content: to_raw_value(&RoomRedactionEventContent {
reason: body.reason.clone(),
})

View File

@ -0,0 +1,146 @@
use ruma::api::client::relations::{
get_relating_events, get_relating_events_with_rel_type,
get_relating_events_with_rel_type_and_event_type,
};
use crate::{service::rooms::timeline::PduCount, services, Result, Ruma};
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}/{eventType}`
pub async fn get_relating_events_with_rel_type_and_event_type_route(
body: Ruma<get_relating_events_with_rel_type_and_event_type::v1::Request>,
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let from = match body.from.clone() {
Some(from) => PduCount::try_from_string(&from)?,
None => match ruma::api::Direction::Backward {
// TODO: fix ruma so `body.dir` exists
ruma::api::Direction::Forward => PduCount::min(),
ruma::api::Direction::Backward => PduCount::max(),
},
};
let to = body
.to
.as_ref()
.and_then(|t| PduCount::try_from_string(&t).ok());
// Use limit or else 10, with maximum 100
let limit = body
.limit
.and_then(|u| u32::try_from(u).ok())
.map_or(10_usize, |u| u as usize)
.min(100);
let res = services()
.rooms
.pdu_metadata
.paginate_relations_with_filter(
sender_user,
&body.room_id,
&body.event_id,
Some(body.event_type.clone()),
Some(body.rel_type.clone()),
from,
to,
limit,
)?;
Ok(
get_relating_events_with_rel_type_and_event_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
},
)
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}/{relType}`
pub async fn get_relating_events_with_rel_type_route(
body: Ruma<get_relating_events_with_rel_type::v1::Request>,
) -> Result<get_relating_events_with_rel_type::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let from = match body.from.clone() {
Some(from) => PduCount::try_from_string(&from)?,
None => match ruma::api::Direction::Backward {
// TODO: fix ruma so `body.dir` exists
ruma::api::Direction::Forward => PduCount::min(),
ruma::api::Direction::Backward => PduCount::max(),
},
};
let to = body
.to
.as_ref()
.and_then(|t| PduCount::try_from_string(&t).ok());
// Use limit or else 10, with maximum 100
let limit = body
.limit
.and_then(|u| u32::try_from(u).ok())
.map_or(10_usize, |u| u as usize)
.min(100);
let res = services()
.rooms
.pdu_metadata
.paginate_relations_with_filter(
sender_user,
&body.room_id,
&body.event_id,
None,
Some(body.rel_type.clone()),
from,
to,
limit,
)?;
Ok(get_relating_events_with_rel_type::v1::Response {
chunk: res.chunk,
next_batch: res.next_batch,
prev_batch: res.prev_batch,
})
}
/// # `GET /_matrix/client/r0/rooms/{roomId}/relations/{eventId}`
pub async fn get_relating_events_route(
body: Ruma<get_relating_events::v1::Request>,
) -> Result<get_relating_events::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let from = match body.from.clone() {
Some(from) => PduCount::try_from_string(&from)?,
None => match ruma::api::Direction::Backward {
// TODO: fix ruma so `body.dir` exists
ruma::api::Direction::Forward => PduCount::min(),
ruma::api::Direction::Backward => PduCount::max(),
},
};
let to = body
.to
.as_ref()
.and_then(|t| PduCount::try_from_string(&t).ok());
// Use limit or else 10, with maximum 100
let limit = body
.limit
.and_then(|u| u32::try_from(u).ok())
.map_or(10_usize, |u| u as usize)
.min(100);
services()
.rooms
.pdu_metadata
.paginate_relations_with_filter(
sender_user,
&body.room_id,
&body.event_id,
None,
None,
from,
to,
limit,
)
}

View File

@ -19,7 +19,7 @@ use ruma::{
tombstone::RoomTombstoneEventContent,
topic::RoomTopicEventContent,
},
RoomEventType, StateEventType,
StateEventType, TimelineEventType,
},
int,
serde::JsonObject,
@ -175,7 +175,7 @@ pub async fn create_room_route(
// 1. The room create event
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomCreate,
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
@ -189,7 +189,7 @@ pub async fn create_room_route(
// 2. Let the room creator join
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services().users.displayname(sender_user)?,
@ -247,7 +247,7 @@ pub async fn create_room_route(
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomPowerLevels,
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_content)
.expect("to_raw_value always works on serde_json::Value"),
unsigned: None,
@ -263,7 +263,7 @@ pub async fn create_room_route(
if let Some(room_alias_id) = &alias {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomCanonicalAlias,
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(room_alias_id.to_owned()),
alt_aliases: vec![],
@ -284,7 +284,7 @@ pub async fn create_room_route(
// 5.1 Join Rules
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomJoinRules,
event_type: TimelineEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(match preset {
RoomPreset::PublicChat => JoinRule::Public,
// according to spec "invite" is the default
@ -303,7 +303,7 @@ pub async fn create_room_route(
// 5.2 History Visibility
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomHistoryVisibility,
event_type: TimelineEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
@ -320,7 +320,7 @@ pub async fn create_room_route(
// 5.3 Guest Access
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomGuestAccess,
event_type: TimelineEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(match preset {
RoomPreset::PublicChat => GuestAccess::Forbidden,
_ => GuestAccess::CanJoin,
@ -346,7 +346,7 @@ pub async fn create_room_route(
pdu_builder.state_key.get_or_insert_with(|| "".to_owned());
// Silently skip encryption events if they are not allowed
if pdu_builder.event_type == RoomEventType::RoomEncryption
if pdu_builder.event_type == TimelineEventType::RoomEncryption
&& !services().globals.allow_encryption()
{
continue;
@ -364,7 +364,7 @@ pub async fn create_room_route(
if let Some(name) = &body.name {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomName,
event_type: TimelineEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(Some(name.clone())))
.expect("event is valid, we just created it"),
unsigned: None,
@ -380,7 +380,7 @@ pub async fn create_room_route(
if let Some(topic) = &body.topic {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomTopic,
event_type: TimelineEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: topic.clone(),
})
@ -425,24 +425,31 @@ pub async fn get_room_event_route(
) -> Result<get_room_event::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if !services()
let event = services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
{
.timeline
.get_pdu(&body.event_id)?
.ok_or_else(|| {
warn!("Event not found, event ID: {:?}", &body.event_id);
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
})?;
if !services().rooms.state_accessor.user_can_see_event(
sender_user,
&event.room_id,
&body.event_id,
)? {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"You don't have permission to view this room.",
"You don't have permission to view this event.",
));
}
let mut event = (*event).clone();
event.add_age()?;
Ok(get_room_event::v3::Response {
event: services()
.rooms
.timeline
.get_pdu(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Event not found."))?
.to_room_event(),
event: event.to_room_event(),
})
}
@ -525,7 +532,7 @@ pub async fn upgrade_room_route(
// Fail if the sender does not have the required permissions
let tombstone_event_id = services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomTombstone,
event_type: TimelineEventType::RoomTombstone,
content: to_raw_value(&RoomTombstoneEventContent {
body: "This room has been replaced".to_owned(),
replacement_room: replacement_room.clone(),
@ -607,7 +614,7 @@ pub async fn upgrade_room_route(
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomCreate,
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&create_event_content)
.expect("event is valid, we just created it"),
unsigned: None,
@ -622,7 +629,7 @@ pub async fn upgrade_room_route(
// Join the new room
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: services().users.displayname(sender_user)?,
@ -715,7 +722,7 @@ pub async fn upgrade_room_route(
// Modify the power levels in the old room to prevent sending of events and inviting new users
let _ = services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomPowerLevels,
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&power_levels_event_content)
.expect("event is valid, we just created it"),
unsigned: None,

View File

@ -31,7 +31,8 @@ pub async fn search_events_route(
.collect()
});
let limit = filter.limit.map_or(10, |l| u64::from(l) as usize);
// Use limit or else 10, with maximum 100
let limit = filter.limit.map_or(10, u64::from).min(100) as usize;
let mut searches = Vec::new();
@ -81,6 +82,21 @@ pub async fn search_events_route(
let results: Vec<_> = results
.iter()
.filter_map(|result| {
services()
.rooms
.timeline
.get_pdu_from_id(result)
.ok()?
.filter(|pdu| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &pdu.room_id, &pdu.event_id)
.unwrap_or(false)
})
.map(|pdu| pdu.to_room_event())
})
.map(|result| {
Ok::<_, Error>(SearchResult {
context: EventContextResult {
@ -91,11 +107,7 @@ pub async fn search_events_route(
start: None,
},
rank: None,
result: services()
.rooms
.timeline
.get_pdu_from_id(result)?
.map(|pdu| pdu.to_room_event()),
result: Some(result),
})
})
.filter_map(|r| r.ok())

View File

@ -9,7 +9,7 @@ use ruma::{
UserId,
};
use serde::Deserialize;
use tracing::info;
use tracing::{info, warn};
#[derive(Debug, Deserialize)]
struct Claims {
@ -26,6 +26,7 @@ pub async fn get_login_types_route(
) -> Result<get_login_types::v3::Response> {
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(Default::default()),
get_login_types::v3::LoginType::ApplicationService(Default::default()),
]))
}
@ -51,6 +52,7 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
let username = if let UserIdentifier::UserIdOrLocalpart(user_id) = identifier {
user_id.to_lowercase()
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::Forbidden, "Bad login type."));
};
let user_id =
@ -103,7 +105,27 @@ pub async fn login_route(body: Ruma<login::v3::Request>) -> Result<login::v3::Re
));
}
}
login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService { identifier }) => {
if !body.from_appservice {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Forbidden login type.",
));
};
let username = if let UserIdentifier::UserIdOrLocalpart(user_id) = identifier {
user_id.to_lowercase()
} else {
return Err(Error::BadRequest(ErrorKind::Forbidden, "Bad login type."));
};
let user_id =
UserId::parse_with_server_name(username, services().globals.server_name())
.map_err(|_| {
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
user_id
}
_ => {
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Unsupported login type.",

View File

@ -0,0 +1,34 @@
use crate::{services, Result, Ruma};
use ruma::api::client::space::get_hierarchy;
/// # `GET /_matrix/client/v1/rooms/{room_id}/hierarchy``
///
/// Paginates over the space tree in a depth-first manner to locate child rooms of a given space.
pub async fn get_hierarchy_route(
body: Ruma<get_hierarchy::v1::Request>,
) -> Result<get_hierarchy::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
let skip = body
.from
.as_ref()
.and_then(|s| s.parse::<usize>().ok())
.unwrap_or(0);
let limit = body.limit.map_or(10, u64::from).min(100) as usize;
let max_depth = body.max_depth.map_or(3, u64::from).min(10) as usize + 1; // +1 to skip the space room itself
services()
.rooms
.spaces
.get_hierarchy(
sender_user,
&body.room_id,
limit,
skip,
max_depth,
body.suggested_only,
)
.await
}

View File

@ -7,15 +7,12 @@ use ruma::{
state::{get_state_events, get_state_events_for_key, send_state_event},
},
events::{
room::{
canonical_alias::RoomCanonicalAliasEventContent,
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
},
AnyStateEventContent, StateEventType,
room::canonical_alias::RoomCanonicalAliasEventContent, AnyStateEventContent, StateEventType,
},
serde::Raw,
EventId, RoomId, UserId,
};
use tracing::log::warn;
/// # `PUT /_matrix/client/r0/rooms/{roomId}/state/{eventType}/{stateKey}`
///
@ -85,29 +82,10 @@ pub async fn get_state_events_route(
) -> Result<get_state_events::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
// Users not in the room should not be able to access the state unless history_visibility is
// WorldReadable
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
&& !matches!(
services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})
}),
Some(Ok(HistoryVisibility::WorldReadable))
)
.state_accessor
.user_can_see_state_events(&sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
@ -137,29 +115,10 @@ pub async fn get_state_events_for_key_route(
) -> Result<get_state_events_for_key::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
// Users not in the room should not be able to access the state unless history_visibility is
// WorldReadable
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
&& !matches!(
services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})
}),
Some(Ok(HistoryVisibility::WorldReadable))
)
.state_accessor
.user_can_see_state_events(&sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
@ -171,10 +130,13 @@ pub async fn get_state_events_for_key_route(
.rooms
.state_accessor
.room_state_get(&body.room_id, &body.event_type, &body.state_key)?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"State event not found.",
))?;
.ok_or_else(|| {
warn!(
"State event {:?} not found in room {:?}",
&body.event_type, &body.room_id
);
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
})?;
Ok(get_state_events_for_key::v3::Response {
content: serde_json::from_str(event.content.get())
@ -192,29 +154,10 @@ pub async fn get_state_events_for_empty_key_route(
) -> Result<RumaResponse<get_state_events_for_key::v3::Response>> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
#[allow(clippy::blocks_in_if_conditions)]
// Users not in the room should not be able to access the state unless history_visibility is
// WorldReadable
if !services()
.rooms
.state_cache
.is_joined(sender_user, &body.room_id)?
&& !matches!(
services()
.rooms
.state_accessor
.room_state_get(&body.room_id, &StateEventType::RoomHistoryVisibility, "")?
.map(|event| {
serde_json::from_str(event.content.get())
.map(|e: RoomHistoryVisibilityEventContent| e.history_visibility)
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})
}),
Some(Ok(HistoryVisibility::WorldReadable))
)
.state_accessor
.user_can_see_state_events(&sender_user, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
@ -226,10 +169,13 @@ pub async fn get_state_events_for_empty_key_route(
.rooms
.state_accessor
.room_state_get(&body.room_id, &body.event_type, "")?
.ok_or(Error::BadRequest(
ErrorKind::NotFound,
"State event not found.",
))?;
.ok_or_else(|| {
warn!(
"State event {:?} not found in room {:?}",
&body.event_type, &body.room_id
);
Error::BadRequest(ErrorKind::NotFound, "State event not found.")
})?;
Ok(get_state_events_for_key::v3::Response {
content: serde_json::from_str(event.content.get())

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,49 @@
use ruma::api::client::{error::ErrorKind, threads::get_threads};
use crate::{services, Error, Result, Ruma};
/// # `GET /_matrix/client/r0/rooms/{roomId}/threads`
pub async fn get_threads_route(
body: Ruma<get_threads::v1::Request>,
) -> Result<get_threads::v1::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// Use limit or else 10, with maximum 100
let limit = body
.limit
.and_then(|l| l.try_into().ok())
.unwrap_or(10)
.min(100);
let from = if let Some(from) = &body.from {
from.parse()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, ""))?
} else {
u64::MAX
};
let threads = services()
.rooms
.threads
.threads_until(sender_user, &body.room_id, from, &body.include)?
.take(limit)
.filter_map(|r| r.ok())
.filter(|(_, pdu)| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &body.room_id, &pdu.event_id)
.unwrap_or(false)
})
.collect::<Vec<_>>();
let next_batch = threads.last().map(|(count, _)| count.to_string());
Ok(get_threads::v1::Response {
chunk: threads
.into_iter()
.map(|(_, pdu)| pdu.to_room_event())
.collect(),
next_batch,
})
}

View File

@ -1,4 +1,3 @@
use ruma::events::ToDeviceEventType;
use std::collections::BTreeMap;
use crate::{services, Error, Result, Ruma};
@ -42,7 +41,7 @@ pub async fn send_event_to_device_route(
serde_json::to_vec(&federation::transactions::edu::Edu::DirectToDevice(
DirectDeviceContent {
sender: sender_user.clone(),
ev_type: ToDeviceEventType::from(&*body.event_type),
ev_type: body.event_type.clone(),
message_id: count.to_string().into(),
messages,
},
@ -60,7 +59,7 @@ pub async fn send_event_to_device_route(
sender_user,
target_user_id,
target_device_id,
&body.event_type,
&body.event_type.to_string(),
event.deserialize_as().map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
})?,
@ -73,7 +72,7 @@ pub async fn send_event_to_device_route(
sender_user,
target_user_id,
&target_device_id?,
&body.event_type,
&body.event_type.to_string(),
event.deserialize_as().map_err(|_| {
Error::BadRequest(ErrorKind::InvalidParam, "Event is invalid")
})?,

View File

@ -1,8 +1,9 @@
use std::{collections::BTreeMap, iter::FromIterator};
use ruma::api::client::discovery::get_supported_versions;
use axum::{response::IntoResponse, Json};
use ruma::api::client::{discovery::get_supported_versions, error::ErrorKind};
use crate::{Result, Ruma};
use crate::{services, Error, Result, Ruma};
/// # `GET /_matrix/client/versions`
///
@ -23,9 +24,26 @@ pub async fn get_supported_versions_route(
"r0.6.0".to_owned(),
"v1.1".to_owned(),
"v1.2".to_owned(),
"v1.3".to_owned(),
"v1.4".to_owned(),
],
unstable_features: BTreeMap::from_iter([("org.matrix.e2e_cross_signing".to_owned(), true)]),
};
Ok(resp)
}
/// # `GET /.well-known/matrix/client`
pub async fn well_known_client_route(
_body: Ruma<get_supported_versions::Request>,
) -> Result<impl IntoResponse> {
let client_url = match services().globals.well_known_client() {
Some(url) => url.clone(),
None => return Err(Error::BadRequest(ErrorKind::NotFound, "Not found.")),
};
Ok(Json(serde_json::json!({
"m.homeserver": {"base_url": client_url},
"org.matrix.msc3575.proxy": {"url": client_url}
})))
}

View File

@ -1,4 +1,5 @@
use crate::{services, Result, Ruma};
use base64::{engine::general_purpose, Engine as _};
use hmac::{Hmac, Mac};
use ruma::{api::client::voip::get_turn_server_info, SecondsSinceUnixEpoch};
use sha1::Sha1;
@ -28,7 +29,7 @@ pub async fn turn_server_route(
.expect("HMAC can take key of any size");
mac.update(username.as_bytes());
let password: String = base64::encode_config(mac.finalize().into_bytes(), base64::STANDARD);
let password: String = general_purpose::STANDARD.encode(mac.finalize().into_bytes());
(username, password)
} else {

View File

@ -3,18 +3,16 @@ use std::{collections::BTreeMap, iter::FromIterator, str};
use axum::{
async_trait,
body::{Full, HttpBody},
extract::{
rejection::TypedHeaderRejectionReason, FromRequest, Path, RequestParts, TypedHeader,
},
extract::{rejection::TypedHeaderRejectionReason, FromRequest, Path, TypedHeader},
headers::{
authorization::{Bearer, Credentials},
Authorization,
},
response::{IntoResponse, Response},
BoxError,
BoxError, RequestExt, RequestPartsExt,
};
use bytes::{BufMut, Bytes, BytesMut};
use http::StatusCode;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use http::{Request, StatusCode};
use ruma::{
api::{client::error::ErrorKind, AuthScheme, IncomingRequest, OutgoingResponse},
CanonicalJsonValue, OwnedDeviceId, OwnedServerName, UserId,
@ -26,28 +24,45 @@ use super::{Ruma, RumaResponse};
use crate::{services, Error, Result};
#[async_trait]
impl<T, B> FromRequest<B> for Ruma<T>
impl<T, S, B> FromRequest<S, B> for Ruma<T>
where
T: IncomingRequest,
B: HttpBody + Send,
B: HttpBody + Send + 'static,
B::Data: Send,
B::Error: Into<BoxError>,
{
type Rejection = Error;
async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
async fn from_request(req: Request<B>, _state: &S) -> Result<Self, Self::Rejection> {
#[derive(Deserialize)]
struct QueryParams {
access_token: Option<String>,
user_id: Option<String>,
}
let metadata = T::METADATA;
let auth_header = Option::<TypedHeader<Authorization<Bearer>>>::from_request(req).await?;
let path_params = Path::<Vec<String>>::from_request(req).await?;
let (mut parts, mut body) = match req.with_limited_body() {
Ok(limited_req) => {
let (parts, body) = limited_req.into_parts();
let body = to_bytes(body)
.await
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
(parts, body)
}
Err(original_req) => {
let (parts, body) = original_req.into_parts();
let body = to_bytes(body)
.await
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
(parts, body)
}
};
let query = req.uri().query().unwrap_or_default();
let query_params: QueryParams = match ruma::serde::urlencoded::from_str(query) {
let metadata = T::METADATA;
let auth_header: Option<TypedHeader<Authorization<Bearer>>> = parts.extract().await?;
let path_params: Path<Vec<String>> = parts.extract().await?;
let query = parts.uri.query().unwrap_or_default();
let query_params: QueryParams = match serde_html_form::from_str(query) {
Ok(params) => params,
Err(e) => {
error!(%query, "Failed to deserialize query parameters: {}", e);
@ -63,10 +78,6 @@ where
None => query_params.access_token.as_deref(),
};
let mut body = Bytes::from_request(req)
.await
.map_err(|_| Error::BadRequest(ErrorKind::MissingToken, "Missing token."))?;
let mut json_body = serde_json::from_slice::<CanonicalJsonValue>(&body).ok();
let appservices = services().appservice.all().unwrap();
@ -138,24 +149,24 @@ where
}
}
AuthScheme::ServerSignatures => {
let TypedHeader(Authorization(x_matrix)) =
TypedHeader::<Authorization<XMatrix>>::from_request(req)
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {}", e);
let TypedHeader(Authorization(x_matrix)) = parts
.extract::<TypedHeader<Authorization<XMatrix>>>()
.await
.map_err(|e| {
warn!("Missing or invalid Authorization header: {}", e);
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => {
"Missing Authorization header."
}
TypedHeaderRejectionReason::Error(_) => {
"Invalid X-Matrix signatures."
}
_ => "Unknown header-related error",
};
let msg = match e.reason() {
TypedHeaderRejectionReason::Missing => {
"Missing Authorization header."
}
TypedHeaderRejectionReason::Error(_) => {
"Invalid X-Matrix signatures."
}
_ => "Unknown header-related error",
};
Error::BadRequest(ErrorKind::Forbidden, msg)
})?;
Error::BadRequest(ErrorKind::Forbidden, msg)
})?;
let origin_signatures = BTreeMap::from_iter([(
x_matrix.key.clone(),
@ -170,11 +181,11 @@ where
let mut request_map = BTreeMap::from_iter([
(
"method".to_owned(),
CanonicalJsonValue::String(req.method().to_string()),
CanonicalJsonValue::String(parts.method.to_string()),
),
(
"uri".to_owned(),
CanonicalJsonValue::String(req.uri().to_string()),
CanonicalJsonValue::String(parts.uri.to_string()),
),
(
"origin".to_owned(),
@ -224,7 +235,7 @@ where
x_matrix.origin, e, request_map
);
if req.uri().to_string().contains('@') {
if parts.uri.to_string().contains('@') {
warn!(
"Request uri contained '@' character. Make sure your \
reverse proxy gives Conduit the raw uri (apache: use \
@ -243,8 +254,8 @@ where
}
};
let mut http_request = http::Request::builder().uri(req.uri()).method(req.method());
*http_request.headers_mut().unwrap() = req.headers().clone();
let mut http_request = http::Request::builder().uri(parts.uri).method(parts.method);
*http_request.headers_mut().unwrap() = parts.headers;
if let Some(CanonicalJsonValue::Object(json_body)) = &mut json_body {
let user_id = sender_user.clone().unwrap_or_else(|| {
@ -281,7 +292,8 @@ where
debug!("{:?}", http_request);
let body = T::try_from_http_request(http_request, &path_params).map_err(|e| {
warn!("{:?}\n{:?}", e, json_body);
warn!("try_from_http_request failed: {:?}", e);
debug!("JSON body: {:?}", json_body);
Error::BadRequest(ErrorKind::BadJson, "Failed to deserialize request.")
})?;
@ -361,3 +373,55 @@ impl<T: OutgoingResponse> IntoResponse for RumaResponse<T> {
}
}
}
// copied from hyper under the following license:
// Copyright (c) 2014-2021 Sean McArthur
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
pub(crate) async fn to_bytes<T>(body: T) -> Result<Bytes, T::Error>
where
T: HttpBody,
{
futures_util::pin_mut!(body);
// If there's only 1 chunk, we can just return Buf::to_bytes()
let mut first = if let Some(buf) = body.data().await {
buf?
} else {
return Ok(Bytes::new());
};
let second = if let Some(buf) = body.data().await {
buf?
} else {
return Ok(first.copy_to_bytes(first.remaining()));
};
// With more than 1 buf, we gotta flatten into a Vec first.
let cap = first.remaining() + second.remaining() + body.size_hint().lower() as usize;
let mut vec = Vec::with_capacity(cap);
vec.put(first);
vec.put(second);
while let Some(buf) = body.data().await {
vec.put(buf?);
}
Ok(vec.into())
}

View File

@ -1,3 +1,5 @@
#![allow(deprecated)]
use crate::{
api::client_server::{self, claim_keys_helper, get_keys_helper},
service::pdu::{gen_event_id_canonical_json, PduBuilder},
@ -12,16 +14,13 @@ use ruma::{
client::error::{Error as RumaError, ErrorKind},
federation::{
authorization::get_event_authorization,
backfill::get_backfill,
device::get_devices::{self, v1::UserDevice},
directory::{get_public_rooms, get_public_rooms_filtered},
discovery::{get_server_keys, get_server_version, ServerSigningKeys, VerifyKey},
event::{get_event, get_missing_events, get_room_state, get_room_state_ids},
keys::{claim_keys, get_keys},
membership::{
create_invite,
create_join_event::{self, RoomState},
prepare_join_event,
},
membership::{create_invite, create_join_event, prepare_join_event},
query::{get_profile_information, get_room_information},
transactions::{
edu::{DeviceListUpdateContent, DirectDeviceContent, Edu, SigningKeyUpdateContent},
@ -38,12 +37,13 @@ use ruma::{
join_rules::{JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent},
},
RoomEventType, StateEventType,
StateEventType, TimelineEventType,
},
serde::{Base64, JsonObject, Raw},
to_device::DeviceIdOrAllDevices,
CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch, OwnedEventId,
OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId, ServerName,
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId, RoomId,
ServerName,
};
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use std::{
@ -55,7 +55,7 @@ use std::{
time::{Duration, Instant, SystemTime},
};
use tracing::{debug, error, info, warn};
use tracing::{debug, error, warn};
/// Wraps either an literal IP address plus port, or a hostname plus complement
/// (colon-plus-port if it was specified).
@ -123,6 +123,14 @@ where
return Err(Error::bad_config("Federation is disabled."));
}
if destination == services().globals.server_name() {
return Err(Error::bad_config(
"Won't send federation request to ourselves",
));
}
debug!("Preparing to send request to {destination}");
let mut write_destination_to_cache = false;
let cached_result = services()
@ -149,7 +157,7 @@ where
.try_into_http_request::<Vec<u8>>(
&actual_destination_str,
SendAccessToken::IfRequired(""),
&[MatrixVersion::V1_0],
&[MatrixVersion::V1_4],
)
.map_err(|e| {
warn!(
@ -229,11 +237,13 @@ where
let url = reqwest_request.url().clone();
debug!("Sending request to {destination} at {url}");
let response = services()
.globals
.federation_client()
.execute(reqwest_request)
.await;
debug!("Received response from {destination} at {url}");
match response {
Ok(mut response) => {
@ -249,10 +259,12 @@ where
.expect("http::response::Builder is usable"),
);
debug!("Getting response bytes from {destination}");
let body = response.bytes().await.unwrap_or_else(|e| {
warn!("server error {}", e);
Vec::new().into()
}); // TODO: handle timeout
debug!("Got response bytes from {destination}");
if status != 200 {
warn!(
@ -271,6 +283,7 @@ where
.expect("reqwest body is valid http body");
if status == 200 {
debug!("Parsing response bytes from {destination}");
let response = T::IncomingResponse::try_from_http_response(http_response);
if response.is_ok() && write_destination_to_cache {
services()
@ -292,6 +305,7 @@ where
Error::BadServerResponse("Server returned bad 200 response.")
})
} else {
debug!("Returning error from {destination}");
Err(Error::FederationError(
destination.to_owned(),
RumaError::from_http_response(http_response),
@ -330,36 +344,38 @@ fn add_port_to_hostname(destination_str: &str) -> FedDest {
/// Implemented according to the specification at https://matrix.org/docs/spec/server_server/r0.1.4#resolving-server-names
/// Numbers in comments below refer to bullet points in linked section of specification
async fn find_actual_destination(destination: &'_ ServerName) -> (FedDest, FedDest) {
debug!("Finding actual destination for {destination}");
let destination_str = destination.as_str().to_owned();
let mut hostname = destination_str.clone();
let actual_destination = match get_ip_with_port(&destination_str) {
Some(host_port) => {
// 1: IP literal with provided or default port
debug!("1: IP literal with provided or default port");
host_port
}
None => {
if let Some(pos) = destination_str.find(':') {
// 2: Hostname with included port
debug!("2: Hostname with included port");
let (host, port) = destination_str.split_at(pos);
FedDest::Named(host.to_owned(), port.to_owned())
} else {
debug!("Requesting well known for {destination}");
match request_well_known(destination.as_str()).await {
// 3: A .well-known file is available
Some(delegated_hostname) => {
debug!("3: A .well-known file is available");
hostname = add_port_to_hostname(&delegated_hostname).into_uri_string();
match get_ip_with_port(&delegated_hostname) {
Some(host_and_port) => host_and_port, // 3.1: IP literal in .well-known file
None => {
if let Some(pos) = delegated_hostname.find(':') {
// 3.2: Hostname with port in .well-known file
debug!("3.2: Hostname with port in .well-known file");
let (host, port) = delegated_hostname.split_at(pos);
FedDest::Named(host.to_owned(), port.to_owned())
} else {
// Delegated hostname has no port in this branch
debug!("Delegated hostname has no port in this branch");
if let Some(hostname_override) =
query_srv_record(&delegated_hostname).await
{
// 3.3: SRV lookup successful
debug!("3.3: SRV lookup successful");
let force_port = hostname_override.port();
if let Ok(override_ip) = services()
@ -390,18 +406,18 @@ async fn find_actual_destination(destination: &'_ ServerName) -> (FedDest, FedDe
add_port_to_hostname(&delegated_hostname)
}
} else {
// 3.4: No SRV records, just use the hostname from .well-known
debug!("3.4: No SRV records, just use the hostname from .well-known");
add_port_to_hostname(&delegated_hostname)
}
}
}
}
}
// 4: No .well-known or an error occured
None => {
debug!("4: No .well-known or an error occured");
match query_srv_record(&destination_str).await {
// 4: SRV record found
Some(hostname_override) => {
debug!("4: SRV record found");
let force_port = hostname_override.port();
if let Ok(override_ip) = services()
@ -432,14 +448,17 @@ async fn find_actual_destination(destination: &'_ ServerName) -> (FedDest, FedDe
add_port_to_hostname(&hostname)
}
}
// 5: No SRV record found
None => add_port_to_hostname(&destination_str),
None => {
debug!("5: No SRV record found");
add_port_to_hostname(&destination_str)
}
}
}
}
}
}
};
debug!("Actual destination: {actual_destination:?}");
// Can't use get_ip_with_port here because we don't want to add a port
// to an IP address if it wasn't specified
@ -457,10 +476,11 @@ async fn find_actual_destination(destination: &'_ ServerName) -> (FedDest, FedDe
}
async fn query_srv_record(hostname: &'_ str) -> Option<FedDest> {
let hostname = hostname.trim_end_matches('.');
if let Ok(Some(host_port)) = services()
.globals
.dns_resolver()
.srv_lookup(format!("_matrix._tcp.{hostname}"))
.srv_lookup(format!("_matrix._tcp.{hostname}."))
.await
.map(|srv| {
srv.iter().next().map(|result| {
@ -478,19 +498,20 @@ async fn query_srv_record(hostname: &'_ str) -> Option<FedDest> {
}
async fn request_well_known(destination: &str) -> Option<String> {
let body: serde_json::Value = serde_json::from_str(
&services()
.globals
.default_client()
.get(&format!("https://{destination}/.well-known/matrix/server"))
.send()
.await
.ok()?
.text()
.await
.ok()?,
)
.ok()?;
let response = services()
.globals
.default_client()
.get(&format!("https://{destination}/.well-known/matrix/server"))
.send()
.await;
debug!("Got well known response");
if let Err(e) = &response {
debug!("Well known error: {e:?}");
return None;
}
let text = response.ok()?.text().await;
debug!("Got well known response text");
let body: serde_json::Value = serde_json::from_str(&text.ok()?).ok()?;
Some(body.get("m.server")?.as_str()?.to_owned())
}
@ -627,6 +648,37 @@ pub async fn get_public_rooms_route(
})
}
pub fn parse_incoming_pdu(
pdu: &RawJsonValue,
) -> Result<(OwnedEventId, CanonicalJsonObject, OwnedRoomId)> {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
Error::BadServerResponse("Invalid PDU in server response")
})?;
let room_id: OwnedRoomId = value
.get("room_id")
.and_then(|id| RoomId::parse(id.as_str()?).ok())
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid room id in pdu",
))?;
let room_version_id = services().rooms.state.get_room_version(&room_id)?;
let (event_id, value) = match gen_event_id_canonical_json(&pdu, &room_version_id) {
Ok(t) => t,
Err(_) => {
// Event could not be converted to canonical json
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Could not convert event to canonical json.",
));
}
};
Ok((event_id, value, room_id))
}
/// # `PUT /_matrix/federation/v1/send/{txnId}`
///
/// Push EDUs and PDUs to this server.
@ -655,33 +707,12 @@ pub async fn send_transaction_message_route(
// let mut auth_cache = EventMap::new();
for pdu in &body.pdus {
let value: CanonicalJsonObject = serde_json::from_str(pdu.get()).map_err(|e| {
warn!("Error parsing incoming event {:?}: {:?}", pdu, e);
Error::BadServerResponse("Invalid PDU in server response")
})?;
let room_id: OwnedRoomId = match value
.get("room_id")
.and_then(|id| RoomId::parse(id.as_str()?).ok())
{
Some(id) => id,
None => {
// Event is invalid
continue;
}
};
let room_version_id = match services().rooms.state.get_room_version(&room_id) {
Ok(v) => v,
Err(_) => {
continue;
}
};
let (event_id, value) = match gen_event_id_canonical_json(pdu, &room_version_id) {
let r = parse_incoming_pdu(&pdu);
let (event_id, value, room_id) = match r {
Ok(t) => t,
Err(_) => {
// Event could not be converted to canonical json
Err(e) => {
warn!("Could not parse PDU: {e}");
warn!("Full PDU: {:?}", &pdu);
continue;
}
};
@ -782,7 +813,7 @@ pub async fn send_transaction_message_route(
.readreceipt_update(&user_id, &room_id, event)?;
} else {
// TODO fetch missing events
info!("No known event ids in read receipt: {:?}", user_updates);
debug!("No known event ids in read receipt: {:?}", user_updates);
}
}
}
@ -886,6 +917,7 @@ pub async fn send_transaction_message_route(
&master_key,
&self_signing_key,
&None,
true,
)?;
}
}
@ -896,7 +928,7 @@ pub async fn send_transaction_message_route(
Ok(send_transaction_message::v1::Response {
pdus: resolved_map
.into_iter()
.map(|(e, r)| (e, r.map_err(|e| e.to_string())))
.map(|(e, r)| (e, r.map_err(|e| e.sanitized_error())))
.collect(),
})
}
@ -922,7 +954,10 @@ pub async fn get_event_route(
.rooms
.timeline
.get_pdu_json(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Event not found."))?;
.ok_or_else(|| {
warn!("Event not found, event ID: {:?}", &body.event_id);
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
})?;
let room_id_str = event
.get("room_id")
@ -943,6 +978,17 @@ pub async fn get_event_route(
));
}
if !services().rooms.state_accessor.server_can_see_event(
sender_servername,
&room_id,
&body.event_id,
)? {
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Server is not allowed to see event.",
));
}
Ok(get_event::v1::Response {
origin: services().globals.server_name().to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
@ -950,6 +996,83 @@ pub async fn get_event_route(
})
}
/// # `GET /_matrix/federation/v1/backfill/<room_id>`
///
/// Retrieves events from before the sender joined the room, if the room's
/// history visibility allows.
pub async fn get_backfill_route(
body: Ruma<get_backfill::v1::Request>,
) -> Result<get_backfill::v1::Response> {
if !services().globals.allow_federation() {
return Err(Error::bad_config("Federation is disabled."));
}
let sender_servername = body
.sender_servername
.as_ref()
.expect("server is authenticated");
debug!("Got backfill request from: {}", sender_servername);
if !services()
.rooms
.state_cache
.server_in_room(sender_servername, &body.room_id)?
{
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Server is not in room.",
));
}
services()
.rooms
.event_handler
.acl_check(sender_servername, &body.room_id)?;
let until = body
.v
.iter()
.map(|eventid| services().rooms.timeline.get_pdu_count(eventid))
.filter_map(|r| r.ok().flatten())
.max()
.ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"No known eventid in v",
))?;
let limit = body.limit.min(uint!(100));
let all_events = services()
.rooms
.timeline
.pdus_until(&user_id!("@doesntmatter:conduit.rs"), &body.room_id, until)?
.take(limit.try_into().unwrap());
let events = all_events
.filter_map(|r| r.ok())
.filter(|(_, e)| {
matches!(
services().rooms.state_accessor.server_can_see_event(
sender_servername,
&e.room_id,
&e.event_id,
),
Ok(true),
)
})
.map(|(_, pdu)| services().rooms.timeline.get_pdu_json(&pdu.event_id))
.filter_map(|r| r.ok().flatten())
.map(|pdu| PduEvent::convert_to_outgoing_federation_event(pdu))
.collect();
Ok(get_backfill::v1::Response {
origin: services().globals.server_name().to_owned(),
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
pdus: events,
})
}
/// # `POST /_matrix/federation/v1/get_missing_events/{roomId}`
///
/// Retrieves events that the sender is missing.
@ -1010,6 +1133,16 @@ pub async fn get_missing_events_route(
i += 1;
continue;
}
if !services().rooms.state_accessor.server_can_see_event(
sender_servername,
&body.room_id,
&queued_events[i],
)? {
i += 1;
continue;
}
queued_events.extend_from_slice(
&serde_json::from_value::<Vec<OwnedEventId>>(
serde_json::to_value(pdu.get("prev_events").cloned().ok_or_else(|| {
@ -1064,7 +1197,10 @@ pub async fn get_event_authorization_route(
.rooms
.timeline
.get_pdu_json(&body.event_id)?
.ok_or(Error::BadRequest(ErrorKind::NotFound, "Event not found."))?;
.ok_or_else(|| {
warn!("Event not found, event ID: {:?}", &body.event_id);
Error::BadRequest(ErrorKind::NotFound, "Event not found.")
})?;
let room_id_str = event
.get("room_id")
@ -1320,7 +1456,7 @@ pub async fn create_join_event_template_route(
let (_pdu, mut pdu_json) = services().rooms.timeline.create_hash_and_sign_event(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content,
unsigned: None,
state_key: Some(body.user_id.to_string()),
@ -1345,7 +1481,7 @@ async fn create_join_event(
sender_servername: &ServerName,
room_id: &RoomId,
pdu: &RawJsonValue,
) -> Result<RoomState> {
) -> Result<create_join_event::v1::RoomState> {
if !services().globals.allow_federation() {
return Err(Error::bad_config("Federation is disabled."));
}
@ -1467,7 +1603,7 @@ async fn create_join_event(
services().sending.send_pdu(servers, &pdu_id)?;
Ok(RoomState {
Ok(create_join_event::v1::RoomState {
auth_chain: auth_chain_ids
.filter_map(|id| services().rooms.timeline.get_pdu_json(&id).ok().flatten())
.map(PduEvent::convert_to_outgoing_federation_event)
@ -1508,7 +1644,18 @@ pub async fn create_join_event_v2_route(
.as_ref()
.expect("server is authenticated");
let room_state = create_join_event(sender_servername, &body.room_id, &body.pdu).await?;
let create_join_event::v1::RoomState {
auth_chain,
state,
event,
} = create_join_event(sender_servername, &body.room_id, &body.pdu).await?;
let room_state = create_join_event::v2::RoomState {
members_omitted: false,
auth_chain,
state,
event,
servers_in_room: None,
};
Ok(create_join_event::v2::Response { room_state })
}
@ -1668,12 +1815,14 @@ pub async fn get_devices_route(
})
})
.collect(),
master_key: services()
.users
.get_master_key(&body.user_id, &|u| u.server_name() == sender_servername)?,
master_key: services().users.get_master_key(None, &body.user_id, &|u| {
u.server_name() == sender_servername
})?,
self_signing_key: services()
.users
.get_self_signing_key(&body.user_id, &|u| u.server_name() == sender_servername)?,
.get_self_signing_key(None, &body.user_id, &|u| {
u.server_name() == sender_servername
})?,
})
}

View File

@ -28,6 +28,8 @@ pub struct Config {
pub db_cache_capacity_mb: f64,
#[serde(default = "true_fn")]
pub enable_lightning_bolt: bool,
#[serde(default = "true_fn")]
pub allow_check_for_updates: bool,
#[serde(default = "default_conduit_cache_capacity_modifier")]
pub conduit_cache_capacity_modifier: f64,
#[serde(default = "default_rocksdb_max_open_files")]
@ -44,6 +46,7 @@ pub struct Config {
pub max_fetch_prev_events: u16,
#[serde(default = "false_fn")]
pub allow_registration: bool,
pub registration_token: Option<String>,
#[serde(default = "true_fn")]
pub allow_encryption: bool,
#[serde(default = "false_fn")]
@ -54,6 +57,7 @@ pub struct Config {
pub allow_unstable_room_versions: bool,
#[serde(default = "default_default_room_version")]
pub default_room_version: RoomVersionId,
pub well_known_client: Option<String>,
#[serde(default = "false_fn")]
pub allow_jaeger: bool,
#[serde(default = "false_fn")]
@ -61,7 +65,7 @@ pub struct Config {
#[serde(default)]
pub proxy: ProxyConfig,
pub jwt_secret: Option<String>,
#[serde(default = "Vec::new")]
#[serde(default = "default_trusted_servers")]
pub trusted_servers: Vec<OwnedServerName>,
#[serde(default = "default_log")]
pub log: String,
@ -224,7 +228,7 @@ fn default_database_backend() -> String {
}
fn default_db_cache_capacity_mb() -> f64 {
1000.0
300.0
}
fn default_conduit_cache_capacity_modifier() -> f64 {
@ -255,6 +259,10 @@ fn default_max_fetch_prev_events() -> u16 {
100_u16
}
fn default_trusted_servers() -> Vec<OwnedServerName> {
vec![OwnedServerName::try_from("matrix.org").unwrap()]
}
fn default_log() -> String {
"warn,state_res=warn,_=off,sled=off".to_owned()
}

View File

@ -38,6 +38,7 @@ pub trait KeyValueDatabaseEngine: Send + Sync {
fn memory_usage(&self) -> Result<String> {
Ok("Current database engine does not support memory usage reporting.".to_owned())
}
fn clear_caches(&self) {}
}
pub trait KvTree: Send + Sync {

View File

@ -45,6 +45,17 @@ fn db_options(max_open_files: i32, rocksdb_cache: &rocksdb::Cache) -> rocksdb::O
db_opts.set_compaction_style(rocksdb::DBCompactionStyle::Level);
db_opts.optimize_level_style_compaction(10 * 1024 * 1024);
// https://github.com/facebook/rocksdb/wiki/Setup-Options-and-Basic-Tuning
db_opts.set_max_background_jobs(6);
db_opts.set_bytes_per_sync(1048576);
// https://github.com/facebook/rocksdb/wiki/WAL-Recovery-Modes#ktoleratecorruptedtailrecords
//
// Unclean shutdowns of a Matrix homeserver are likely to be fine when
// recovered in this manner as it's likely any lost information will be
// restored via federation.
db_opts.set_wal_recovery_mode(rocksdb::DBRecoveryMode::TolerateCorruptedTailRecords);
let prefix_extractor = rocksdb::SliceTransform::create_fixed_prefix(1);
db_opts.set_prefix_extractor(prefix_extractor);
@ -54,7 +65,7 @@ fn db_options(max_open_files: i32, rocksdb_cache: &rocksdb::Cache) -> rocksdb::O
impl KeyValueDatabaseEngine for Arc<Engine> {
fn open(config: &Config) -> Result<Self> {
let cache_capacity_bytes = (config.db_cache_capacity_mb * 1024.0 * 1024.0) as usize;
let rocksdb_cache = rocksdb::Cache::new_lru_cache(cache_capacity_bytes).unwrap();
let rocksdb_cache = rocksdb::Cache::new_lru_cache(cache_capacity_bytes);
let db_opts = db_options(config.rocksdb_max_open_files, &rocksdb_cache);
@ -121,6 +132,8 @@ impl KeyValueDatabaseEngine for Arc<Engine> {
self.cache.get_pinned_usage() as f64 / 1024.0 / 1024.0,
))
}
fn clear_caches(&self) {}
}
impl RocksDbEngineTree<'_> {
@ -161,7 +174,7 @@ impl KvTree for RocksDbEngineTree<'_> {
self.db
.rocks
.iterator_cf(&self.cf(), rocksdb::IteratorMode::Start)
//.map(|r| r.unwrap())
.map(|r| r.unwrap())
.map(|(k, v)| (Vec::from(k), Vec::from(v))),
)
}
@ -185,7 +198,7 @@ impl KvTree for RocksDbEngineTree<'_> {
},
),
)
//.map(|r| r.unwrap())
.map(|r| r.unwrap())
.map(|(k, v)| (Vec::from(k), Vec::from(v))),
)
}
@ -226,7 +239,7 @@ impl KvTree for RocksDbEngineTree<'_> {
&self.cf(),
rocksdb::IteratorMode::From(&prefix, rocksdb::Direction::Forward),
)
//.map(|r| r.unwrap())
.map(|r| r.unwrap())
.map(|(k, v)| (Vec::from(k), Vec::from(v)))
.take_while(move |(k, _)| k.starts_with(&prefix)),
)

View File

@ -1,7 +1,8 @@
use std::collections::BTreeMap;
use std::collections::{BTreeMap, HashMap};
use async_trait::async_trait;
use futures_util::{stream::FuturesUnordered, StreamExt};
use lru_cache::LruCache;
use ruma::{
api::federation::discovery::{ServerSigningKeys, VerifyKey},
signatures::Ed25519KeyPair,
@ -11,6 +12,7 @@ use ruma::{
use crate::{database::KeyValueDatabase, service, services, utils, Error, Result};
pub const COUNTER: &[u8] = b"c";
pub const LAST_CHECK_FOR_UPDATES_COUNT: &[u8] = b"u";
#[async_trait]
impl service::globals::Data for KeyValueDatabase {
@ -26,6 +28,23 @@ impl service::globals::Data for KeyValueDatabase {
})
}
fn last_check_for_updates_id(&self) -> Result<u64> {
self.global
.get(LAST_CHECK_FOR_UPDATES_COUNT)?
.map_or(Ok(0_u64), |bytes| {
utils::u64_from_bytes(&bytes).map_err(|_| {
Error::bad_database("last check for updates count has invalid bytes.")
})
})
}
fn update_check_for_updates_id(&self, id: u64) -> Result<()> {
self.global
.insert(LAST_CHECK_FOR_UPDATES_COUNT, &id.to_be_bytes())?;
Ok(())
}
async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> {
let userid_bytes = user_id.as_bytes().to_vec();
let mut userid_prefix = userid_bytes.clone();
@ -118,8 +137,67 @@ impl service::globals::Data for KeyValueDatabase {
self._db.cleanup()
}
fn memory_usage(&self) -> Result<String> {
self._db.memory_usage()
fn memory_usage(&self) -> String {
let pdu_cache = self.pdu_cache.lock().unwrap().len();
let shorteventid_cache = self.shorteventid_cache.lock().unwrap().len();
let auth_chain_cache = self.auth_chain_cache.lock().unwrap().len();
let eventidshort_cache = self.eventidshort_cache.lock().unwrap().len();
let statekeyshort_cache = self.statekeyshort_cache.lock().unwrap().len();
let our_real_users_cache = self.our_real_users_cache.read().unwrap().len();
let appservice_in_room_cache = self.appservice_in_room_cache.read().unwrap().len();
let lasttimelinecount_cache = self.lasttimelinecount_cache.lock().unwrap().len();
let mut response = format!(
"\
pdu_cache: {pdu_cache}
shorteventid_cache: {shorteventid_cache}
auth_chain_cache: {auth_chain_cache}
eventidshort_cache: {eventidshort_cache}
statekeyshort_cache: {statekeyshort_cache}
our_real_users_cache: {our_real_users_cache}
appservice_in_room_cache: {appservice_in_room_cache}
lasttimelinecount_cache: {lasttimelinecount_cache}\n"
);
if let Ok(db_stats) = self._db.memory_usage() {
response += &db_stats;
}
response
}
fn clear_caches(&self, amount: u32) {
if amount > 0 {
let c = &mut *self.pdu_cache.lock().unwrap();
*c = LruCache::new(c.capacity());
}
if amount > 1 {
let c = &mut *self.shorteventid_cache.lock().unwrap();
*c = LruCache::new(c.capacity());
}
if amount > 2 {
let c = &mut *self.auth_chain_cache.lock().unwrap();
*c = LruCache::new(c.capacity());
}
if amount > 3 {
let c = &mut *self.eventidshort_cache.lock().unwrap();
*c = LruCache::new(c.capacity());
}
if amount > 4 {
let c = &mut *self.statekeyshort_cache.lock().unwrap();
*c = LruCache::new(c.capacity());
}
if amount > 5 {
let c = &mut *self.our_real_users_cache.write().unwrap();
*c = HashMap::new();
}
if amount > 6 {
let c = &mut *self.appservice_in_room_cache.write().unwrap();
*c = HashMap::new();
}
if amount > 7 {
let c = &mut *self.lasttimelinecount_cache.lock().unwrap();
*c = HashMap::new();
}
}
fn load_keypair(&self) -> Result<Ed25519KeyPair> {

View File

@ -12,6 +12,7 @@ mod state;
mod state_accessor;
mod state_cache;
mod state_compressor;
mod threads;
mod timeline;
mod user;

View File

@ -1,10 +1,64 @@
use std::sync::Arc;
use std::{mem, sync::Arc};
use ruma::{EventId, RoomId};
use ruma::{EventId, RoomId, UserId};
use crate::{database::KeyValueDatabase, service, Result};
use crate::{
database::KeyValueDatabase,
service::{self, rooms::timeline::PduCount},
services, utils, Error, PduEvent, Result,
};
impl service::rooms::pdu_metadata::Data for KeyValueDatabase {
fn add_relation(&self, from: u64, to: u64) -> Result<()> {
let mut key = to.to_be_bytes().to_vec();
key.extend_from_slice(&from.to_be_bytes());
self.tofrom_relation.insert(&key, &[])?;
Ok(())
}
fn relations_until<'a>(
&'a self,
user_id: &'a UserId,
shortroomid: u64,
target: u64,
until: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>> {
let prefix = target.to_be_bytes().to_vec();
let mut current = prefix.clone();
let count_raw = match until {
PduCount::Normal(x) => x - 1,
PduCount::Backfilled(x) => {
current.extend_from_slice(&0_u64.to_be_bytes());
u64::MAX - x - 1
}
};
current.extend_from_slice(&count_raw.to_be_bytes());
Ok(Box::new(
self.tofrom_relation
.iter_from(&current, true)
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(move |(tofrom, _data)| {
let from = utils::u64_from_bytes(&tofrom[(mem::size_of::<u64>())..])
.map_err(|_| Error::bad_database("Invalid count in tofrom_relation."))?;
let mut pduid = shortroomid.to_be_bytes().to_vec();
pduid.extend_from_slice(&from.to_be_bytes());
let mut pdu = services()
.rooms
.timeline
.get_pdu_from_id(&pduid)?
.ok_or_else(|| Error::bad_database("Pdu in tofrom_relation is invalid."))?;
if pdu.sender != user_id {
pdu.remove_transaction_id()?;
}
Ok((PduCount::Normal(from), pdu))
}),
))
}
fn mark_as_referenced(&self, room_id: &RoomId, event_ids: &[Arc<EventId>]) -> Result<()> {
for prev in event_ids {
let mut key = room_id.as_bytes().to_vec();

View File

@ -1,5 +1,3 @@
use std::mem::size_of;
use ruma::RoomId;
use crate::{database::KeyValueDatabase, service, services, utils, Result};
@ -15,7 +13,7 @@ impl service::rooms::search::Data for KeyValueDatabase {
let mut key = shortroomid.to_be_bytes().to_vec();
key.extend_from_slice(word.as_bytes());
key.push(0xff);
key.extend_from_slice(pdu_id);
key.extend_from_slice(pdu_id); // TODO: currently we save the room id a second time here
(key, Vec::new())
});
@ -34,7 +32,6 @@ impl service::rooms::search::Data for KeyValueDatabase {
.expect("room exists")
.to_be_bytes()
.to_vec();
let prefix_clone = prefix.clone();
let words: Vec<_> = search_string
.split_terminator(|c: char| !c.is_alphanumeric())
@ -46,6 +43,7 @@ impl service::rooms::search::Data for KeyValueDatabase {
let mut prefix2 = prefix.clone();
prefix2.extend_from_slice(word.as_bytes());
prefix2.push(0xff);
let prefix3 = prefix2.clone();
let mut last_possible_id = prefix2.clone();
last_possible_id.extend_from_slice(&u64::MAX.to_be_bytes());
@ -53,7 +51,7 @@ impl service::rooms::search::Data for KeyValueDatabase {
self.tokenids
.iter_from(&last_possible_id, true) // Newest pdus first
.take_while(move |(k, _)| k.starts_with(&prefix2))
.map(|(key, _)| key[key.len() - size_of::<u64>()..].to_vec())
.map(move |(key, _)| key[prefix3.len()..].to_vec())
});
let common_elements = match utils::common_elements(iterators, |a, b| {
@ -64,12 +62,6 @@ impl service::rooms::search::Data for KeyValueDatabase {
None => return Ok(None),
};
let mapped = common_elements.map(move |id| {
let mut pduid = prefix_clone.clone();
pduid.extend_from_slice(&id);
pduid
});
Ok(Some((Box::new(mapped), words)))
Ok(Some((Box::new(common_elements), words)))
}
}

View File

@ -16,7 +16,7 @@ impl service::rooms::state_accessor::Data for KeyValueDatabase {
.1;
let mut result = HashMap::new();
let mut i = 0;
for compressed in full_state.into_iter() {
for compressed in full_state.iter() {
let parsed = services()
.rooms
.state_compressor
@ -45,7 +45,7 @@ impl service::rooms::state_accessor::Data for KeyValueDatabase {
let mut result = HashMap::new();
let mut i = 0;
for compressed in full_state {
for compressed in full_state.iter() {
let (_, eventid) = services()
.rooms
.state_compressor
@ -95,7 +95,7 @@ impl service::rooms::state_accessor::Data for KeyValueDatabase {
.expect("there is always one layer")
.1;
Ok(full_state
.into_iter()
.iter()
.find(|bytes| bytes.starts_with(&shortstatekey.to_be_bytes()))
.and_then(|compressed| {
services()

View File

@ -1,4 +1,4 @@
use std::{collections::HashSet, mem::size_of};
use std::{collections::HashSet, mem::size_of, sync::Arc};
use crate::{
database::KeyValueDatabase,
@ -37,20 +37,20 @@ impl service::rooms::state_compressor::Data for KeyValueDatabase {
Ok(StateDiff {
parent,
added,
removed,
added: Arc::new(added),
removed: Arc::new(removed),
})
}
fn save_statediff(&self, shortstatehash: u64, diff: StateDiff) -> Result<()> {
let mut value = diff.parent.unwrap_or(0).to_be_bytes().to_vec();
for new in &diff.added {
for new in diff.added.iter() {
value.extend_from_slice(&new[..]);
}
if !diff.removed.is_empty() {
value.extend_from_slice(&0_u64.to_be_bytes());
for removed in &diff.removed {
for removed in diff.removed.iter() {
value.extend_from_slice(&removed[..]);
}
}

View File

@ -0,0 +1,78 @@
use std::mem;
use ruma::{api::client::threads::get_threads::v1::IncludeThreads, OwnedUserId, RoomId, UserId};
use crate::{database::KeyValueDatabase, service, services, utils, Error, PduEvent, Result};
impl service::rooms::threads::Data for KeyValueDatabase {
fn threads_until<'a>(
&'a self,
user_id: &'a UserId,
room_id: &'a RoomId,
until: u64,
include: &'a IncludeThreads,
) -> Result<Box<dyn Iterator<Item = Result<(u64, PduEvent)>> + 'a>> {
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
let mut current = prefix.clone();
current.extend_from_slice(&(until - 1).to_be_bytes());
Ok(Box::new(
self.threadid_userids
.iter_from(&current, true)
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(move |(pduid, users)| {
let count = utils::u64_from_bytes(&pduid[(mem::size_of::<u64>())..])
.map_err(|_| Error::bad_database("Invalid pduid in threadid_userids."))?;
let mut pdu = services()
.rooms
.timeline
.get_pdu_from_id(&pduid)?
.ok_or_else(|| {
Error::bad_database("Invalid pduid reference in threadid_userids")
})?;
if pdu.sender != user_id {
pdu.remove_transaction_id()?;
}
Ok((count, pdu))
}),
))
}
fn update_participants(&self, root_id: &[u8], participants: &[OwnedUserId]) -> Result<()> {
let users = participants
.iter()
.map(|user| user.as_bytes())
.collect::<Vec<_>>()
.join(&[0xff][..]);
self.threadid_userids.insert(&root_id, &users)?;
Ok(())
}
fn get_participants(&self, root_id: &[u8]) -> Result<Option<Vec<OwnedUserId>>> {
if let Some(users) = self.threadid_userids.get(&root_id)? {
Ok(Some(
users
.split(|b| *b == 0xff)
.map(|bytes| {
UserId::parse(utils::string_from_bytes(bytes).map_err(|_| {
Error::bad_database("Invalid UserId bytes in threadid_userids.")
})?)
.map_err(|_| Error::bad_database("Invalid UserId in threadid_userids."))
})
.filter_map(|r| r.ok())
.collect(),
))
} else {
Ok(None)
}
}
}

View File

@ -7,30 +7,10 @@ use tracing::error;
use crate::{database::KeyValueDatabase, service, services, utils, Error, PduEvent, Result};
use service::rooms::timeline::PduCount;
impl service::rooms::timeline::Data for KeyValueDatabase {
fn first_pdu_in_room(&self, room_id: &RoomId) -> Result<Option<Arc<PduEvent>>> {
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
// Look for PDUs in that room.
self.pduid_pdu
.iter_from(&prefix, false)
.filter(|(k, _)| k.starts_with(&prefix))
.map(|(_, pdu)| {
serde_json::from_slice(&pdu)
.map_err(|_| Error::bad_database("Invalid first PDU in db."))
.map(Arc::new)
})
.next()
.transpose()
}
fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<u64> {
fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<PduCount> {
match self
.lasttimelinecount_cache
.lock()
@ -39,20 +19,18 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
{
hash_map::Entry::Vacant(v) => {
if let Some(last_count) = self
.pdus_until(sender_user, room_id, u64::MAX)?
.filter_map(|r| {
.pdus_until(sender_user, room_id, PduCount::max())?
.find_map(|r| {
// Filter out buggy events
if r.is_err() {
error!("Bad pdu in pdus_since: {:?}", r);
}
r.ok()
})
.map(|(pduid, _)| self.pdu_count(&pduid))
.next()
{
Ok(*v.insert(last_count?))
Ok(*v.insert(last_count.0))
} else {
Ok(0)
Ok(PduCount::Normal(0))
}
}
hash_map::Entry::Occupied(o) => Ok(*o.get()),
@ -60,29 +38,28 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
}
/// Returns the `count` of this pdu's id.
fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<u64>> {
self.eventid_pduid
fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<PduCount>> {
Ok(self
.eventid_pduid
.get(event_id.as_bytes())?
.map(|pdu_id| self.pdu_count(&pdu_id))
.transpose()
.map(|pdu_id| pdu_count(&pdu_id))
.transpose()?)
}
/// Returns the json of a pdu.
fn get_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>> {
self.eventid_pduid
.get(event_id.as_bytes())?
.map_or_else(
|| self.eventid_outlierpdu.get(event_id.as_bytes()),
|pduid| {
Ok(Some(self.pduid_pdu.get(&pduid)?.ok_or_else(|| {
Error::bad_database("Invalid pduid in eventid_pduid.")
})?))
},
)?
.map(|pdu| {
serde_json::from_slice(&pdu).map_err(|_| Error::bad_database("Invalid PDU in db."))
})
.transpose()
self.get_non_outlier_pdu_json(event_id)?.map_or_else(
|| {
self.eventid_outlierpdu
.get(event_id.as_bytes())?
.map(|pdu| {
serde_json::from_slice(&pdu)
.map_err(|_| Error::bad_database("Invalid PDU in db."))
})
.transpose()
},
|x| Ok(Some(x)),
)
}
/// Returns the json of a pdu.
@ -103,7 +80,7 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
/// Returns the pdu's id.
fn get_pdu_id(&self, event_id: &EventId) -> Result<Option<Vec<u8>>> {
self.eventid_pduid.get(event_id.as_bytes())
Ok(self.eventid_pduid.get(event_id.as_bytes())?)
}
/// Returns the pdu.
@ -133,22 +110,20 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
}
if let Some(pdu) = self
.eventid_pduid
.get(event_id.as_bytes())?
.get_non_outlier_pdu(event_id)?
.map_or_else(
|| self.eventid_outlierpdu.get(event_id.as_bytes()),
|pduid| {
Ok(Some(self.pduid_pdu.get(&pduid)?.ok_or_else(|| {
Error::bad_database("Invalid pduid in eventid_pduid.")
})?))
|| {
self.eventid_outlierpdu
.get(event_id.as_bytes())?
.map(|pdu| {
serde_json::from_slice(&pdu)
.map_err(|_| Error::bad_database("Invalid PDU in db."))
})
.transpose()
},
|x| Ok(Some(x)),
)?
.map(|pdu| {
serde_json::from_slice(&pdu)
.map_err(|_| Error::bad_database("Invalid PDU in db."))
.map(Arc::new)
})
.transpose()?
.map(Arc::new)
{
self.pdu_cache
.lock()
@ -182,12 +157,6 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
})
}
/// Returns the `count` of this pdu's id.
fn pdu_count(&self, pdu_id: &[u8]) -> Result<u64> {
utils::u64_from_bytes(&pdu_id[pdu_id.len() - size_of::<u64>()..])
.map_err(|_| Error::bad_database("PDU has invalid count bytes."))
}
fn append_pdu(
&self,
pdu_id: &[u8],
@ -203,7 +172,7 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
self.lasttimelinecount_cache
.lock()
.unwrap()
.insert(pdu.room_id.clone(), count);
.insert(pdu.room_id.clone(), PduCount::Normal(count));
self.eventid_pduid.insert(pdu.event_id.as_bytes(), pdu_id)?;
self.eventid_outlierpdu.remove(pdu.event_id.as_bytes())?;
@ -211,57 +180,48 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
Ok(())
}
fn prepend_backfill_pdu(
&self,
pdu_id: &[u8],
event_id: &EventId,
json: &CanonicalJsonObject,
) -> Result<()> {
self.pduid_pdu.insert(
pdu_id,
&serde_json::to_vec(json).expect("CanonicalJsonObject is always a valid"),
)?;
self.eventid_pduid.insert(event_id.as_bytes(), pdu_id)?;
self.eventid_outlierpdu.remove(event_id.as_bytes())?;
Ok(())
}
/// Removes a pdu and creates a new one with the same id.
fn replace_pdu(&self, pdu_id: &[u8], pdu: &PduEvent) -> Result<()> {
fn replace_pdu(
&self,
pdu_id: &[u8],
pdu_json: &CanonicalJsonObject,
pdu: &PduEvent,
) -> Result<()> {
if self.pduid_pdu.get(pdu_id)?.is_some() {
self.pduid_pdu.insert(
pdu_id,
&serde_json::to_vec(pdu).expect("CanonicalJsonObject is always a valid"),
&serde_json::to_vec(pdu_json).expect("CanonicalJsonObject is always a valid"),
)?;
Ok(())
} else {
Err(Error::BadRequest(
return Err(Error::BadRequest(
ErrorKind::NotFound,
"PDU does not exist.",
))
));
}
}
/// Returns an iterator over all events in a room that happened after the event with id `since`
/// in chronological order.
fn pdus_since<'a>(
&'a self,
user_id: &UserId,
room_id: &RoomId,
since: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>> {
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
self.pdu_cache
.lock()
.unwrap()
.remove(&(*pdu.event_id).to_owned());
// Skip the first pdu if it's exactly at since, because we sent that last time
let mut first_pdu_id = prefix.clone();
first_pdu_id.extend_from_slice(&(since + 1).to_be_bytes());
let user_id = user_id.to_owned();
Ok(Box::new(
self.pduid_pdu
.iter_from(&first_pdu_id, false)
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(move |(pdu_id, v)| {
let mut pdu = serde_json::from_slice::<PduEvent>(&v)
.map_err(|_| Error::bad_database("PDU in db is invalid."))?;
if pdu.sender != user_id {
pdu.remove_transaction_id()?;
}
Ok((pdu_id, pdu))
}),
))
Ok(())
}
/// Returns an iterator over all events and their tokens in a room that happened before the
@ -270,27 +230,15 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
&'a self,
user_id: &UserId,
room_id: &RoomId,
until: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>> {
// Create the first part of the full pdu id
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
let mut current = prefix.clone();
current.extend_from_slice(&(until.saturating_sub(1)).to_be_bytes()); // -1 because we don't want event at `until`
let current: &[u8] = &current;
until: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>> {
let (prefix, current) = count_to_id(&room_id, until, 1, true)?;
let user_id = user_id.to_owned();
Ok(Box::new(
self.pduid_pdu
.iter_from(current, true)
.iter_from(&current, true)
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(move |(pdu_id, v)| {
let mut pdu = serde_json::from_slice::<PduEvent>(&v)
@ -298,7 +246,9 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
if pdu.sender != user_id {
pdu.remove_transaction_id()?;
}
Ok((pdu_id, pdu))
pdu.add_age()?;
let count = pdu_count(&pdu_id)?;
Ok((count, pdu))
}),
))
}
@ -307,27 +257,15 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
&'a self,
user_id: &UserId,
room_id: &RoomId,
from: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>> {
// Create the first part of the full pdu id
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
let mut current = prefix.clone();
current.extend_from_slice(&(from + 1).to_be_bytes()); // +1 so we don't send the base event
let current: &[u8] = &current;
from: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>> {
let (prefix, current) = count_to_id(&room_id, from, 1, false)?;
let user_id = user_id.to_owned();
Ok(Box::new(
self.pduid_pdu
.iter_from(current, false)
.iter_from(&current, false)
.take_while(move |(k, _)| k.starts_with(&prefix))
.map(move |(pdu_id, v)| {
let mut pdu = serde_json::from_slice::<PduEvent>(&v)
@ -335,7 +273,9 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
if pdu.sender != user_id {
pdu.remove_transaction_id()?;
}
Ok((pdu_id, pdu))
pdu.add_age()?;
let count = pdu_count(&pdu_id)?;
Ok((count, pdu))
}),
))
}
@ -368,3 +308,60 @@ impl service::rooms::timeline::Data for KeyValueDatabase {
Ok(())
}
}
/// Returns the `count` of this pdu's id.
fn pdu_count(pdu_id: &[u8]) -> Result<PduCount> {
let last_u64 = utils::u64_from_bytes(&pdu_id[pdu_id.len() - size_of::<u64>()..])
.map_err(|_| Error::bad_database("PDU has invalid count bytes."))?;
let second_last_u64 = utils::u64_from_bytes(
&pdu_id[pdu_id.len() - 2 * size_of::<u64>()..pdu_id.len() - size_of::<u64>()],
);
if matches!(second_last_u64, Ok(0)) {
Ok(PduCount::Backfilled(u64::MAX - last_u64))
} else {
Ok(PduCount::Normal(last_u64))
}
}
fn count_to_id(
room_id: &RoomId,
count: PduCount,
offset: u64,
subtract: bool,
) -> Result<(Vec<u8>, Vec<u8>)> {
let prefix = services()
.rooms
.short
.get_shortroomid(room_id)?
.expect("room exists")
.to_be_bytes()
.to_vec();
let mut pdu_id = prefix.clone();
// +1 so we don't send the base event
let count_raw = match count {
PduCount::Normal(x) => {
if subtract {
x - offset
} else {
x + offset
}
}
PduCount::Backfilled(x) => {
pdu_id.extend_from_slice(&0_u64.to_be_bytes());
let num = u64::MAX - x;
if subtract {
if num > 0 {
num - offset
} else {
num
}
} else {
num + offset
}
}
};
pdu_id.extend_from_slice(&count_raw.to_be_bytes());
Ok((prefix, pdu_id))
}

View File

@ -449,33 +449,13 @@ impl service::users::Data for KeyValueDatabase {
master_key: &Raw<CrossSigningKey>,
self_signing_key: &Option<Raw<CrossSigningKey>>,
user_signing_key: &Option<Raw<CrossSigningKey>>,
notify: bool,
) -> Result<()> {
// TODO: Check signatures
let mut prefix = user_id.as_bytes().to_vec();
prefix.push(0xff);
// Master key
let mut master_key_ids = master_key
.deserialize()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid master key"))?
.keys
.into_values();
let master_key_id = master_key_ids.next().ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Master key contained no key.",
))?;
if master_key_ids.next().is_some() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Master key contained more than one key.",
));
}
let mut master_key_key = prefix.clone();
master_key_key.extend_from_slice(master_key_id.as_bytes());
let (master_key_key, _) = self.parse_master_key(user_id, master_key)?;
self.keyid_key
.insert(&master_key_key, master_key.json().get().as_bytes())?;
@ -551,7 +531,9 @@ impl service::users::Data for KeyValueDatabase {
.insert(user_id.as_bytes(), &user_signing_key_key)?;
}
self.mark_device_key_update(user_id)?;
if notify {
self.mark_device_key_update(user_id)?;
}
Ok(())
}
@ -592,7 +574,6 @@ impl service::users::Data for KeyValueDatabase {
&serde_json::to_vec(&cross_signing_key).expect("CrossSigningKey::to_vec always works"),
)?;
// TODO: Should we notify about this change?
self.mark_device_key_update(target_id)?;
Ok(())
@ -691,45 +672,80 @@ impl service::users::Data for KeyValueDatabase {
})
}
fn parse_master_key(
&self,
user_id: &UserId,
master_key: &Raw<CrossSigningKey>,
) -> Result<(Vec<u8>, CrossSigningKey)> {
let mut prefix = user_id.as_bytes().to_vec();
prefix.push(0xff);
let master_key = master_key
.deserialize()
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid master key"))?;
let mut master_key_ids = master_key.keys.values();
let master_key_id = master_key_ids.next().ok_or(Error::BadRequest(
ErrorKind::InvalidParam,
"Master key contained no key.",
))?;
if master_key_ids.next().is_some() {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Master key contained more than one key.",
));
}
let mut master_key_key = prefix.clone();
master_key_key.extend_from_slice(master_key_id.as_bytes());
Ok((master_key_key, master_key))
}
fn get_key(
&self,
key: &[u8],
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.keyid_key.get(key)?.map_or(Ok(None), |bytes| {
let mut cross_signing_key = serde_json::from_slice::<serde_json::Value>(&bytes)
.map_err(|_| Error::bad_database("CrossSigningKey in db is invalid."))?;
clean_signatures(
&mut cross_signing_key,
sender_user,
user_id,
allowed_signatures,
)?;
Ok(Some(Raw::from_json(
serde_json::value::to_raw_value(&cross_signing_key)
.expect("Value to RawValue serialization"),
)))
})
}
fn get_master_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.userid_masterkeyid
.get(user_id.as_bytes())?
.map_or(Ok(None), |key| {
self.keyid_key.get(&key)?.map_or(Ok(None), |bytes| {
let mut cross_signing_key = serde_json::from_slice::<serde_json::Value>(&bytes)
.map_err(|_| Error::bad_database("CrossSigningKey in db is invalid."))?;
clean_signatures(&mut cross_signing_key, user_id, allowed_signatures)?;
Ok(Some(Raw::from_json(
serde_json::value::to_raw_value(&cross_signing_key)
.expect("Value to RawValue serialization"),
)))
})
self.get_key(&key, sender_user, user_id, allowed_signatures)
})
}
fn get_self_signing_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.userid_selfsigningkeyid
.get(user_id.as_bytes())?
.map_or(Ok(None), |key| {
self.keyid_key.get(&key)?.map_or(Ok(None), |bytes| {
let mut cross_signing_key = serde_json::from_slice::<serde_json::Value>(&bytes)
.map_err(|_| Error::bad_database("CrossSigningKey in db is invalid."))?;
clean_signatures(&mut cross_signing_key, user_id, allowed_signatures)?;
Ok(Some(Raw::from_json(
serde_json::value::to_raw_value(&cross_signing_key)
.expect("Value to RawValue serialization"),
)))
})
self.get_key(&key, sender_user, user_id, allowed_signatures)
})
}
@ -930,6 +946,8 @@ impl service::users::Data for KeyValueDatabase {
}
}
impl KeyValueDatabase {}
/// Will only return with Some(username) if the password was not empty and the
/// username could be successfully parsed.
/// If utils::string_from_bytes(...) returns an error that username will be skipped

View File

@ -1,7 +1,10 @@
pub mod abstraction;
pub mod key_value;
use crate::{services, utils, Config, Error, PduEvent, Result, Services, SERVICES};
use crate::{
service::rooms::timeline::PduCount, services, utils, Config, Error, PduEvent, Result, Services,
SERVICES,
};
use abstraction::{KeyValueDatabaseEngine, KvTree};
use directories::ProjectDirs;
use lru_cache::LruCache;
@ -15,6 +18,7 @@ use ruma::{
CanonicalJsonValue, EventId, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId,
UserId,
};
use serde::Deserialize;
use std::{
collections::{BTreeMap, HashMap, HashSet},
fs::{self, remove_dir_all},
@ -22,7 +26,9 @@ use std::{
mem::size_of,
path::Path,
sync::{Arc, Mutex, RwLock},
time::Duration,
};
use tokio::time::interval;
use tracing::{debug, error, info, warn};
@ -77,6 +83,8 @@ pub struct KeyValueDatabase {
pub(super) aliasid_alias: Arc<dyn KvTree>, // AliasId = RoomId + Count
pub(super) publicroomids: Arc<dyn KvTree>,
pub(super) threadid_userids: Arc<dyn KvTree>, // ThreadId = RoomId + Count
pub(super) tokenids: Arc<dyn KvTree>, // TokenId = ShortRoomId + Token + PduIdCount
/// Participating servers in a room.
@ -125,6 +133,8 @@ pub struct KeyValueDatabase {
pub(super) eventid_outlierpdu: Arc<dyn KvTree>,
pub(super) softfailedeventids: Arc<dyn KvTree>,
/// ShortEventId + ShortEventId -> ().
pub(super) tofrom_relation: Arc<dyn KvTree>,
/// RoomId + EventId -> Parent PDU EventId.
pub(super) referencedevents: Arc<dyn KvTree>,
@ -161,7 +171,7 @@ pub struct KeyValueDatabase {
pub(super) shortstatekey_cache: Mutex<LruCache<u64, (StateEventType, String)>>,
pub(super) our_real_users_cache: RwLock<HashMap<OwnedRoomId, Arc<HashSet<OwnedUserId>>>>,
pub(super) appservice_in_room_cache: RwLock<HashMap<OwnedRoomId, HashMap<String, bool>>>,
pub(super) lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, u64>>,
pub(super) lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, PduCount>>,
}
impl KeyValueDatabase {
@ -257,6 +267,10 @@ impl KeyValueDatabase {
}
};
if config.registration_token == Some(String::new()) {
return Err(Error::bad_config("Registration token is empty"));
}
if config.max_request_size < 1024 {
error!(?config.max_request_size, "Max request size is less than 1KB. Please increase it.");
}
@ -299,6 +313,8 @@ impl KeyValueDatabase {
aliasid_alias: builder.open_tree("aliasid_alias")?,
publicroomids: builder.open_tree("publicroomids")?,
threadid_userids: builder.open_tree("threadid_userids")?,
tokenids: builder.open_tree("tokenids")?,
roomserverids: builder.open_tree("roomserverids")?,
@ -339,6 +355,7 @@ impl KeyValueDatabase {
eventid_outlierpdu: builder.open_tree("eventid_outlierpdu")?,
softfailedeventids: builder.open_tree("softfailedeventids")?,
tofrom_relation: builder.open_tree("tofrom_relation")?,
referencedevents: builder.open_tree("referencedevents")?,
roomuserdataid_accountdata: builder.open_tree("roomuserdataid_accountdata")?,
roomusertype_roomuserdataid: builder.open_tree("roomusertype_roomuserdataid")?,
@ -408,7 +425,7 @@ impl KeyValueDatabase {
}
// If the database has any data, perform data migrations before starting
let latest_database_version = 12;
let latest_database_version = 13;
if services().users.count()? > 0 {
// MIGRATIONS
@ -577,8 +594,8 @@ impl KeyValueDatabase {
services().rooms.state_compressor.save_state_from_diff(
current_sstatehash,
statediffnew,
statediffremoved,
Arc::new(statediffnew),
Arc::new(statediffremoved),
2, // every state change is 2 event changes on average
states_parents,
)?;
@ -800,10 +817,17 @@ impl KeyValueDatabase {
}
if services().globals.database_version()? < 12 {
for username in services().users.list_local_users().unwrap() {
let user =
UserId::parse_with_server_name(username, services().globals.server_name())
.unwrap();
for username in services().users.list_local_users()? {
let user = match UserId::parse_with_server_name(
username.clone(),
services().globals.server_name(),
) {
Ok(u) => u,
Err(e) => {
warn!("Invalid username {username}: {e}");
continue;
}
};
let raw_rules_list = services()
.account_data
@ -870,6 +894,52 @@ impl KeyValueDatabase {
warn!("Migration: 11 -> 12 finished");
}
// This migration can be reused as-is anytime the server-default rules are updated.
if services().globals.database_version()? < 13 {
for username in services().users.list_local_users()? {
let user = match UserId::parse_with_server_name(
username.clone(),
services().globals.server_name(),
) {
Ok(u) => u,
Err(e) => {
warn!("Invalid username {username}: {e}");
continue;
}
};
let raw_rules_list = services()
.account_data
.get(
None,
&user,
GlobalAccountDataEventType::PushRules.to_string().into(),
)
.unwrap()
.expect("Username is invalid");
let mut account_data =
serde_json::from_str::<PushRulesEvent>(raw_rules_list.get()).unwrap();
let user_default_rules = ruma::push::Ruleset::server_default(&user);
account_data
.content
.global
.update_with_server_default(user_default_rules);
services().account_data.update(
None,
&user,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(account_data).expect("to json value always works"),
)?;
}
services().globals.bump_database_version(13)?;
warn!("Migration: 12 -> 13 finished");
}
assert_eq!(
services().globals.database_version().unwrap(),
latest_database_version
@ -919,6 +989,9 @@ impl KeyValueDatabase {
services().sending.start_handler();
Self::start_cleanup_task().await;
if services().globals.allow_check_for_updates() {
Self::start_check_for_updates_task();
}
Ok(())
}
@ -935,9 +1008,61 @@ impl KeyValueDatabase {
}
#[tracing::instrument]
pub async fn start_cleanup_task() {
use tokio::time::interval;
pub fn start_check_for_updates_task() {
tokio::spawn(async move {
let timer_interval = Duration::from_secs(60 * 60);
let mut i = interval(timer_interval);
loop {
i.tick().await;
let _ = Self::try_handle_updates().await;
}
});
}
async fn try_handle_updates() -> Result<()> {
let response = services()
.globals
.default_client()
.get("https://conduit.rs/check-for-updates/stable")
.send()
.await?;
#[derive(Deserialize)]
struct CheckForUpdatesResponseEntry {
id: u64,
date: String,
message: String,
}
#[derive(Deserialize)]
struct CheckForUpdatesResponse {
updates: Vec<CheckForUpdatesResponseEntry>,
}
let response = serde_json::from_str::<CheckForUpdatesResponse>(&response.text().await?)
.map_err(|_| Error::BadServerResponse("Bad version check response"))?;
let mut last_update_id = services().globals.last_check_for_updates_id()?;
for update in response.updates {
last_update_id = last_update_id.max(update.id);
if update.id > services().globals.last_check_for_updates_id()? {
println!("{}", update.message);
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"@room: The following is a message from the Conduit developers. It was sent on '{}':\n\n{}",
update.date, update.message
)))
}
}
services()
.globals
.update_check_for_updates_id(last_update_id)?;
Ok(())
}
#[tracing::instrument]
pub async fn start_cleanup_task() {
#[cfg(unix)]
use tokio::signal::unix::{signal, SignalKind};

View File

@ -2,16 +2,16 @@
rust_2018_idioms,
unused_qualifications,
clippy::cloned_instead_of_copied,
clippy::str_to_string
clippy::str_to_string,
clippy::future_not_send
)]
#![allow(clippy::suspicious_else_formatting)]
#![deny(clippy::dbg_macro)]
use std::{future::Future, io, net::SocketAddr, time::Duration};
use std::{future::Future, io, net::SocketAddr, sync::atomic, time::Duration};
use axum::{
extract::{DefaultBodyLimit, FromRequest, MatchedPath},
handler::Handler,
extract::{DefaultBodyLimit, FromRequestParts, MatchedPath},
response::IntoResponse,
routing::{get, on, MethodFilter},
Router,
@ -40,7 +40,7 @@ use tower_http::{
trace::TraceLayer,
ServiceBuilderExt as _,
};
use tracing::{error, info, warn};
use tracing::{debug, error, info, warn};
use tracing_subscriber::{prelude::*, EnvFilter};
pub use conduit::*; // Re-export everything from the library crate
@ -54,7 +54,7 @@ static GLOBAL: Jemalloc = Jemalloc;
#[tokio::main]
async fn main() {
// Initialize DB
// Initialize config
let raw_config =
Figment::new()
.merge(
@ -75,6 +75,8 @@ async fn main() {
config.warn_deprecated();
let log = format!("{},ruma_state_res=error,_=off,sled=off", config.log);
if config.allow_jaeger {
opentelemetry::global::set_text_map_propagator(opentelemetry_jaeger::Propagator::new());
let tracer = opentelemetry_jaeger::new_agent_pipeline()
@ -84,7 +86,7 @@ async fn main() {
.unwrap();
let telemetry = tracing_opentelemetry::layer().with_tracer(tracer);
let filter_layer = match EnvFilter::try_new(&config.log) {
let filter_layer = match EnvFilter::try_new(&log) {
Ok(s) => s,
Err(e) => {
eprintln!(
@ -111,7 +113,7 @@ async fn main() {
} else {
let registry = tracing_subscriber::Registry::default();
let fmt_layer = tracing_subscriber::fmt::Layer::new();
let filter_layer = match EnvFilter::try_new(&config.log) {
let filter_layer = match EnvFilter::try_new(&log) {
Ok(s) => s,
Err(e) => {
eprintln!("It looks like your config is invalid. The following error occured while parsing it: {e}");
@ -123,6 +125,16 @@ async fn main() {
tracing::subscriber::set_global_default(subscriber).unwrap();
}
// This is needed for opening lots of file descriptors, which tends to
// happen more often when using RocksDB and making lots of federation
// connections at startup. The soft limit is usually 1024, and the hard
// limit is usually 512000; I've personally seen it hit >2000.
//
// * https://www.freedesktop.org/software/systemd/man/systemd.exec.html#id-1.12.2.1.17.6
// * https://github.com/systemd/systemd/commit/0abf94923b4a95a7d89bc526efc84e7ca2b71741
#[cfg(unix)]
maximize_fd_limit().expect("should be able to increase the soft limit to the hard limit");
info!("Loading database");
if let Err(error) = KeyValueDatabase::load_or_create(config).await {
error!(?error, "The database couldn't be loaded or created");
@ -147,6 +159,7 @@ async fn run_server() -> io::Result<()> {
let middlewares = ServiceBuilder::new()
.sensitive_headers([header::AUTHORIZATION])
.layer(axum::middleware::from_fn(spawn_task))
.layer(
TraceLayer::new_for_http().make_span_with(|request: &http::Request<_>| {
let path = if let Some(path) = request.extensions().get::<MatchedPath>() {
@ -158,7 +171,6 @@ async fn run_server() -> io::Result<()> {
tracing::info_span!("http_request", %path)
}),
)
.compression()
.layer(axum::middleware::from_fn(unrecognized_method))
.layer(
CorsLayer::new()
@ -211,16 +223,21 @@ async fn run_server() -> io::Result<()> {
}
}
// On shutdown
info!(target: "shutdown-sync", "Received shutdown notification, notifying sync helpers...");
services().globals.rotate.fire();
#[cfg(feature = "systemd")]
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]);
Ok(())
}
async fn spawn_task<B: Send + 'static>(
req: axum::http::Request<B>,
next: axum::middleware::Next<B>,
) -> std::result::Result<axum::response::Response, StatusCode> {
if services().globals.shutdown.load(atomic::Ordering::Relaxed) {
return Err(StatusCode::SERVICE_UNAVAILABLE);
}
tokio::spawn(next.run(req))
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)
}
async fn unrecognized_method<B>(
req: axum::http::Request<B>,
next: axum::middleware::Next<B>,
@ -352,6 +369,7 @@ fn routes() -> Router {
.put(client_server::send_state_event_for_empty_key_route),
)
.ruma_route(client_server::sync_events_route)
.ruma_route(client_server::sync_events_v4_route)
.ruma_route(client_server::get_context_route)
.ruma_route(client_server::get_message_events_route)
.ruma_route(client_server::search_events_route)
@ -377,6 +395,11 @@ fn routes() -> Router {
.ruma_route(client_server::set_pushers_route)
// .ruma_route(client_server::third_party_route)
.ruma_route(client_server::upgrade_room_route)
.ruma_route(client_server::get_threads_route)
.ruma_route(client_server::get_relating_events_with_rel_type_and_event_type_route)
.ruma_route(client_server::get_relating_events_with_rel_type_route)
.ruma_route(client_server::get_relating_events_route)
.ruma_route(client_server::get_hierarchy_route)
.ruma_route(server_server::get_server_version_route)
.route(
"/_matrix/key/v2/server",
@ -390,6 +413,7 @@ fn routes() -> Router {
.ruma_route(server_server::get_public_rooms_filtered_route)
.ruma_route(server_server::send_transaction_message_route)
.ruma_route(server_server::get_event_route)
.ruma_route(server_server::get_backfill_route)
.ruma_route(server_server::get_missing_events_route)
.ruma_route(server_server::get_event_authorization_route)
.ruma_route(server_server::get_room_state_route)
@ -411,7 +435,8 @@ fn routes() -> Router {
"/_matrix/client/v3/rooms/:room_id/initialSync",
get(initial_sync),
)
.fallback(not_found.into_service())
.route("/", get(it_works))
.fallback(not_found)
}
async fn shutdown_signal(handle: ServerHandle) {
@ -441,6 +466,11 @@ async fn shutdown_signal(handle: ServerHandle) {
warn!("Received {}, shutting down...", sig);
handle.graceful_shutdown(Some(Duration::from_secs(30)));
services().globals.shutdown();
#[cfg(feature = "systemd")]
let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Stopping]);
}
async fn not_found(uri: Uri) -> impl IntoResponse {
@ -455,6 +485,10 @@ async fn initial_sync(_uri: Uri) -> impl IntoResponse {
)
}
async fn it_works() -> &'static str {
"Hello from Conduit!"
}
trait RouterExt {
fn ruma_route<H, T>(self, handler: H) -> Self
where
@ -490,7 +524,7 @@ macro_rules! impl_ruma_handler {
Fut: Future<Output = Result<Req::OutgoingResponse, E>>
+ Send,
E: IntoResponse,
$( $ty: FromRequest<axum::body::Body> + Send + 'static, )*
$( $ty: FromRequestParts<()> + Send + 'static, )*
{
fn add_to_router(self, mut router: Router) -> Router {
let meta = Req::METADATA;
@ -533,3 +567,21 @@ fn method_to_filter(method: Method) -> MethodFilter {
m => panic!("Unsupported HTTP method: {m:?}"),
}
}
#[cfg(unix)]
#[tracing::instrument(err)]
fn maximize_fd_limit() -> Result<(), nix::errno::Errno> {
use nix::sys::resource::{getrlimit, setrlimit, Resource};
let res = Resource::RLIMIT_NOFILE;
let (soft_limit, hard_limit) = getrlimit(res)?;
debug!("Current nofile soft limit: {soft_limit}");
setrlimit(res, hard_limit, hard_limit)?;
debug!("Increased nofile soft limit to {hard_limit}");
Ok(())
}

View File

@ -1,7 +1,7 @@
use std::{
collections::BTreeMap,
convert::{TryFrom, TryInto},
sync::Arc,
sync::{Arc, RwLock},
time::Instant,
};
@ -21,7 +21,7 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent,
topic::RoomTopicEventContent,
},
RoomEventType,
TimelineEventType,
},
EventId, OwnedRoomAliasId, RoomAliasId, RoomId, RoomVersionId, ServerName, UserId,
};
@ -134,7 +134,13 @@ enum AdminCommand {
},
/// Print database memory usage statistics
DatabaseMemoryUsage,
MemoryUsage,
/// Clears all of Conduit's database caches with index smaller than the amount
ClearDatabaseCaches { amount: u32 },
/// Clears all of Conduit's service caches with index smaller than the amount
ClearServiceCaches { amount: u32 },
/// Show configuration values
ShowConfig,
@ -157,6 +163,20 @@ enum AdminCommand {
DisableRoom { room_id: Box<RoomId> },
/// Enables incoming federation handling for a room again.
EnableRoom { room_id: Box<RoomId> },
/// Verify json signatures
/// [commandbody]
/// # ```
/// # json here
/// # ```
SignJson,
/// Verify json signatures
/// [commandbody]
/// # ```
/// # json here
/// # ```
VerifyJson,
}
#[derive(Debug)]
@ -212,7 +232,7 @@ impl Service {
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMessage,
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&message)
.expect("event is valid, we just created it"),
unsigned: None,
@ -267,7 +287,7 @@ impl Service {
// Parse and process a message from the admin room
async fn process_admin_message(&self, room_message: String) -> RoomMessageEventContent {
let mut lines = room_message.lines();
let mut lines = room_message.lines().filter(|l| !l.trim().is_empty());
let command_line = lines.next().expect("each string has at least one line");
let body: Vec<_> = lines.collect();
@ -531,12 +551,24 @@ impl Service {
None => RoomMessageEventContent::text_plain("PDU not found."),
}
}
AdminCommand::DatabaseMemoryUsage => match services().globals.db.memory_usage() {
Ok(response) => RoomMessageEventContent::text_plain(response),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Failed to get database memory usage: {e}"
)),
},
AdminCommand::MemoryUsage => {
let response1 = services().memory_usage();
let response2 = services().globals.db.memory_usage();
RoomMessageEventContent::text_plain(format!(
"Services:\n{response1}\n\nDatabase:\n{response2}"
))
}
AdminCommand::ClearDatabaseCaches { amount } => {
services().globals.db.clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
AdminCommand::ClearServiceCaches { amount } => {
services().clear_caches(amount);
RoomMessageEventContent::text_plain("Done.")
}
AdminCommand::ShowConfig => {
// Construct and send the response
RoomMessageEventContent::text_plain(format!("{}", services().globals.config))
@ -556,7 +588,6 @@ impl Service {
// Check if the specified user is valid
if !services().users.exists(&user_id)?
|| services().users.is_deactivated(&user_id)?
|| user_id
== UserId::parse_with_server_name(
"conduit",
@ -565,7 +596,7 @@ impl Service {
.expect("conduit user exists")
{
return Ok(RoomMessageEventContent::text_plain(
"The specified user does not exist or is deactivated!",
"The specified user does not exist!",
));
}
@ -600,12 +631,12 @@ impl Service {
};
if user_id.is_historical() {
return Ok(RoomMessageEventContent::text_plain(format!(
"userid {user_id} is not allowed due to historical"
"Userid {user_id} is not allowed due to historical"
)));
}
if services().users.exists(&user_id)? {
return Ok(RoomMessageEventContent::text_plain(format!(
"userid {user_id} already exists"
"Userid {user_id} already exists"
)));
}
// Create user
@ -737,6 +768,60 @@ impl Service {
)
}
}
AdminCommand::SignJson => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(mut value) => {
ruma::signatures::sign_json(
services().globals.server_name().as_str(),
services().globals.keypair(),
&mut value,
)
.expect("our request json is what ruma expects");
let json_text = serde_json::to_string_pretty(&value)
.expect("canonical json is valid json");
RoomMessageEventContent::text_plain(json_text)
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
AdminCommand::VerifyJson => {
if body.len() > 2 && body[0].trim() == "```" && body.last().unwrap().trim() == "```"
{
let string = body[1..body.len() - 1].join("\n");
match serde_json::from_str(&string) {
Ok(value) => {
let pub_key_map = RwLock::new(BTreeMap::new());
services()
.rooms
.event_handler
.fetch_required_signing_keys(&value, &pub_key_map)
.await?;
let pub_key_map = pub_key_map.read().unwrap();
match ruma::signatures::verify_json(&pub_key_map, &value) {
Ok(_) => RoomMessageEventContent::text_plain("Signature correct"),
Err(e) => RoomMessageEventContent::text_plain(format!(
"Signature verification failed: {e}"
)),
}
}
Err(e) => RoomMessageEventContent::text_plain(format!("Invalid json: {e}")),
}
} else {
RoomMessageEventContent::text_plain(
"Expected code block in command body. Add --help for details.",
)
}
}
};
Ok(reply_message_content)
@ -855,7 +940,7 @@ impl Service {
// 1. The room create event
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomCreate,
event_type: TimelineEventType::RoomCreate,
content: to_raw_value(&content).expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
@ -869,7 +954,7 @@ impl Service {
// 2. Make conduit bot join
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: None,
@ -896,7 +981,7 @@ impl Service {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomPowerLevels,
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
@ -914,7 +999,7 @@ impl Service {
// 4.1 Join Rules
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomJoinRules,
event_type: TimelineEventType::RoomJoinRules,
content: to_raw_value(&RoomJoinRulesEventContent::new(JoinRule::Invite))
.expect("event is valid, we just created it"),
unsigned: None,
@ -929,7 +1014,7 @@ impl Service {
// 4.2 History Visibility
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomHistoryVisibility,
event_type: TimelineEventType::RoomHistoryVisibility,
content: to_raw_value(&RoomHistoryVisibilityEventContent::new(
HistoryVisibility::Shared,
))
@ -946,7 +1031,7 @@ impl Service {
// 4.3 Guest Access
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomGuestAccess,
event_type: TimelineEventType::RoomGuestAccess,
content: to_raw_value(&RoomGuestAccessEventContent::new(GuestAccess::Forbidden))
.expect("event is valid, we just created it"),
unsigned: None,
@ -962,7 +1047,7 @@ impl Service {
let room_name = format!("{} Admin Room", services().globals.server_name());
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomName,
event_type: TimelineEventType::RoomName,
content: to_raw_value(&RoomNameEventContent::new(Some(room_name)))
.expect("event is valid, we just created it"),
unsigned: None,
@ -976,7 +1061,7 @@ impl Service {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomTopic,
event_type: TimelineEventType::RoomTopic,
content: to_raw_value(&RoomTopicEventContent {
topic: format!("Manage {}", services().globals.server_name()),
})
@ -997,7 +1082,7 @@ impl Service {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomCanonicalAlias,
event_type: TimelineEventType::RoomCanonicalAlias,
content: to_raw_value(&RoomCanonicalAliasEventContent {
alias: Some(alias.clone()),
alt_aliases: Vec::new(),
@ -1054,7 +1139,7 @@ impl Service {
// Invite and join the real user
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Invite,
displayname: None,
@ -1076,7 +1161,7 @@ impl Service {
)?;
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMember,
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: Some(displayname),
@ -1104,7 +1189,7 @@ impl Service {
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomPowerLevels,
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
@ -1122,7 +1207,7 @@ impl Service {
// Send welcome message
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: RoomEventType::RoomMessage,
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&RoomMessageEventContent::text_html(
format!("## Thank you for trying out Conduit!\n\nConduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.\n\nHelpful links:\n> Website: https://conduit.rs\n> Git and Documentation: https://gitlab.com/famedly/conduit\n> Report issues: https://gitlab.com/famedly/conduit/-/issues\n\nFor a list of available commands, send the following message in this room: `@conduit:{}: --help`\n\nHere are some rooms you can join (by typing the command):\n\nConduit room (Ask questions and get notified on updates):\n`/join #conduit:fachschaften.org`\n\nConduit lounge (Off-topic, only Conduit users are allowed to join)\n`/join #conduit-lounge:conduit.rs`", services().globals.server_name()),
format!("<h2>Thank you for trying out Conduit!</h2>\n<p>Conduit is currently in Beta. This means you can join and participate in most Matrix rooms, but not all features are supported and you might run into bugs from time to time.</p>\n<p>Helpful links:</p>\n<blockquote>\n<p>Website: https://conduit.rs<br>Git and Documentation: https://gitlab.com/famedly/conduit<br>Report issues: https://gitlab.com/famedly/conduit/-/issues</p>\n</blockquote>\n<p>For a list of available commands, send the following message in this room: <code>@conduit:{}: --help</code></p>\n<p>Here are some rooms you can join (by typing the command):</p>\n<p>Conduit room (Ask questions and get notified on updates):<br><code>/join #conduit:fachschaften.org</code></p>\n<p>Conduit lounge (Off-topic, only Conduit users are allowed to join)<br><code>/join #conduit-lounge:conduit.rs</code></p>\n", services().globals.server_name()),

View File

@ -13,9 +13,12 @@ use crate::Result;
pub trait Data: Send + Sync {
fn next_count(&self) -> Result<u64>;
fn current_count(&self) -> Result<u64>;
fn last_check_for_updates_id(&self) -> Result<u64>;
fn update_check_for_updates_id(&self, id: u64) -> Result<()>;
async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()>;
fn cleanup(&self) -> Result<()>;
fn memory_usage(&self) -> Result<String>;
fn memory_usage(&self) -> String;
fn clear_caches(&self, amount: u32);
fn load_keypair(&self) -> Result<Ed25519KeyPair>;
fn remove_keypair(&self) -> Result<()>;
fn add_signing_key(

View File

@ -1,12 +1,13 @@
mod data;
pub use data::Data;
use ruma::{
OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedServerName, OwnedServerSigningKeyId, OwnedUserId,
serde::Base64, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedServerName,
OwnedServerSigningKeyId, OwnedUserId,
};
use crate::api::server_server::FedDest;
use crate::{Config, Error, Result};
use crate::{services, Config, Error, Result};
use ruma::{
api::{
client::sync::sync_events,
@ -20,13 +21,18 @@ use std::{
future::Future,
net::{IpAddr, SocketAddr},
path::PathBuf,
sync::{Arc, Mutex, RwLock},
sync::{
atomic::{self, AtomicBool},
Arc, Mutex, RwLock,
},
time::{Duration, Instant},
};
use tokio::sync::{broadcast, watch::Receiver, Mutex as TokioMutex, Semaphore};
use tracing::error;
use tracing::{error, info};
use trust_dns_resolver::TokioAsyncResolver;
use base64::{engine::general_purpose, Engine as _};
type WellKnownMap = HashMap<OwnedServerName, (FedDest, String)>;
type TlsNameMap = HashMap<String, (Vec<IpAddr>, u16)>;
type RateLimitState = (Instant, u32); // Time if last failed try, number of failed tries
@ -58,6 +64,8 @@ pub struct Service {
pub roomid_federationhandletime: RwLock<HashMap<OwnedRoomId, (OwnedEventId, Instant)>>,
pub stateres_mutex: Arc<Mutex<()>>,
pub rotate: RotationHandler,
pub shutdown: AtomicBool,
}
/// Handles "rotation" of long-polling requests. "Rotation" in this context is similar to "rotation" of log files and the like.
@ -160,6 +168,7 @@ impl Service {
stateres_mutex: Arc::new(Mutex::new(())),
sync_receivers: RwLock::new(HashMap::new()),
rotate: RotationHandler::new(),
shutdown: AtomicBool::new(false),
};
fs::create_dir_all(s.get_media_folder())?;
@ -202,6 +211,16 @@ impl Service {
self.db.current_count()
}
#[tracing::instrument(skip(self))]
pub fn last_check_for_updates_id(&self) -> Result<u64> {
self.db.last_check_for_updates_id()
}
#[tracing::instrument(skip(self))]
pub fn update_check_for_updates_id(&self, id: u64) -> Result<()> {
self.db.update_check_for_updates_id(id)
}
pub async fn watch(&self, user_id: &UserId, device_id: &DeviceId) -> Result<()> {
self.db.watch(user_id, device_id).await
}
@ -210,10 +229,6 @@ impl Service {
self.db.cleanup()
}
pub fn memory_usage(&self) -> Result<String> {
self.db.memory_usage()
}
pub fn server_name(&self) -> &ServerName {
self.config.server_name.as_ref()
}
@ -254,6 +269,10 @@ impl Service {
self.config.enable_lightning_bolt
}
pub fn allow_check_for_updates(&self) -> bool {
self.config.allow_check_for_updates
}
pub fn trusted_servers(&self) -> &[OwnedServerName] {
&self.config.trusted_servers
}
@ -316,7 +335,19 @@ impl Service {
&self,
origin: &ServerName,
) -> Result<BTreeMap<OwnedServerSigningKeyId, VerifyKey>> {
self.db.signing_keys_for(origin)
let mut keys = self.db.signing_keys_for(origin)?;
if origin == self.server_name() {
keys.insert(
format!("ed25519:{}", services().globals.keypair().version())
.try_into()
.expect("found invalid server signing keys in DB"),
VerifyKey {
key: Base64::new(self.keypair.public_key().to_vec()),
},
);
}
Ok(keys)
}
pub fn database_version(&self) -> Result<u64> {
@ -338,13 +369,25 @@ impl Service {
let mut r = PathBuf::new();
r.push(self.config.database_path.clone());
r.push("media");
r.push(base64::encode_config(key, base64::URL_SAFE_NO_PAD));
r.push(general_purpose::URL_SAFE_NO_PAD.encode(key));
r
}
pub fn well_known_client(&self) -> &Option<String> {
&self.config.well_known_client
}
pub fn shutdown(&self) {
self.shutdown.store(true, atomic::Ordering::Relaxed);
// On shutdown
info!(target: "shutdown-sync", "Received shutdown notification, notifying sync helpers...");
services().globals.rotate.fire();
}
}
fn reqwest_client_builder(config: &Config) -> Result<reqwest::ClientBuilder> {
let mut reqwest_client_builder = reqwest::Client::builder()
.pool_max_idle_per_host(0)
.connect_timeout(Duration::from_secs(30))
.timeout(Duration::from_secs(60 * 3));

View File

@ -8,7 +8,7 @@ use image::imageops::FilterType;
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
io::{AsyncReadExt, AsyncWriteExt, BufReader},
};
pub struct FileMeta {
@ -70,7 +70,9 @@ impl Service {
{
let path = services().globals.get_media_file(&key);
let mut file = Vec::new();
File::open(path).await?.read_to_end(&mut file).await?;
BufReader::new(File::open(path).await?)
.read_to_end(&mut file)
.await?;
Ok(Some(FileMeta {
content_disposition,

View File

@ -1,5 +1,5 @@
use std::{
collections::HashMap,
collections::{BTreeMap, HashMap},
sync::{Arc, Mutex},
};
@ -77,7 +77,15 @@ impl Services {
search: rooms::search::Service { db },
short: rooms::short::Service { db },
state: rooms::state::Service { db },
state_accessor: rooms::state_accessor::Service { db },
state_accessor: rooms::state_accessor::Service {
db,
server_visibility_cache: Mutex::new(LruCache::new(
(100.0 * config.conduit_cache_capacity_modifier) as usize,
)),
user_visibility_cache: Mutex::new(LruCache::new(
(100.0 * config.conduit_cache_capacity_modifier) as usize,
)),
},
state_cache: rooms::state_cache::Service { db },
state_compressor: rooms::state_compressor::Service {
db,
@ -89,11 +97,18 @@ impl Services {
db,
lasttimelinecount_cache: Mutex::new(HashMap::new()),
},
threads: rooms::threads::Service { db },
spaces: rooms::spaces::Service {
roomid_spacechunk_cache: Mutex::new(LruCache::new(200)),
},
user: rooms::user::Service { db },
},
transaction_ids: transaction_ids::Service { db },
uiaa: uiaa::Service { db },
users: users::Service { db },
users: users::Service {
db,
connections: Mutex::new(BTreeMap::new()),
},
account_data: account_data::Service { db },
admin: admin::Service::build(),
key_backups: key_backups::Service { db },
@ -103,4 +118,109 @@ impl Services {
globals: globals::Service::load(db, config)?,
})
}
fn memory_usage(&self) -> String {
let lazy_load_waiting = self
.rooms
.lazy_loading
.lazy_load_waiting
.lock()
.unwrap()
.len();
let server_visibility_cache = self
.rooms
.state_accessor
.server_visibility_cache
.lock()
.unwrap()
.len();
let user_visibility_cache = self
.rooms
.state_accessor
.user_visibility_cache
.lock()
.unwrap()
.len();
let stateinfo_cache = self
.rooms
.state_compressor
.stateinfo_cache
.lock()
.unwrap()
.len();
let lasttimelinecount_cache = self
.rooms
.timeline
.lasttimelinecount_cache
.lock()
.unwrap()
.len();
let roomid_spacechunk_cache = self
.rooms
.spaces
.roomid_spacechunk_cache
.lock()
.unwrap()
.len();
format!(
"\
lazy_load_waiting: {lazy_load_waiting}
server_visibility_cache: {server_visibility_cache}
user_visibility_cache: {user_visibility_cache}
stateinfo_cache: {stateinfo_cache}
lasttimelinecount_cache: {lasttimelinecount_cache}
roomid_spacechunk_cache: {roomid_spacechunk_cache}\
"
)
}
fn clear_caches(&self, amount: u32) {
if amount > 0 {
self.rooms
.lazy_loading
.lazy_load_waiting
.lock()
.unwrap()
.clear();
}
if amount > 1 {
self.rooms
.state_accessor
.server_visibility_cache
.lock()
.unwrap()
.clear();
}
if amount > 2 {
self.rooms
.state_accessor
.user_visibility_cache
.lock()
.unwrap()
.clear();
}
if amount > 3 {
self.rooms
.state_compressor
.stateinfo_cache
.lock()
.unwrap()
.clear();
}
if amount > 4 {
self.rooms
.timeline
.lasttimelinecount_cache
.lock()
.unwrap()
.clear();
}
if amount > 5 {
self.rooms
.spaces
.roomid_spacechunk_cache
.lock()
.unwrap()
.clear();
}
}
}

View File

@ -1,9 +1,9 @@
use crate::Error;
use ruma::{
events::{
room::member::RoomMemberEventContent, AnyEphemeralRoomEvent, AnyStateEvent,
AnyStrippedStateEvent, AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent,
RoomEventType, StateEvent,
room::member::RoomMemberEventContent, space::child::HierarchySpaceChildEvent,
AnyEphemeralRoomEvent, AnyMessageLikeEvent, AnyStateEvent, AnyStrippedStateEvent,
AnySyncStateEvent, AnySyncTimelineEvent, AnyTimelineEvent, StateEvent, TimelineEventType,
},
serde::Raw,
state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, MilliSecondsSinceUnixEpoch,
@ -31,7 +31,7 @@ pub struct PduEvent {
pub sender: OwnedUserId,
pub origin_server_ts: UInt,
#[serde(rename = "type")]
pub kind: RoomEventType,
pub kind: TimelineEventType,
pub content: Box<RawJsonValue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub state_key: Option<String>,
@ -53,10 +53,10 @@ impl PduEvent {
self.unsigned = None;
let allowed: &[&str] = match self.kind {
RoomEventType::RoomMember => &["join_authorised_via_users_server", "membership"],
RoomEventType::RoomCreate => &["creator"],
RoomEventType::RoomJoinRules => &["join_rule"],
RoomEventType::RoomPowerLevels => &[
TimelineEventType::RoomMember => &["join_authorised_via_users_server", "membership"],
TimelineEventType::RoomCreate => &["creator"],
TimelineEventType::RoomJoinRules => &["join_rule"],
TimelineEventType::RoomPowerLevels => &[
"ban",
"events",
"events_default",
@ -66,7 +66,7 @@ impl PduEvent {
"users",
"users_default",
],
RoomEventType::RoomHistoryVisibility => &["history_visibility"],
TimelineEventType::RoomHistoryVisibility => &["history_visibility"],
_ => &[],
};
@ -103,6 +103,19 @@ impl PduEvent {
Ok(())
}
pub fn add_age(&mut self) -> crate::Result<()> {
let mut unsigned: BTreeMap<String, Box<RawJsonValue>> = self
.unsigned
.as_ref()
.map_or_else(|| Ok(BTreeMap::new()), |u| serde_json::from_str(u.get()))
.map_err(|_| Error::bad_database("Invalid unsigned in pdu event"))?;
unsigned.insert("age".to_owned(), to_raw_value(&1).unwrap());
self.unsigned = Some(to_raw_value(&unsigned).expect("unsigned is valid"));
Ok(())
}
#[tracing::instrument(skip(self))]
pub fn to_sync_room_event(&self) -> Raw<AnySyncTimelineEvent> {
let mut json = json!({
@ -111,9 +124,11 @@ impl PduEvent {
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"unsigned": self.unsigned,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
@ -133,10 +148,12 @@ impl PduEvent {
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"unsigned": self.unsigned,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
@ -155,10 +172,36 @@ impl PduEvent {
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"unsigned": self.unsigned,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
if let Some(redacts) = &self.redacts {
json["redacts"] = json!(redacts);
}
serde_json::from_value(json).expect("Raw::from_value always works")
}
#[tracing::instrument(skip(self))]
pub fn to_message_like_event(&self) -> Raw<AnyMessageLikeEvent> {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"room_id": self.room_id,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
if let Some(state_key) = &self.state_key {
json["state_key"] = json!(state_key);
}
@ -171,32 +214,38 @@ impl PduEvent {
#[tracing::instrument(skip(self))]
pub fn to_state_event(&self) -> Raw<AnyStateEvent> {
let json = json!({
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"unsigned": self.unsigned,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Raw::from_value always works")
}
#[tracing::instrument(skip(self))]
pub fn to_sync_state_event(&self) -> Raw<AnySyncStateEvent> {
let json = json!({
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"unsigned": self.unsigned,
"state_key": self.state_key,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Raw::from_value always works")
}
@ -213,19 +262,35 @@ impl PduEvent {
}
#[tracing::instrument(skip(self))]
pub fn to_member_event(&self) -> Raw<StateEvent<RoomMemberEventContent>> {
pub fn to_stripped_spacechild_state_event(&self) -> Raw<HierarchySpaceChildEvent> {
let json = json!({
"content": self.content,
"type": self.kind,
"sender": self.sender,
"state_key": self.state_key,
"origin_server_ts": self.origin_server_ts,
});
serde_json::from_value(json).expect("Raw::from_value always works")
}
#[tracing::instrument(skip(self))]
pub fn to_member_event(&self) -> Raw<StateEvent<RoomMemberEventContent>> {
let mut json = json!({
"content": self.content,
"type": self.kind,
"event_id": self.event_id,
"sender": self.sender,
"origin_server_ts": self.origin_server_ts,
"redacts": self.redacts,
"unsigned": self.unsigned,
"room_id": self.room_id,
"state_key": self.state_key,
});
if let Some(unsigned) = &self.unsigned {
json["unsigned"] = json!(unsigned);
}
serde_json::from_value(json).expect("Raw::from_value always works")
}
@ -281,7 +346,7 @@ impl state_res::Event for PduEvent {
&self.sender
}
fn event_type(&self) -> &RoomEventType {
fn event_type(&self) -> &TimelineEventType {
&self.kind
}
@ -357,7 +422,7 @@ pub(crate) fn gen_event_id_canonical_json(
#[derive(Debug, Deserialize)]
pub struct PduBuilder {
#[serde(rename = "type")]
pub event_type: RoomEventType,
pub event_type: TimelineEventType,
pub content: Box<RawJsonValue>,
pub unsigned: Option<BTreeMap<String, serde_json::Value>>,
pub state_key: Option<String>,

View File

@ -13,10 +13,7 @@ use ruma::{
},
IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken,
},
events::{
room::{name::RoomNameEventContent, power_levels::RoomPowerLevelsEventContent},
RoomEventType, StateEventType,
},
events::{room::power_levels::RoomPowerLevelsEventContent, StateEventType, TimelineEventType},
push::{Action, PushConditionRoomCtx, PushFormat, Ruleset, Tweak},
serde::Raw,
uint, RoomId, UInt, UserId,
@ -162,13 +159,12 @@ impl Service {
&pdu.room_id,
)? {
let n = match action {
Action::DontNotify => false,
// TODO: Implement proper support for coalesce
Action::Notify | Action::Coalesce => true,
Action::Notify => true,
Action::SetTweak(tweak) => {
tweaks.push(tweak.clone());
continue;
}
_ => false,
};
if notify.is_some() {
@ -248,7 +244,7 @@ impl Service {
// TODO: missed calls
notifi.counts = NotificationCounts::new(unread, uint!(0));
if event.kind == RoomEventType::RoomEncrypted
if event.kind == TimelineEventType::RoomEncrypted
|| tweaks
.iter()
.any(|t| matches!(t, Tweak::Highlight(true) | Tweak::Sound(_)))
@ -264,28 +260,14 @@ impl Service {
notifi.event_type = Some(event.kind.clone());
notifi.content = serde_json::value::to_raw_value(&event.content).ok();
if event.kind == RoomEventType::RoomMember {
if event.kind == TimelineEventType::RoomMember {
notifi.user_is_target =
event.state_key.as_deref() == Some(event.sender.as_str());
}
notifi.sender_display_name = services().users.displayname(&event.sender)?;
let room_name = if let Some(room_name_pdu) = services()
.rooms
.state_accessor
.room_state_get(&event.room_id, &StateEventType::RoomName, "")?
{
serde_json::from_str::<RoomNameEventContent>(room_name_pdu.content.get())
.map_err(|_| {
Error::bad_database("Invalid room name event in database.")
})?
.name
} else {
None
};
notifi.room_name = room_name;
notifi.room_name = services().rooms.state_accessor.get_name(&event.room_id)?;
self.send_request(&http.url, send_event_notification::v1::Request::new(notifi))
.await?;

View File

@ -38,6 +38,8 @@ use tracing::{debug, error, info, trace, warn};
use crate::{service::*, services, Error, PduEvent, Result};
use super::state_compressor::CompressedStateEvent;
pub struct Service;
impl Service {
@ -62,9 +64,8 @@ impl Service {
/// 12. Ensure that the state is derived from the previous current state (i.e. we calculated by
/// doing state res where one of the inputs was a previously trusted set of state, don't just
/// trust a set of state we got from a remote)
/// 13. Check if the event passes auth based on the "current state" of the room, if not "soft fail"
/// it
/// 14. Use state resolution to find new room state
/// 13. Use state resolution to find new room state
/// 14. Check if the event passes auth based on the "current state" of the room, if not soft fail it
// We use some AsyncRecursiveType hacks here so we can call this async funtion recursively
#[tracing::instrument(skip(self, value, is_timeline_event, pub_key_map))]
pub(crate) async fn handle_incoming_pdu<'a>(
@ -118,6 +119,7 @@ impl Service {
let (incoming_pdu, val) = self
.handle_outlier_pdu(origin, &create_event, event_id, room_id, value, pub_key_map)
.await?;
self.check_room_id(room_id, &incoming_pdu)?;
// 8. if not timeline event: stop
if !is_timeline_event {
@ -304,7 +306,7 @@ impl Service {
) {
Err(e) => {
// Drop
warn!("Dropping bad event {}: {}", event_id, e);
warn!("Dropping bad event {}: {}", event_id, e,);
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Signature verification failed",
@ -337,6 +339,8 @@ impl Service {
)
.map_err(|_| Error::bad_database("Event is not a valid PDU."))?;
self.check_room_id(room_id, &incoming_pdu)?;
// 4. fetch any missing auth events doing all checks listed here starting at 1. These are not timeline events
// 5. Reject "due to auth events" if can't get all the auth events or some of the auth events are also rejected "due to auth events"
// NOTE: Step 5 is not applied anymore because it failed too often
@ -356,7 +360,7 @@ impl Service {
.await;
// 6. Reject "due to auth events" if the event doesn't pass auth based on the auth events
info!(
debug!(
"Auth check for {} based on auth events",
incoming_pdu.event_id
);
@ -372,6 +376,8 @@ impl Service {
}
};
self.check_room_id(room_id, &auth_event)?;
match auth_events.entry((
auth_event.kind.to_string().into(),
auth_event
@ -392,11 +398,12 @@ impl Service {
}
// The original create event must be in the auth events
if auth_events
.get(&(StateEventType::RoomCreate, "".to_owned()))
.map(|a| a.as_ref())
!= Some(create_event)
{
if !matches!(
auth_events
.get(&(StateEventType::RoomCreate, "".to_owned()))
.map(|a| a.as_ref()),
Some(_) | None
) {
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Incoming event refers to wrong create event.",
@ -417,7 +424,7 @@ impl Service {
));
}
info!("Validation successful.");
debug!("Validation successful.");
// 7. Persist the event as an outlier.
services()
@ -425,7 +432,7 @@ impl Service {
.outlier
.add_pdu_outlier(&incoming_pdu.event_id, &val)?;
info!("Added pdu as outlier.");
debug!("Added pdu as outlier.");
Ok((Arc::new(incoming_pdu), val))
})
@ -474,7 +481,7 @@ impl Service {
// TODO: if we know the prev_events of the incoming event we can avoid the request and build
// the state from a known point and resolve if > 1 prev_event
info!("Requesting state at event");
debug!("Requesting state at event");
let mut state_at_incoming_event = None;
if incoming_pdu.prev_events.len() == 1 {
@ -497,7 +504,7 @@ impl Service {
};
if let Some(Ok(mut state)) = state {
info!("Using cached state");
debug!("Using cached state");
let prev_pdu = services()
.rooms
.timeline
@ -521,7 +528,7 @@ impl Service {
state_at_incoming_event = Some(state);
}
} else {
info!("Calculating state at event using state res");
debug!("Calculating state at event using state res");
let mut extremity_sstatehashes = HashMap::new();
let mut okay = true;
@ -630,7 +637,7 @@ impl Service {
}
if state_at_incoming_event.is_none() {
info!("Calling /state_ids");
debug!("Calling /state_ids");
// Call /state_ids to find out what the state at this pdu is. We trust the server's
// response to some extend, but we still do a lot of checks on the events
match services()
@ -645,7 +652,7 @@ impl Service {
.await
{
Ok(res) => {
info!("Fetching state events at event.");
debug!("Fetching state events at event.");
let state_vec = self
.fetch_and_handle_outliers(
origin,
@ -708,7 +715,7 @@ impl Service {
let state_at_incoming_event =
state_at_incoming_event.expect("we always set this to some above");
info!("Starting auth check");
debug!("Starting auth check");
// 11. Check the auth of the event passes based on the state of the event
let check_result = state_res::event_auth::auth_check(
&room_version,
@ -732,10 +739,28 @@ impl Service {
"Event has failed auth check with state at the event.",
));
}
info!("Auth check succeeded");
debug!("Auth check succeeded");
// Soft fail check before doing state res
let auth_events = services().rooms.state.get_auth_events(
room_id,
&incoming_pdu.kind,
&incoming_pdu.sender,
incoming_pdu.state_key.as_deref(),
&incoming_pdu.content,
)?;
let soft_fail = !state_res::event_auth::auth_check(
&room_version,
&incoming_pdu,
None::<PduEvent>,
|k, s| auth_events.get(&(k.clone(), s.to_owned())),
)
.map_err(|_e| Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed."))?;
// 13. Use state resolution to find new room state
// We start looking at current room state now, so lets lock the room
let mutex_state = Arc::clone(
services()
.globals
@ -749,7 +774,7 @@ impl Service {
// Now we calculate the set of extremities this room has after the incoming event has been
// applied. We start with the previous extremities (aka leaves)
info!("Calculating extremities");
debug!("Calculating extremities");
let mut extremities = services().rooms.state.get_forward_extremities(room_id)?;
// Remove any forward extremities that are referenced by this incoming event's prev_events
@ -770,35 +795,54 @@ impl Service {
)
});
info!("Compressing state at event");
let state_ids_compressed = state_at_incoming_event
.iter()
.map(|(shortstatekey, id)| {
services()
.rooms
.state_compressor
.compress_state_event(*shortstatekey, id)
})
.collect::<Result<_>>()?;
debug!("Compressing state at event");
let state_ids_compressed = Arc::new(
state_at_incoming_event
.iter()
.map(|(shortstatekey, id)| {
services()
.rooms
.state_compressor
.compress_state_event(*shortstatekey, id)
})
.collect::<Result<_>>()?,
);
// 13. Check if the event passes auth based on the "current state" of the room, if not "soft fail" it
info!("Starting soft fail auth check");
if incoming_pdu.state_key.is_some() {
debug!("Preparing for stateres to derive new room state");
let auth_events = services().rooms.state.get_auth_events(
room_id,
&incoming_pdu.kind,
&incoming_pdu.sender,
incoming_pdu.state_key.as_deref(),
&incoming_pdu.content,
)?;
// We also add state after incoming event to the fork states
let mut state_after = state_at_incoming_event.clone();
if let Some(state_key) = &incoming_pdu.state_key {
let shortstatekey = services().rooms.short.get_or_create_shortstatekey(
&incoming_pdu.kind.to_string().into(),
state_key,
)?;
let soft_fail = !state_res::event_auth::auth_check(
&room_version,
&incoming_pdu,
None::<PduEvent>,
|k, s| auth_events.get(&(k.clone(), s.to_owned())),
)
.map_err(|_e| Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed."))?;
state_after.insert(shortstatekey, Arc::from(&*incoming_pdu.event_id));
}
let new_room_state = self
.resolve_state(room_id, room_version_id, state_after)
.await?;
// Set the new room state to the resolved state
debug!("Forcing new room state");
let (sstatehash, new, removed) = services()
.rooms
.state_compressor
.save_state(room_id, new_room_state)?;
services()
.rooms
.state
.force_state(room_id, sstatehash, new, removed, &state_lock)
.await?;
}
// 14. Check if the event passes auth based on the "current state" of the room, if not soft fail it
debug!("Starting soft fail auth check");
if soft_fail {
services().rooms.timeline.append_incoming_pdu(
@ -822,182 +866,7 @@ impl Service {
));
}
if incoming_pdu.state_key.is_some() {
info!("Loading current room state ids");
let current_sstatehash = services()
.rooms
.state
.get_room_shortstatehash(room_id)?
.expect("every room has state");
let current_state_ids = services()
.rooms
.state_accessor
.state_full_ids(current_sstatehash)
.await?;
info!("Preparing for stateres to derive new room state");
let mut extremity_sstatehashes = HashMap::new();
info!(?extremities, "Loading extremities");
for id in &extremities {
match services().rooms.timeline.get_pdu(id)? {
Some(leaf_pdu) => {
extremity_sstatehashes.insert(
services()
.rooms
.state_accessor
.pdu_shortstatehash(&leaf_pdu.event_id)?
.ok_or_else(|| {
error!(
"Found extremity pdu with no statehash in db: {:?}",
leaf_pdu
);
Error::bad_database("Found pdu with no statehash in db.")
})?,
leaf_pdu,
);
}
_ => {
error!("Missing state snapshot for {:?}", id);
return Err(Error::BadDatabase("Missing state snapshot."));
}
}
}
let mut fork_states = Vec::new();
// 12. Ensure that the state is derived from the previous current state (i.e. we calculated
// by doing state res where one of the inputs was a previously trusted set of state,
// don't just trust a set of state we got from a remote).
// We do this by adding the current state to the list of fork states
extremity_sstatehashes.remove(&current_sstatehash);
fork_states.push(current_state_ids);
// We also add state after incoming event to the fork states
let mut state_after = state_at_incoming_event.clone();
if let Some(state_key) = &incoming_pdu.state_key {
let shortstatekey = services().rooms.short.get_or_create_shortstatekey(
&incoming_pdu.kind.to_string().into(),
state_key,
)?;
state_after.insert(shortstatekey, Arc::from(&*incoming_pdu.event_id));
}
fork_states.push(state_after);
let mut update_state = false;
// 14. Use state resolution to find new room state
let new_room_state = if fork_states.is_empty() {
panic!("State is empty");
} else if fork_states.iter().skip(1).all(|f| &fork_states[0] == f) {
info!("State resolution trivial");
// There was only one state, so it has to be the room's current state (because that is
// always included)
fork_states[0]
.iter()
.map(|(k, id)| {
services()
.rooms
.state_compressor
.compress_state_event(*k, id)
})
.collect::<Result<_>>()?
} else {
info!("Loading auth chains");
// We do need to force an update to this room's state
update_state = true;
let mut auth_chain_sets = Vec::new();
for state in &fork_states {
auth_chain_sets.push(
services()
.rooms
.auth_chain
.get_auth_chain(
room_id,
state.iter().map(|(_, id)| id.clone()).collect(),
)
.await?
.collect(),
);
}
info!("Loading fork states");
let fork_states: Vec<_> = fork_states
.into_iter()
.map(|map| {
map.into_iter()
.filter_map(|(k, id)| {
services()
.rooms
.short
.get_statekey_from_short(k)
.map(|(ty, st_key)| ((ty.to_string().into(), st_key), id))
.ok()
})
.collect::<StateMap<_>>()
})
.collect();
info!("Resolving state");
let lock = services().globals.stateres_mutex.lock();
let state = match state_res::resolve(
room_version_id,
&fork_states,
auth_chain_sets,
|id| {
let res = services().rooms.timeline.get_pdu(id);
if let Err(e) = &res {
error!("LOOK AT ME Failed to fetch event: {}", e);
}
res.ok().flatten()
},
) {
Ok(new_state) => new_state,
Err(_) => {
return Err(Error::bad_database("State resolution failed, either an event could not be found or deserialization"));
}
};
drop(lock);
info!("State resolution done. Compressing state");
state
.into_iter()
.map(|((event_type, state_key), event_id)| {
let shortstatekey = services().rooms.short.get_or_create_shortstatekey(
&event_type.to_string().into(),
&state_key,
)?;
services()
.rooms
.state_compressor
.compress_state_event(shortstatekey, &event_id)
})
.collect::<Result<_>>()?
};
// Set the new room state to the resolved state
if update_state {
info!("Forcing new room state");
let (sstatehash, new, removed) = services()
.rooms
.state_compressor
.save_state(room_id, new_room_state)?;
services()
.rooms
.state
.force_state(room_id, sstatehash, new, removed, &state_lock)
.await?;
}
}
info!("Appending pdu to timeline");
debug!("Appending pdu to timeline");
extremities.insert(incoming_pdu.event_id.clone());
// Now that the event has passed all auth it is added into the timeline.
@ -1013,13 +882,101 @@ impl Service {
&state_lock,
)?;
info!("Appended incoming pdu");
debug!("Appended incoming pdu");
// Event has passed all auth/stateres checks
drop(state_lock);
Ok(pdu_id)
}
async fn resolve_state(
&self,
room_id: &RoomId,
room_version_id: &RoomVersionId,
incoming_state: HashMap<u64, Arc<EventId>>,
) -> Result<Arc<HashSet<CompressedStateEvent>>> {
debug!("Loading current room state ids");
let current_sstatehash = services()
.rooms
.state
.get_room_shortstatehash(room_id)?
.expect("every room has state");
let current_state_ids = services()
.rooms
.state_accessor
.state_full_ids(current_sstatehash)
.await?;
let fork_states = [current_state_ids, incoming_state];
let mut auth_chain_sets = Vec::new();
for state in &fork_states {
auth_chain_sets.push(
services()
.rooms
.auth_chain
.get_auth_chain(room_id, state.iter().map(|(_, id)| id.clone()).collect())
.await?
.collect(),
);
}
debug!("Loading fork states");
let fork_states: Vec<_> = fork_states
.into_iter()
.map(|map| {
map.into_iter()
.filter_map(|(k, id)| {
services()
.rooms
.short
.get_statekey_from_short(k)
.map(|(ty, st_key)| ((ty.to_string().into(), st_key), id))
.ok()
})
.collect::<StateMap<_>>()
})
.collect();
debug!("Resolving state");
let lock = services().globals.stateres_mutex.lock();
let state = match state_res::resolve(room_version_id, &fork_states, auth_chain_sets, |id| {
let res = services().rooms.timeline.get_pdu(id);
if let Err(e) = &res {
error!("LOOK AT ME Failed to fetch event: {}", e);
}
res.ok().flatten()
}) {
Ok(new_state) => new_state,
Err(_) => {
return Err(Error::bad_database("State resolution failed, either an event could not be found or deserialization"));
}
};
drop(lock);
debug!("State resolution done. Compressing state");
let new_room_state = state
.into_iter()
.map(|((event_type, state_key), event_id)| {
let shortstatekey = services()
.rooms
.short
.get_or_create_shortstatekey(&event_type.to_string().into(), &state_key)?;
services()
.rooms
.state_compressor
.compress_state_event(shortstatekey, &event_id)
})
.collect::<Result<_>>()?;
Ok(Arc::new(new_room_state))
}
/// Find the event and auth it. Once the event is validated (steps 1 - 8)
/// it is appended to the outliers Tree.
///
@ -1226,6 +1183,8 @@ impl Service {
.await
.pop()
{
self.check_room_id(room_id, &pdu)?;
if amount > services().globals.max_fetch_prev_events() {
// Max limit reached
warn!("Max prev event limit reached!");
@ -1459,12 +1418,12 @@ impl Service {
}
if servers.is_empty() {
// We had all keys locally
info!("We had all keys locally");
return Ok(());
}
for server in services().globals.trusted_servers() {
trace!("Asking batch signing keys from trusted server {}", server);
info!("Asking batch signing keys from trusted server {}", server);
if let Ok(keys) = services()
.sending
.send_federation_request(
@ -1507,10 +1466,12 @@ impl Service {
}
if servers.is_empty() {
info!("Trusted server supplied all signing keys");
return Ok(());
}
}
info!("Asking individual servers for signing keys: {servers:?}");
let mut futures: FuturesUnordered<_> = servers
.into_keys()
.map(|server| async move {
@ -1525,21 +1486,27 @@ impl Service {
.collect();
while let Some(result) = futures.next().await {
info!("Received new result");
if let (Ok(get_keys_response), origin) = result {
let result: BTreeMap<_, _> = services()
.globals
.add_signing_key(&origin, get_keys_response.server_key.deserialize().unwrap())?
.into_iter()
.map(|(k, v)| (k.to_string(), v.key))
.collect();
pub_key_map
.write()
.map_err(|_| Error::bad_database("RwLock is poisoned."))?
.insert(origin.to_string(), result);
info!("Result is from {origin}");
if let Ok(key) = get_keys_response.server_key.deserialize() {
let result: BTreeMap<_, _> = services()
.globals
.add_signing_key(&origin, key)?
.into_iter()
.map(|(k, v)| (k.to_string(), v.key))
.collect();
pub_key_map
.write()
.map_err(|_| Error::bad_database("RwLock is poisoned."))?
.insert(origin.to_string(), result);
}
}
info!("Done handling result");
}
info!("Search for signing keys done");
Ok(())
}
@ -1566,9 +1533,13 @@ impl Service {
if acl_event_content.is_allowed(server_name) {
Ok(())
} else {
info!(
"Server {} was denied by room ACL in {}",
server_name, room_id
);
Err(Error::BadRequest(
ErrorKind::Forbidden,
"Server was denied by ACL",
"Server was denied by room ACL",
))
}
}
@ -1738,4 +1709,15 @@ impl Service {
"Failed to find public key for server",
))
}
fn check_room_id(&self, room_id: &RoomId, pdu: &PduEvent) -> Result<()> {
if pdu.room_id != room_id {
warn!("Found event from room {} in room {}", pdu.room_id, room_id);
return Err(Error::BadRequest(
ErrorKind::InvalidParam,
"Event has wrong room id",
));
}
Ok(())
}
}

View File

@ -9,11 +9,13 @@ use ruma::{DeviceId, OwnedDeviceId, OwnedRoomId, OwnedUserId, RoomId, UserId};
use crate::Result;
use super::timeline::PduCount;
pub struct Service {
pub db: &'static dyn Data,
pub lazy_load_waiting:
Mutex<HashMap<(OwnedUserId, OwnedDeviceId, OwnedRoomId, u64), HashSet<OwnedUserId>>>,
Mutex<HashMap<(OwnedUserId, OwnedDeviceId, OwnedRoomId, PduCount), HashSet<OwnedUserId>>>,
}
impl Service {
@ -36,7 +38,7 @@ impl Service {
device_id: &DeviceId,
room_id: &RoomId,
lazy_load: HashSet<OwnedUserId>,
count: u64,
count: PduCount,
) {
self.lazy_load_waiting.lock().unwrap().insert(
(
@ -55,7 +57,7 @@ impl Service {
user_id: &UserId,
device_id: &DeviceId,
room_id: &RoomId,
since: u64,
since: PduCount,
) -> Result<()> {
if let Some(user_ids) = self.lazy_load_waiting.lock().unwrap().remove(&(
user_id.to_owned(),

View File

@ -9,10 +9,12 @@ pub mod outlier;
pub mod pdu_metadata;
pub mod search;
pub mod short;
pub mod spaces;
pub mod state;
pub mod state_accessor;
pub mod state_cache;
pub mod state_compressor;
pub mod threads;
pub mod timeline;
pub mod user;
@ -32,6 +34,7 @@ pub trait Data:
+ state_cache::Data
+ state_compressor::Data
+ timeline::Data
+ threads::Data
+ user::Data
{
}
@ -53,5 +56,7 @@ pub struct Service {
pub state_cache: state_cache::Service,
pub state_compressor: state_compressor::Service,
pub timeline: timeline::Service,
pub threads: threads::Service,
pub spaces: spaces::Service,
pub user: user::Service,
}

View File

@ -1,9 +1,17 @@
use std::sync::Arc;
use crate::Result;
use ruma::{EventId, RoomId};
use crate::{service::rooms::timeline::PduCount, PduEvent, Result};
use ruma::{EventId, RoomId, UserId};
pub trait Data: Send + Sync {
fn add_relation(&self, from: u64, to: u64) -> Result<()>;
fn relations_until<'a>(
&'a self,
user_id: &'a UserId,
room_id: u64,
target: u64,
until: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>>;
fn mark_as_referenced(&self, room_id: &RoomId, event_ids: &[Arc<EventId>]) -> Result<()>;
fn is_event_referenced(&self, room_id: &RoomId, event_id: &EventId) -> Result<bool>;
fn mark_event_soft_failed(&self, event_id: &EventId) -> Result<()>;

View File

@ -2,15 +2,171 @@ mod data;
use std::sync::Arc;
pub use data::Data;
use ruma::{EventId, RoomId};
use ruma::{
api::client::relations::get_relating_events,
events::{relation::RelationType, TimelineEventType},
EventId, RoomId, UserId,
};
use serde::Deserialize;
use crate::Result;
use crate::{services, PduEvent, Result};
use super::timeline::PduCount;
pub struct Service {
pub db: &'static dyn Data,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelType {
rel_type: RelationType,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractRelType,
}
impl Service {
#[tracing::instrument(skip(self, from, to))]
pub fn add_relation(&self, from: PduCount, to: PduCount) -> Result<()> {
match (from, to) {
(PduCount::Normal(f), PduCount::Normal(t)) => self.db.add_relation(f, t),
_ => {
// TODO: Relations with backfilled pdus
Ok(())
}
}
}
pub fn paginate_relations_with_filter(
&self,
sender_user: &UserId,
room_id: &RoomId,
target: &EventId,
filter_event_type: Option<TimelineEventType>,
filter_rel_type: Option<RelationType>,
from: PduCount,
to: Option<PduCount>,
limit: usize,
) -> Result<get_relating_events::v1::Response> {
let next_token;
//TODO: Fix ruma: match body.dir {
match ruma::api::Direction::Backward {
ruma::api::Direction::Forward => {
let events_after: Vec<_> = services()
.rooms
.pdu_metadata
.relations_until(sender_user, room_id, target, from)? // TODO: should be relations_after
.filter(|r| {
r.as_ref().map_or(true, |(_, pdu)| {
filter_event_type.as_ref().map_or(true, |t| &pdu.kind == t)
&& if let Ok(content) =
serde_json::from_str::<ExtractRelatesToEventId>(
pdu.content.get(),
)
{
filter_rel_type
.as_ref()
.map_or(true, |r| &content.relates_to.rel_type == r)
} else {
false
}
})
})
.take(limit)
.filter_map(|r| r.ok()) // Filter out buggy events
.filter(|(_, pdu)| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.collect();
next_token = events_after.last().map(|(count, _)| count).copied();
let events_after: Vec<_> = events_after
.into_iter()
.rev() // relations are always most recent first
.map(|(_, pdu)| pdu.to_message_like_event())
.collect();
Ok(get_relating_events::v1::Response {
chunk: events_after,
next_batch: next_token.map(|t| t.stringify()),
prev_batch: Some(from.stringify()),
})
}
ruma::api::Direction::Backward => {
let events_before: Vec<_> = services()
.rooms
.pdu_metadata
.relations_until(sender_user, &room_id, target, from)?
.filter(|r| {
r.as_ref().map_or(true, |(_, pdu)| {
filter_event_type.as_ref().map_or(true, |t| &pdu.kind == t)
&& if let Ok(content) =
serde_json::from_str::<ExtractRelatesToEventId>(
pdu.content.get(),
)
{
filter_rel_type
.as_ref()
.map_or(true, |r| &content.relates_to.rel_type == r)
} else {
false
}
})
})
.take(limit)
.filter_map(|r| r.ok()) // Filter out buggy events
.filter(|(_, pdu)| {
services()
.rooms
.state_accessor
.user_can_see_event(sender_user, &room_id, &pdu.event_id)
.unwrap_or(false)
})
.take_while(|&(k, _)| Some(k) != to) // Stop at `to`
.collect();
next_token = events_before.last().map(|(count, _)| count).copied();
let events_before: Vec<_> = events_before
.into_iter()
.map(|(_, pdu)| pdu.to_message_like_event())
.collect();
Ok(get_relating_events::v1::Response {
chunk: events_before,
next_batch: next_token.map(|t| t.stringify()),
prev_batch: Some(from.stringify()),
})
}
}
}
pub fn relations_until<'a>(
&'a self,
user_id: &'a UserId,
room_id: &'a RoomId,
target: &'a EventId,
until: PduCount,
) -> Result<impl Iterator<Item = Result<(PduCount, PduEvent)>> + 'a> {
let room_id = services().rooms.short.get_or_create_shortroomid(room_id)?;
let target = match services().rooms.timeline.get_pdu_count(target)? {
Some(PduCount::Normal(c)) => c,
// TODO: Support backfilled relations
_ => 0, // This will result in an empty iterator
};
self.db.relations_until(user_id, room_id, target, until)
}
#[tracing::instrument(skip(self, room_id, event_ids))]
pub fn mark_as_referenced(&self, room_id: &RoomId, event_ids: &[Arc<EventId>]) -> Result<()> {
self.db.mark_as_referenced(room_id, event_ids)

View File

@ -0,0 +1,505 @@
use std::sync::{Arc, Mutex};
use lru_cache::LruCache;
use ruma::{
api::{
client::{
error::ErrorKind,
space::{get_hierarchy, SpaceHierarchyRoomsChunk},
},
federation,
},
events::{
room::{
avatar::RoomAvatarEventContent,
canonical_alias::RoomCanonicalAliasEventContent,
create::RoomCreateEventContent,
guest_access::{GuestAccess, RoomGuestAccessEventContent},
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
join_rules::{self, AllowRule, JoinRule, RoomJoinRulesEventContent},
topic::RoomTopicEventContent,
},
space::child::SpaceChildEventContent,
StateEventType,
},
space::SpaceRoomJoinRule,
OwnedRoomId, RoomId, UserId,
};
use tracing::{debug, error, warn};
use crate::{services, Error, PduEvent, Result};
pub enum CachedJoinRule {
//Simplified(SpaceRoomJoinRule),
Full(JoinRule),
}
pub struct CachedSpaceChunk {
chunk: SpaceHierarchyRoomsChunk,
children: Vec<OwnedRoomId>,
join_rule: CachedJoinRule,
}
pub struct Service {
pub roomid_spacechunk_cache: Mutex<LruCache<OwnedRoomId, Option<CachedSpaceChunk>>>,
}
impl Service {
pub async fn get_hierarchy(
&self,
sender_user: &UserId,
room_id: &RoomId,
limit: usize,
skip: usize,
max_depth: usize,
suggested_only: bool,
) -> Result<get_hierarchy::v1::Response> {
let mut left_to_skip = skip;
let mut rooms_in_path = Vec::new();
let mut stack = vec![vec![room_id.to_owned()]];
let mut results = Vec::new();
while let Some(current_room) = {
while stack.last().map_or(false, |s| s.is_empty()) {
stack.pop();
}
if !stack.is_empty() {
stack.last_mut().and_then(|s| s.pop())
} else {
None
}
} {
rooms_in_path.push(current_room.clone());
if results.len() >= limit {
break;
}
if let Some(cached) = self
.roomid_spacechunk_cache
.lock()
.unwrap()
.get_mut(&current_room.to_owned())
.as_ref()
{
if let Some(cached) = cached {
let allowed = match &cached.join_rule {
//CachedJoinRule::Simplified(s) => {
//self.handle_simplified_join_rule(s, sender_user, &current_room)?
//}
CachedJoinRule::Full(f) => {
self.handle_join_rule(f, sender_user, &current_room)?
}
};
if allowed {
if left_to_skip > 0 {
left_to_skip -= 1;
} else {
results.push(cached.chunk.clone());
}
if rooms_in_path.len() < max_depth {
stack.push(cached.children.clone());
}
}
}
continue;
}
if let Some(current_shortstatehash) = services()
.rooms
.state
.get_room_shortstatehash(&current_room)?
{
let state = services()
.rooms
.state_accessor
.state_full_ids(current_shortstatehash)
.await?;
let mut children_ids = Vec::new();
let mut children_pdus = Vec::new();
for (key, id) in state {
let (event_type, state_key) =
services().rooms.short.get_statekey_from_short(key)?;
if event_type != StateEventType::SpaceChild {
continue;
}
let pdu = services()
.rooms
.timeline
.get_pdu(&id)?
.ok_or_else(|| Error::bad_database("Event in space state not found"))?;
if serde_json::from_str::<SpaceChildEventContent>(pdu.content.get())
.ok()
.and_then(|c| c.via)
.map_or(true, |v| v.is_empty())
{
continue;
}
if let Ok(room_id) = OwnedRoomId::try_from(state_key) {
children_ids.push(room_id);
children_pdus.push(pdu);
}
}
// TODO: Sort children
children_ids.reverse();
let chunk = self.get_room_chunk(sender_user, &current_room, children_pdus);
if let Ok(chunk) = chunk {
if left_to_skip > 0 {
left_to_skip -= 1;
} else {
results.push(chunk.clone());
}
let join_rule = services()
.rooms
.state_accessor
.room_state_get(&current_room, &StateEventType::RoomJoinRules, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomJoinRulesEventContent| c.join_rule)
.map_err(|e| {
error!("Invalid room join rule event in database: {}", e);
Error::BadDatabase("Invalid room join rule event in database.")
})
})
.transpose()?
.unwrap_or(JoinRule::Invite);
self.roomid_spacechunk_cache.lock().unwrap().insert(
current_room.clone(),
Some(CachedSpaceChunk {
chunk,
children: children_ids.clone(),
join_rule: CachedJoinRule::Full(join_rule),
}),
);
}
if rooms_in_path.len() < max_depth {
stack.push(children_ids);
}
} else {
let server = current_room.server_name();
if server == services().globals.server_name() {
continue;
}
if !results.is_empty() {
// Early return so the client can see some data already
break;
}
warn!("Asking {server} for /hierarchy");
if let Ok(response) = services()
.sending
.send_federation_request(
&server,
federation::space::get_hierarchy::v1::Request {
room_id: current_room.to_owned(),
suggested_only,
},
)
.await
{
warn!("Got response from {server} for /hierarchy\n{response:?}");
let chunk = SpaceHierarchyRoomsChunk {
canonical_alias: response.room.canonical_alias,
name: response.room.name,
num_joined_members: response.room.num_joined_members,
room_id: response.room.room_id,
topic: response.room.topic,
world_readable: response.room.world_readable,
guest_can_join: response.room.guest_can_join,
avatar_url: response.room.avatar_url,
join_rule: response.room.join_rule.clone(),
room_type: response.room.room_type,
children_state: response.room.children_state,
};
let children = response
.children
.iter()
.map(|c| c.room_id.clone())
.collect::<Vec<_>>();
let join_rule = match response.room.join_rule {
SpaceRoomJoinRule::Invite => JoinRule::Invite,
SpaceRoomJoinRule::Knock => JoinRule::Knock,
SpaceRoomJoinRule::Private => JoinRule::Private,
SpaceRoomJoinRule::Restricted => {
JoinRule::Restricted(join_rules::Restricted {
allow: response
.room
.allowed_room_ids
.into_iter()
.map(|room| AllowRule::room_membership(room))
.collect(),
})
}
SpaceRoomJoinRule::KnockRestricted => {
JoinRule::KnockRestricted(join_rules::Restricted {
allow: response
.room
.allowed_room_ids
.into_iter()
.map(|room| AllowRule::room_membership(room))
.collect(),
})
}
SpaceRoomJoinRule::Public => JoinRule::Public,
_ => return Err(Error::BadServerResponse("Unknown join rule")),
};
if self.handle_join_rule(&join_rule, sender_user, &current_room)? {
if left_to_skip > 0 {
left_to_skip -= 1;
} else {
results.push(chunk.clone());
}
if rooms_in_path.len() < max_depth {
stack.push(children.clone());
}
}
self.roomid_spacechunk_cache.lock().unwrap().insert(
current_room.clone(),
Some(CachedSpaceChunk {
chunk,
children,
join_rule: CachedJoinRule::Full(join_rule),
}),
);
/* TODO:
for child in response.children {
roomid_spacechunk_cache.insert(
current_room.clone(),
CachedSpaceChunk {
chunk: child.chunk,
children,
join_rule,
},
);
}
*/
} else {
self.roomid_spacechunk_cache
.lock()
.unwrap()
.insert(current_room.clone(), None);
}
}
}
Ok(get_hierarchy::v1::Response {
next_batch: if results.is_empty() {
None
} else {
Some((skip + results.len()).to_string())
},
rooms: results,
})
}
fn get_room_chunk(
&self,
sender_user: &UserId,
room_id: &RoomId,
children: Vec<Arc<PduEvent>>,
) -> Result<SpaceHierarchyRoomsChunk> {
Ok(SpaceHierarchyRoomsChunk {
canonical_alias: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCanonicalAlias, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomCanonicalAliasEventContent| c.alias)
.map_err(|_| {
Error::bad_database("Invalid canonical alias event in database.")
})
})?,
name: services().rooms.state_accessor.get_name(&room_id)?,
num_joined_members: services()
.rooms
.state_cache
.room_joined_count(&room_id)?
.unwrap_or_else(|| {
warn!("Room {} has no member count", room_id);
0
})
.try_into()
.expect("user count should not be that big"),
room_id: room_id.to_owned(),
topic: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomTopic, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomTopicEventContent| Some(c.topic))
.map_err(|_| {
error!("Invalid room topic event in database for room {}", room_id);
Error::bad_database("Invalid room topic event in database.")
})
})?,
world_readable: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| {
c.history_visibility == HistoryVisibility::WorldReadable
})
.map_err(|_| {
Error::bad_database(
"Invalid room history visibility event in database.",
)
})
})?,
guest_can_join: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomGuestAccess, "")?
.map_or(Ok(false), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomGuestAccessEventContent| {
c.guest_access == GuestAccess::CanJoin
})
.map_err(|_| {
Error::bad_database("Invalid room guest access event in database.")
})
})?,
avatar_url: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomAvatarEventContent| c.url)
.map_err(|_| Error::bad_database("Invalid room avatar event in database."))
})
.transpose()?
// url is now an Option<String> so we must flatten
.flatten(),
join_rule: {
let join_rule = services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomJoinRules, "")?
.map(|s| {
serde_json::from_str(s.content.get())
.map(|c: RoomJoinRulesEventContent| c.join_rule)
.map_err(|e| {
error!("Invalid room join rule event in database: {}", e);
Error::BadDatabase("Invalid room join rule event in database.")
})
})
.transpose()?
.unwrap_or(JoinRule::Invite);
if !self.handle_join_rule(&join_rule, sender_user, room_id)? {
debug!("User is not allowed to see room {room_id}");
// This error will be caught later
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"User is not allowed to see the room",
));
}
self.translate_joinrule(&join_rule)?
},
room_type: services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomCreate, "")?
.map(|s| {
serde_json::from_str::<RoomCreateEventContent>(s.content.get()).map_err(|e| {
error!("Invalid room create event in database: {}", e);
Error::BadDatabase("Invalid room create event in database.")
})
})
.transpose()?
.and_then(|e| e.room_type),
children_state: children
.into_iter()
.map(|pdu| pdu.to_stripped_spacechild_state_event())
.collect(),
})
}
fn translate_joinrule(&self, join_rule: &JoinRule) -> Result<SpaceRoomJoinRule> {
match join_rule {
JoinRule::Invite => Ok(SpaceRoomJoinRule::Invite),
JoinRule::Knock => Ok(SpaceRoomJoinRule::Knock),
JoinRule::Private => Ok(SpaceRoomJoinRule::Private),
JoinRule::Restricted(_) => Ok(SpaceRoomJoinRule::Restricted),
JoinRule::KnockRestricted(_) => Ok(SpaceRoomJoinRule::KnockRestricted),
JoinRule::Public => Ok(SpaceRoomJoinRule::Public),
_ => Err(Error::BadServerResponse("Unknown join rule")),
}
}
fn handle_simplified_join_rule(
&self,
join_rule: &SpaceRoomJoinRule,
sender_user: &UserId,
room_id: &RoomId,
) -> Result<bool> {
let allowed = match join_rule {
SpaceRoomJoinRule::Public => true,
SpaceRoomJoinRule::Knock => true,
SpaceRoomJoinRule::Invite => services()
.rooms
.state_cache
.is_joined(sender_user, &room_id)?,
_ => false,
};
Ok(allowed)
}
fn handle_join_rule(
&self,
join_rule: &JoinRule,
sender_user: &UserId,
room_id: &RoomId,
) -> Result<bool> {
if self.handle_simplified_join_rule(
&self.translate_joinrule(join_rule)?,
sender_user,
room_id,
)? {
return Ok(true);
}
match join_rule {
JoinRule::Restricted(r) => {
for rule in &r.allow {
match rule {
join_rules::AllowRule::RoomMembership(rm) => {
if let Ok(true) = services()
.rooms
.state_cache
.is_joined(sender_user, &rm.room_id)
{
return Ok(true);
}
}
_ => {}
}
}
Ok(false)
}
JoinRule::KnockRestricted(_) => {
// TODO: Check rules
Ok(false)
}
_ => Ok(false),
}
}
}

View File

@ -8,7 +8,7 @@ pub use data::Data;
use ruma::{
events::{
room::{create::RoomCreateEventContent, member::MembershipState},
AnyStrippedStateEvent, RoomEventType, StateEventType,
AnyStrippedStateEvent, StateEventType, TimelineEventType,
},
serde::Raw,
state_res::{self, StateMap},
@ -32,11 +32,11 @@ impl Service {
&self,
room_id: &RoomId,
shortstatehash: u64,
statediffnew: HashSet<CompressedStateEvent>,
_statediffremoved: HashSet<CompressedStateEvent>,
statediffnew: Arc<HashSet<CompressedStateEvent>>,
_statediffremoved: Arc<HashSet<CompressedStateEvent>>,
state_lock: &MutexGuard<'_, ()>, // Take mutex guard to make sure users get the room state mutex
) -> Result<()> {
for event_id in statediffnew.into_iter().filter_map(|new| {
for event_id in statediffnew.iter().filter_map(|new| {
services()
.rooms
.state_compressor
@ -49,10 +49,6 @@ impl Service {
None => continue,
};
if pdu.get("type").and_then(|val| val.as_str()) != Some("m.room.member") {
continue;
}
let pdu: PduEvent = match serde_json::from_str(
&serde_json::to_string(&pdu).expect("CanonicalJsonObj can be serialized to JSON"),
) {
@ -60,34 +56,49 @@ impl Service {
Err(_) => continue,
};
#[derive(Deserialize)]
struct ExtractMembership {
membership: MembershipState,
match pdu.kind {
TimelineEventType::RoomMember => {
#[derive(Deserialize)]
struct ExtractMembership {
membership: MembershipState,
}
let membership =
match serde_json::from_str::<ExtractMembership>(pdu.content.get()) {
Ok(e) => e.membership,
Err(_) => continue,
};
let state_key = match pdu.state_key {
Some(k) => k,
None => continue,
};
let user_id = match UserId::parse(state_key) {
Ok(id) => id,
Err(_) => continue,
};
services().rooms.state_cache.update_membership(
room_id,
&user_id,
membership,
&pdu.sender,
None,
false,
)?;
}
TimelineEventType::SpaceChild => {
services()
.rooms
.spaces
.roomid_spacechunk_cache
.lock()
.unwrap()
.remove(&pdu.room_id);
}
_ => continue,
}
let membership = match serde_json::from_str::<ExtractMembership>(pdu.content.get()) {
Ok(e) => e.membership,
Err(_) => continue,
};
let state_key = match pdu.state_key {
Some(k) => k,
None => continue,
};
let user_id = match UserId::parse(state_key) {
Ok(id) => id,
Err(_) => continue,
};
services().rooms.state_cache.update_membership(
room_id,
&user_id,
membership,
&pdu.sender,
None,
false,
)?;
}
services().rooms.state_cache.update_joined_count(room_id)?;
@ -107,7 +118,7 @@ impl Service {
&self,
event_id: &EventId,
room_id: &RoomId,
state_ids_compressed: HashSet<CompressedStateEvent>,
state_ids_compressed: Arc<HashSet<CompressedStateEvent>>,
) -> Result<u64> {
let shorteventid = services()
.rooms
@ -152,9 +163,9 @@ impl Service {
.copied()
.collect();
(statediffnew, statediffremoved)
(Arc::new(statediffnew), Arc::new(statediffremoved))
} else {
(state_ids_compressed, HashSet::new())
(state_ids_compressed, Arc::new(HashSet::new()))
};
services().rooms.state_compressor.save_state_from_diff(
shortstatehash,
@ -234,8 +245,8 @@ impl Service {
services().rooms.state_compressor.save_state_from_diff(
shortstatehash,
statediffnew,
statediffremoved,
Arc::new(statediffnew),
Arc::new(statediffremoved),
2,
states_parents,
)?;
@ -331,7 +342,10 @@ impl Service {
.transpose()?;
let room_version = create_event_content
.map(|create_event| create_event.room_version)
.ok_or(Error::BadDatabase("Invalid room version"))?;
.ok_or_else(|| {
warn!("Invalid room version for room {room_id}");
Error::BadDatabase("Invalid room version")
})?;
Ok(room_version)
}
@ -358,7 +372,7 @@ impl Service {
pub fn get_auth_events(
&self,
room_id: &RoomId,
kind: &RoomEventType,
kind: &TimelineEventType,
sender: &UserId,
state_key: Option<&str>,
content: &serde_json::value::RawValue,
@ -396,7 +410,7 @@ impl Service {
.1;
Ok(full_state
.into_iter()
.iter()
.filter_map(|compressed| {
services()
.rooms

View File

@ -1,13 +1,31 @@
mod data;
use std::{collections::HashMap, sync::Arc};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
pub use data::Data;
use ruma::{events::StateEventType, EventId, RoomId};
use lru_cache::LruCache;
use ruma::{
events::{
room::{
avatar::RoomAvatarEventContent,
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
member::{MembershipState, RoomMemberEventContent},
name::RoomNameEventContent,
},
StateEventType,
},
EventId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId,
};
use tracing::error;
use crate::{PduEvent, Result};
use crate::{services, Error, PduEvent, Result};
pub struct Service {
pub db: &'static dyn Data,
pub server_visibility_cache: Mutex<LruCache<(OwnedServerName, u64), bool>>,
pub user_visibility_cache: Mutex<LruCache<(OwnedUserId, u64), bool>>,
}
impl Service {
@ -46,6 +64,178 @@ impl Service {
self.db.state_get(shortstatehash, event_type, state_key)
}
/// Get membership for given user in state
fn user_membership(&self, shortstatehash: u64, user_id: &UserId) -> Result<MembershipState> {
self.state_get(
shortstatehash,
&StateEventType::RoomMember,
user_id.as_str(),
)?
.map_or(Ok(MembershipState::Leave), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomMemberEventContent| c.membership)
.map_err(|_| Error::bad_database("Invalid room membership event in database."))
})
}
/// The user was a joined member at this state (potentially in the past)
fn user_was_joined(&self, shortstatehash: u64, user_id: &UserId) -> bool {
self.user_membership(shortstatehash, user_id)
.map(|s| s == MembershipState::Join)
.unwrap_or_default() // Return sensible default, i.e. false
}
/// The user was an invited or joined room member at this state (potentially
/// in the past)
fn user_was_invited(&self, shortstatehash: u64, user_id: &UserId) -> bool {
self.user_membership(shortstatehash, user_id)
.map(|s| s == MembershipState::Join || s == MembershipState::Invite)
.unwrap_or_default() // Return sensible default, i.e. false
}
/// Whether a server is allowed to see an event through federation, based on
/// the room's history_visibility at that event's state.
#[tracing::instrument(skip(self, origin, room_id, event_id))]
pub fn server_can_see_event(
&self,
origin: &ServerName,
room_id: &RoomId,
event_id: &EventId,
) -> Result<bool> {
let shortstatehash = match self.pdu_shortstatehash(event_id)? {
Some(shortstatehash) => shortstatehash,
None => return Ok(true),
};
if let Some(visibility) = self
.server_visibility_cache
.lock()
.unwrap()
.get_mut(&(origin.to_owned(), shortstatehash))
{
return Ok(*visibility);
}
let history_visibility = self
.state_get(shortstatehash, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(HistoryVisibility::Shared), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| c.history_visibility)
.map_err(|_| {
Error::bad_database("Invalid history visibility event in database.")
})
})?;
let mut current_server_members = services()
.rooms
.state_cache
.room_members(room_id)
.filter_map(|r| r.ok())
.filter(|member| member.server_name() == origin);
let visibility = match history_visibility {
HistoryVisibility::WorldReadable | HistoryVisibility::Shared => true,
HistoryVisibility::Invited => {
// Allow if any member on requesting server was AT LEAST invited, else deny
current_server_members.any(|member| self.user_was_invited(shortstatehash, &member))
}
HistoryVisibility::Joined => {
// Allow if any member on requested server was joined, else deny
current_server_members.any(|member| self.user_was_joined(shortstatehash, &member))
}
_ => {
error!("Unknown history visibility {history_visibility}");
false
}
};
self.server_visibility_cache
.lock()
.unwrap()
.insert((origin.to_owned(), shortstatehash), visibility);
Ok(visibility)
}
/// Whether a user is allowed to see an event, based on
/// the room's history_visibility at that event's state.
#[tracing::instrument(skip(self, user_id, room_id, event_id))]
pub fn user_can_see_event(
&self,
user_id: &UserId,
room_id: &RoomId,
event_id: &EventId,
) -> Result<bool> {
let shortstatehash = match self.pdu_shortstatehash(event_id)? {
Some(shortstatehash) => shortstatehash,
None => return Ok(true),
};
if let Some(visibility) = self
.user_visibility_cache
.lock()
.unwrap()
.get_mut(&(user_id.to_owned(), shortstatehash))
{
return Ok(*visibility);
}
let currently_member = services().rooms.state_cache.is_joined(&user_id, &room_id)?;
let history_visibility = self
.state_get(shortstatehash, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(HistoryVisibility::Shared), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| c.history_visibility)
.map_err(|_| {
Error::bad_database("Invalid history visibility event in database.")
})
})?;
let visibility = match history_visibility {
HistoryVisibility::WorldReadable => true,
HistoryVisibility::Shared => currently_member,
HistoryVisibility::Invited => {
// Allow if any member on requesting server was AT LEAST invited, else deny
self.user_was_invited(shortstatehash, &user_id)
}
HistoryVisibility::Joined => {
// Allow if any member on requested server was joined, else deny
self.user_was_joined(shortstatehash, &user_id)
}
_ => {
error!("Unknown history visibility {history_visibility}");
false
}
};
self.user_visibility_cache
.lock()
.unwrap()
.insert((user_id.to_owned(), shortstatehash), visibility);
Ok(visibility)
}
/// Whether a user is allowed to see an event, based on
/// the room's history_visibility at that event's state.
#[tracing::instrument(skip(self, user_id, room_id))]
pub fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId) -> Result<bool> {
let currently_member = services().rooms.state_cache.is_joined(&user_id, &room_id)?;
let history_visibility = self
.room_state_get(&room_id, &StateEventType::RoomHistoryVisibility, "")?
.map_or(Ok(HistoryVisibility::Shared), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomHistoryVisibilityEventContent| c.history_visibility)
.map_err(|_| {
Error::bad_database("Invalid history visibility event in database.")
})
})?;
Ok(currently_member || history_visibility == HistoryVisibility::WorldReadable)
}
/// Returns the state hash for this pdu.
pub fn pdu_shortstatehash(&self, event_id: &EventId) -> Result<Option<u64>> {
self.db.pdu_shortstatehash(event_id)
@ -81,4 +271,42 @@ impl Service {
) -> Result<Option<Arc<PduEvent>>> {
self.db.room_state_get(room_id, event_type, state_key)
}
pub fn get_name(&self, room_id: &RoomId) -> Result<Option<String>> {
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomName, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map(|c: RoomNameEventContent| c.name)
.map_err(|_| Error::bad_database("Invalid room name event in database."))
})
}
pub fn get_avatar(&self, room_id: &RoomId) -> Result<Option<RoomAvatarEventContent>> {
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomAvatar, "")?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map_err(|_| Error::bad_database("Invalid room avatar event in database."))
})
}
pub fn get_member(
&self,
room_id: &RoomId,
user_id: &UserId,
) -> Result<Option<RoomMemberEventContent>> {
services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomMember, user_id.as_str())?
.map_or(Ok(None), |s| {
serde_json::from_str(s.content.get())
.map_err(|_| Error::bad_database("Invalid room member event in database."))
})
}
}

View File

@ -14,6 +14,7 @@ use ruma::{
serde::Raw,
OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, UserId,
};
use tracing::warn;
use crate::{services, Error, Result};
@ -88,8 +89,9 @@ impl Service {
RoomAccountDataEventType::Tag,
)?
.map(|event| {
serde_json::from_str(event.get()).map_err(|_| {
Error::bad_database("Invalid account data event in db.")
serde_json::from_str(event.get()).map_err(|e| {
warn!("Invalid account data event in db: {e:?}");
Error::BadDatabase("Invalid account data event in db.")
})
})
{
@ -113,8 +115,9 @@ impl Service {
GlobalAccountDataEventType::Direct.to_string().into(),
)?
.map(|event| {
serde_json::from_str::<DirectEvent>(event.get()).map_err(|_| {
Error::bad_database("Invalid account data event in db.")
serde_json::from_str::<DirectEvent>(event.get()).map_err(|e| {
warn!("Invalid account data event in db: {e:?}");
Error::BadDatabase("Invalid account data event in db.")
})
})
{
@ -155,8 +158,10 @@ impl Service {
.into(),
)?
.map(|event| {
serde_json::from_str::<IgnoredUserListEvent>(event.get())
.map_err(|_| Error::bad_database("Invalid account data event in db."))
serde_json::from_str::<IgnoredUserListEvent>(event.get()).map_err(|e| {
warn!("Invalid account data event in db: {e:?}");
Error::BadDatabase("Invalid account data event in db.")
})
})
.transpose()?
.map_or(false, |ignored| {

View File

@ -1,12 +1,12 @@
use std::collections::HashSet;
use std::{collections::HashSet, sync::Arc};
use super::CompressedStateEvent;
use crate::Result;
pub struct StateDiff {
pub parent: Option<u64>,
pub added: HashSet<CompressedStateEvent>,
pub removed: HashSet<CompressedStateEvent>,
pub added: Arc<HashSet<CompressedStateEvent>>,
pub removed: Arc<HashSet<CompressedStateEvent>>,
}
pub trait Data: Send + Sync {

View File

@ -20,10 +20,10 @@ pub struct Service {
LruCache<
u64,
Vec<(
u64, // sstatehash
HashSet<CompressedStateEvent>, // full state
HashSet<CompressedStateEvent>, // added
HashSet<CompressedStateEvent>, // removed
u64, // sstatehash
Arc<HashSet<CompressedStateEvent>>, // full state
Arc<HashSet<CompressedStateEvent>>, // added
Arc<HashSet<CompressedStateEvent>>, // removed
)>,
>,
>,
@ -39,10 +39,10 @@ impl Service {
shortstatehash: u64,
) -> Result<
Vec<(
u64, // sstatehash
HashSet<CompressedStateEvent>, // full state
HashSet<CompressedStateEvent>, // added
HashSet<CompressedStateEvent>, // removed
u64, // sstatehash
Arc<HashSet<CompressedStateEvent>>, // full state
Arc<HashSet<CompressedStateEvent>>, // added
Arc<HashSet<CompressedStateEvent>>, // removed
)>,
> {
if let Some(r) = self
@ -62,13 +62,19 @@ impl Service {
if let Some(parent) = parent {
let mut response = self.load_shortstatehash_info(parent)?;
let mut state = response.last().unwrap().1.clone();
let mut state = (*response.last().unwrap().1).clone();
state.extend(added.iter().copied());
let removed = (*removed).clone();
for r in &removed {
state.remove(r);
}
response.push((shortstatehash, state, added, removed));
response.push((shortstatehash, Arc::new(state), added, Arc::new(removed)));
self.stateinfo_cache
.lock()
.unwrap()
.insert(shortstatehash, response.clone());
Ok(response)
} else {
@ -135,14 +141,14 @@ impl Service {
pub fn save_state_from_diff(
&self,
shortstatehash: u64,
statediffnew: HashSet<CompressedStateEvent>,
statediffremoved: HashSet<CompressedStateEvent>,
statediffnew: Arc<HashSet<CompressedStateEvent>>,
statediffremoved: Arc<HashSet<CompressedStateEvent>>,
diff_to_sibling: usize,
mut parent_states: Vec<(
u64, // sstatehash
HashSet<CompressedStateEvent>, // full state
HashSet<CompressedStateEvent>, // added
HashSet<CompressedStateEvent>, // removed
u64, // sstatehash
Arc<HashSet<CompressedStateEvent>>, // full state
Arc<HashSet<CompressedStateEvent>>, // added
Arc<HashSet<CompressedStateEvent>>, // removed
)>,
) -> Result<()> {
let diffsum = statediffnew.len() + statediffremoved.len();
@ -152,29 +158,29 @@ impl Service {
// To many layers, we have to go deeper
let parent = parent_states.pop().unwrap();
let mut parent_new = parent.2;
let mut parent_removed = parent.3;
let mut parent_new = (*parent.2).clone();
let mut parent_removed = (*parent.3).clone();
for removed in statediffremoved {
if !parent_new.remove(&removed) {
for removed in statediffremoved.iter() {
if !parent_new.remove(removed) {
// It was not added in the parent and we removed it
parent_removed.insert(removed);
parent_removed.insert(removed.clone());
}
// Else it was added in the parent and we removed it again. We can forget this change
}
for new in statediffnew {
if !parent_removed.remove(&new) {
for new in statediffnew.iter() {
if !parent_removed.remove(new) {
// It was not touched in the parent and we added it
parent_new.insert(new);
parent_new.insert(new.clone());
}
// Else it was removed in the parent and we added it again. We can forget this change
}
self.save_state_from_diff(
shortstatehash,
parent_new,
parent_removed,
Arc::new(parent_new),
Arc::new(parent_removed),
diffsum,
parent_states,
)?;
@ -205,29 +211,29 @@ impl Service {
if diffsum * diffsum >= 2 * diff_to_sibling * parent_diff {
// Diff too big, we replace above layer(s)
let mut parent_new = parent.2;
let mut parent_removed = parent.3;
let mut parent_new = (*parent.2).clone();
let mut parent_removed = (*parent.3).clone();
for removed in statediffremoved {
if !parent_new.remove(&removed) {
for removed in statediffremoved.iter() {
if !parent_new.remove(removed) {
// It was not added in the parent and we removed it
parent_removed.insert(removed);
parent_removed.insert(removed.clone());
}
// Else it was added in the parent and we removed it again. We can forget this change
}
for new in statediffnew {
if !parent_removed.remove(&new) {
for new in statediffnew.iter() {
if !parent_removed.remove(new) {
// It was not touched in the parent and we added it
parent_new.insert(new);
parent_new.insert(new.clone());
}
// Else it was removed in the parent and we added it again. We can forget this change
}
self.save_state_from_diff(
shortstatehash,
parent_new,
parent_removed,
Arc::new(parent_new),
Arc::new(parent_removed),
diffsum,
parent_states,
)?;
@ -250,11 +256,11 @@ impl Service {
pub fn save_state(
&self,
room_id: &RoomId,
new_state_ids_compressed: HashSet<CompressedStateEvent>,
new_state_ids_compressed: Arc<HashSet<CompressedStateEvent>>,
) -> Result<(
u64,
HashSet<CompressedStateEvent>,
HashSet<CompressedStateEvent>,
Arc<HashSet<CompressedStateEvent>>,
Arc<HashSet<CompressedStateEvent>>,
)> {
let previous_shortstatehash = services().rooms.state.get_room_shortstatehash(room_id)?;
@ -271,7 +277,11 @@ impl Service {
.get_or_create_shortstatehash(&state_hash)?;
if Some(new_shortstatehash) == previous_shortstatehash {
return Ok((new_shortstatehash, HashSet::new(), HashSet::new()));
return Ok((
new_shortstatehash,
Arc::new(HashSet::new()),
Arc::new(HashSet::new()),
));
}
let states_parents = previous_shortstatehash
@ -290,9 +300,9 @@ impl Service {
.copied()
.collect();
(statediffnew, statediffremoved)
(Arc::new(statediffnew), Arc::new(statediffremoved))
} else {
(new_state_ids_compressed, HashSet::new())
(new_state_ids_compressed, Arc::new(HashSet::new()))
};
if !already_existed {

View File

@ -0,0 +1,15 @@
use crate::{PduEvent, Result};
use ruma::{api::client::threads::get_threads::v1::IncludeThreads, OwnedUserId, RoomId, UserId};
pub trait Data: Send + Sync {
fn threads_until<'a>(
&'a self,
user_id: &'a UserId,
room_id: &'a RoomId,
until: u64,
include: &'a IncludeThreads,
) -> Result<Box<dyn Iterator<Item = Result<(u64, PduEvent)>> + 'a>>;
fn update_participants(&self, root_id: &[u8], participants: &[OwnedUserId]) -> Result<()>;
fn get_participants(&self, root_id: &[u8]) -> Result<Option<Vec<OwnedUserId>>>;
}

View File

@ -0,0 +1,116 @@
mod data;
pub use data::Data;
use ruma::{
api::client::{error::ErrorKind, threads::get_threads::v1::IncludeThreads},
events::relation::BundledThread,
uint, CanonicalJsonValue, EventId, RoomId, UserId,
};
use serde_json::json;
use crate::{services, Error, PduEvent, Result};
pub struct Service {
pub db: &'static dyn Data,
}
impl Service {
pub fn threads_until<'a>(
&'a self,
user_id: &'a UserId,
room_id: &'a RoomId,
until: u64,
include: &'a IncludeThreads,
) -> Result<impl Iterator<Item = Result<(u64, PduEvent)>> + 'a> {
self.db.threads_until(user_id, room_id, until, include)
}
pub fn add_to_thread<'a>(&'a self, root_event_id: &EventId, pdu: &PduEvent) -> Result<()> {
let root_id = &services()
.rooms
.timeline
.get_pdu_id(root_event_id)?
.ok_or_else(|| {
Error::BadRequest(
ErrorKind::InvalidParam,
"Invalid event id in thread message",
)
})?;
let root_pdu = services()
.rooms
.timeline
.get_pdu_from_id(root_id)?
.ok_or_else(|| {
Error::BadRequest(ErrorKind::InvalidParam, "Thread root pdu not found")
})?;
let mut root_pdu_json = services()
.rooms
.timeline
.get_pdu_json_from_id(root_id)?
.ok_or_else(|| {
Error::BadRequest(ErrorKind::InvalidParam, "Thread root pdu not found")
})?;
if let CanonicalJsonValue::Object(unsigned) = root_pdu_json
.entry("unsigned".to_owned())
.or_insert_with(|| CanonicalJsonValue::Object(Default::default()))
{
if let Some(mut relations) = unsigned
.get("m.relations")
.and_then(|r| r.as_object())
.and_then(|r| r.get("m.thread"))
.and_then(|relations| {
serde_json::from_value::<BundledThread>(relations.clone().into()).ok()
})
{
// Thread already existed
relations.count += uint!(1);
relations.latest_event = pdu.to_message_like_event();
let content = serde_json::to_value(relations).expect("to_value always works");
unsigned.insert(
"m.relations".to_owned(),
json!({ "m.thread": content })
.try_into()
.expect("thread is valid json"),
);
} else {
// New thread
let relations = BundledThread {
latest_event: pdu.to_message_like_event(),
count: uint!(1),
current_user_participated: true,
};
let content = serde_json::to_value(relations).expect("to_value always works");
unsigned.insert(
"m.relations".to_owned(),
json!({ "m.thread": content })
.try_into()
.expect("thread is valid json"),
);
}
services()
.rooms
.timeline
.replace_pdu(root_id, &root_pdu_json, &root_pdu)?;
}
let mut users = Vec::new();
if let Some(userids) = self.db.get_participants(&root_id)? {
users.extend_from_slice(&userids);
users.push(pdu.sender.clone());
} else {
users.push(root_pdu.sender);
users.push(pdu.sender.clone());
}
self.db.update_participants(root_id, &users)
}
}

View File

@ -4,12 +4,13 @@ use ruma::{CanonicalJsonObject, EventId, OwnedUserId, RoomId, UserId};
use crate::{PduEvent, Result};
use super::PduCount;
pub trait Data: Send + Sync {
fn first_pdu_in_room(&self, room_id: &RoomId) -> Result<Option<Arc<PduEvent>>>;
fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<u64>;
fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<PduCount>;
/// Returns the `count` of this pdu's id.
fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<u64>>;
fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<PduCount>>;
/// Returns the json of a pdu.
fn get_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>>;
@ -38,9 +39,6 @@ pub trait Data: Send + Sync {
/// Returns the pdu as a `BTreeMap<String, CanonicalJsonValue>`.
fn get_pdu_json_from_id(&self, pdu_id: &[u8]) -> Result<Option<CanonicalJsonObject>>;
/// Returns the `count` of this pdu's id.
fn pdu_count(&self, pdu_id: &[u8]) -> Result<u64>;
/// Adds a new pdu to the timeline
fn append_pdu(
&self,
@ -50,17 +48,21 @@ pub trait Data: Send + Sync {
count: u64,
) -> Result<()>;
/// Removes a pdu and creates a new one with the same id.
fn replace_pdu(&self, pdu_id: &[u8], pdu: &PduEvent) -> Result<()>;
// Adds a new pdu to the backfilled timeline
fn prepend_backfill_pdu(
&self,
pdu_id: &[u8],
event_id: &EventId,
json: &CanonicalJsonObject,
) -> Result<()>;
/// Returns an iterator over all events in a room that happened after the event with id `since`
/// in chronological order.
fn pdus_since<'a>(
&'a self,
user_id: &UserId,
room_id: &RoomId,
since: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>>;
/// Removes a pdu and creates a new one with the same id.
fn replace_pdu(
&self,
pdu_id: &[u8],
pdu_json: &CanonicalJsonObject,
pdu: &PduEvent,
) -> Result<()>;
/// Returns an iterator over all events and their tokens in a room that happened before the
/// event with id `until` in reverse-chronological order.
@ -68,15 +70,17 @@ pub trait Data: Send + Sync {
&'a self,
user_id: &UserId,
room_id: &RoomId,
until: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>>;
until: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>>;
/// Returns an iterator over all events in a room that happened after the event with id `from`
/// in chronological order.
fn pdus_after<'a>(
&'a self,
user_id: &UserId,
room_id: &RoomId,
from: u64,
) -> Result<Box<dyn Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a>>;
from: PduCount,
) -> Result<Box<dyn Iterator<Item = Result<(PduCount, PduEvent)>> + 'a>>;
fn increment_notification_counts(
&self,

View File

@ -1,60 +1,133 @@
mod data;
use std::collections::HashMap;
use std::{
cmp::Ordering,
collections::{BTreeMap, HashMap},
};
use std::{
collections::HashSet,
sync::{Arc, Mutex},
sync::{Arc, Mutex, RwLock},
};
pub use data::Data;
use regex::Regex;
use ruma::{
api::client::error::ErrorKind,
api::{client::error::ErrorKind, federation},
canonical_json::to_canonical_value,
events::{
push_rules::PushRulesEvent,
room::{
create::RoomCreateEventContent, member::MembershipState,
create::RoomCreateEventContent, encrypted::Relation, member::MembershipState,
power_levels::RoomPowerLevelsEventContent,
},
GlobalAccountDataEventType, RoomEventType, StateEventType,
GlobalAccountDataEventType, StateEventType, TimelineEventType,
},
push::{Action, Ruleset, Tweak},
serde::Base64,
state_res,
state_res::RoomVersion,
uint, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedServerName, RoomAliasId, RoomId, UserId,
state_res::{Event, RoomVersion},
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
OwnedServerName, RoomAliasId, RoomId, ServerName, UserId,
};
use serde::Deserialize;
use serde_json::value::to_raw_value;
use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use tokio::sync::MutexGuard;
use tracing::{error, warn};
use tracing::{error, info, warn};
use crate::{
api::server_server,
service::pdu::{EventHash, PduBuilder},
services, utils, Error, PduEvent, Result,
};
use super::state_compressor::CompressedStateEvent;
#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
pub enum PduCount {
Backfilled(u64),
Normal(u64),
}
impl PduCount {
pub fn min() -> Self {
Self::Backfilled(u64::MAX)
}
pub fn max() -> Self {
Self::Normal(u64::MAX)
}
pub fn try_from_string(token: &str) -> Result<Self> {
if token.starts_with('-') {
token[1..].parse().map(PduCount::Backfilled)
} else {
token.parse().map(PduCount::Normal)
}
.map_err(|_| Error::BadRequest(ErrorKind::InvalidParam, "Invalid pagination token."))
}
pub fn stringify(&self) -> String {
match self {
PduCount::Backfilled(x) => format!("-{x}"),
PduCount::Normal(x) => x.to_string(),
}
}
}
impl PartialOrd for PduCount {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PduCount {
fn cmp(&self, other: &Self) -> Ordering {
match (self, other) {
(PduCount::Normal(s), PduCount::Normal(o)) => s.cmp(o),
(PduCount::Backfilled(s), PduCount::Backfilled(o)) => o.cmp(s),
(PduCount::Normal(_), PduCount::Backfilled(_)) => Ordering::Greater,
(PduCount::Backfilled(_), PduCount::Normal(_)) => Ordering::Less,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comparisons() {
assert!(PduCount::Normal(1) < PduCount::Normal(2));
assert!(PduCount::Backfilled(2) < PduCount::Backfilled(1));
assert!(PduCount::Normal(1) > PduCount::Backfilled(1));
assert!(PduCount::Backfilled(1) < PduCount::Normal(1));
}
}
pub struct Service {
pub db: &'static dyn Data,
pub lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, u64>>,
pub lasttimelinecount_cache: Mutex<HashMap<OwnedRoomId, PduCount>>,
}
impl Service {
#[tracing::instrument(skip(self))]
pub fn first_pdu_in_room(&self, room_id: &RoomId) -> Result<Option<Arc<PduEvent>>> {
self.db.first_pdu_in_room(room_id)
self.all_pdus(&user_id!("@doesntmatter:conduit.rs"), &room_id)?
.next()
.map(|o| o.map(|(_, p)| Arc::new(p)))
.transpose()
}
#[tracing::instrument(skip(self))]
pub fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<u64> {
pub fn last_timeline_count(&self, sender_user: &UserId, room_id: &RoomId) -> Result<PduCount> {
self.db.last_timeline_count(sender_user, room_id)
}
/// Returns the `count` of this pdu's id.
pub fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<PduCount>> {
self.db.get_pdu_count(event_id)
}
// TODO Is this the same as the function above?
/*
#[tracing::instrument(skip(self))]
@ -78,11 +151,6 @@ impl Service {
}
*/
/// Returns the `count` of this pdu's id.
pub fn get_pdu_count(&self, event_id: &EventId) -> Result<Option<u64>> {
self.db.get_pdu_count(event_id)
}
/// Returns the json of a pdu.
pub fn get_pdu_json(&self, event_id: &EventId) -> Result<Option<CanonicalJsonObject>> {
self.db.get_pdu_json(event_id)
@ -127,15 +195,15 @@ impl Service {
self.db.get_pdu_json_from_id(pdu_id)
}
/// Returns the `count` of this pdu's id.
pub fn pdu_count(&self, pdu_id: &[u8]) -> Result<u64> {
self.db.pdu_count(pdu_id)
}
/// Removes a pdu and creates a new one with the same id.
#[tracing::instrument(skip(self))]
fn replace_pdu(&self, pdu_id: &[u8], pdu: &PduEvent) -> Result<()> {
self.db.replace_pdu(pdu_id, pdu)
pub fn replace_pdu(
&self,
pdu_id: &[u8],
pdu_json: &CanonicalJsonObject,
pdu: &PduEvent,
) -> Result<()> {
self.db.replace_pdu(pdu_id, pdu_json, pdu)
}
/// Creates a new persisted data unit and adds it to a room.
@ -289,9 +357,7 @@ impl Service {
&pdu.room_id,
)? {
match action {
Action::DontNotify => notify = false,
// TODO: Implement proper support for coalesce
Action::Notify | Action::Coalesce => notify = true,
Action::Notify => notify = true,
Action::SetTweak(Tweak::Highlight(true)) => {
highlight = true;
}
@ -316,12 +382,23 @@ impl Service {
.increment_notification_counts(&pdu.room_id, notifies, highlights)?;
match pdu.kind {
RoomEventType::RoomRedaction => {
TimelineEventType::RoomRedaction => {
if let Some(redact_id) = &pdu.redacts {
self.redact_pdu(redact_id, pdu)?;
}
}
RoomEventType::RoomMember => {
TimelineEventType::SpaceChild => {
if let Some(_state_key) = &pdu.state_key {
services()
.rooms
.spaces
.roomid_spacechunk_cache
.lock()
.unwrap()
.remove(&pdu.room_id);
}
}
TimelineEventType::RoomMember => {
if let Some(state_key) = &pdu.state_key {
#[derive(Deserialize)]
struct ExtractMembership {
@ -355,7 +432,7 @@ impl Service {
)?;
}
}
RoomEventType::RoomMessage => {
TimelineEventType::RoomMessage => {
#[derive(Deserialize)]
struct ExtractBody {
body: Option<String>,
@ -378,7 +455,10 @@ impl Service {
)?;
let server_user = format!("@conduit:{}", services().globals.server_name());
let to_conduit = body.starts_with(&format!("{server_user}: "));
let to_conduit = body.starts_with(&format!("{server_user}: "))
|| body.starts_with(&format!("{server_user} "))
|| body == format!("{server_user}:")
|| body == format!("{server_user}");
// This will evaluate to false if the emergency password is set up so that
// the administrator can execute commands as conduit
@ -393,6 +473,62 @@ impl Service {
_ => {}
}
// Update Relationships
#[derive(Deserialize)]
struct ExtractRelatesTo {
#[serde(rename = "m.relates_to")]
relates_to: Relation,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractEventId {
event_id: OwnedEventId,
}
#[derive(Clone, Debug, Deserialize)]
struct ExtractRelatesToEventId {
#[serde(rename = "m.relates_to")]
relates_to: ExtractEventId,
}
if let Ok(content) = serde_json::from_str::<ExtractRelatesToEventId>(pdu.content.get()) {
if let Some(related_pducount) = services()
.rooms
.timeline
.get_pdu_count(&content.relates_to.event_id)?
{
services()
.rooms
.pdu_metadata
.add_relation(PduCount::Normal(count2), related_pducount)?;
}
}
if let Ok(content) = serde_json::from_str::<ExtractRelatesTo>(pdu.content.get()) {
match content.relates_to {
Relation::Reply { in_reply_to } => {
// We need to do it again here, because replies don't have
// event_id as a top level field
if let Some(related_pducount) = services()
.rooms
.timeline
.get_pdu_count(&in_reply_to.event_id)?
{
services()
.rooms
.pdu_metadata
.add_relation(PduCount::Normal(count2), related_pducount)?;
}
}
Relation::Thread(thread) => {
services()
.rooms
.threads
.add_to_thread(&thread.event_id, pdu)?;
}
_ => {} // TODO: Aggregate other types
}
}
for appservice in services().appservice.all()? {
if services()
.rooms
@ -407,7 +543,7 @@ impl Service {
// If the RoomMember event has a non-empty state_key, it is targeted at someone.
// If it is our appservice user, we send this PDU to it.
if pdu.kind == RoomEventType::RoomMember {
if pdu.kind == TimelineEventType::RoomMember {
if let Some(state_key_uid) = &pdu
.state_key
.as_ref()
@ -457,7 +593,7 @@ impl Service {
let matching_users = |users: &Regex| {
users.is_match(pdu.sender.as_str())
|| pdu.kind == RoomEventType::RoomMember
|| pdu.kind == TimelineEventType::RoomMember
&& pdu
.state_key
.as_ref()
@ -683,6 +819,92 @@ impl Service {
let (pdu, pdu_json) =
self.create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock)?;
let admin_room = services().rooms.alias.resolve_local_alias(
<&RoomAliasId>::try_from(
format!("#admins:{}", services().globals.server_name()).as_str(),
)
.expect("#admins:server_name is a valid room alias"),
)?;
if admin_room.filter(|v| v == room_id).is_some() {
match pdu.event_type() {
TimelineEventType::RoomEncryption => {
warn!("Encryption is not allowed in the admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Encryption is not allowed in the admins room.",
));
}
TimelineEventType::RoomMember => {
#[derive(Deserialize)]
struct ExtractMembership {
membership: MembershipState,
}
let target = pdu
.state_key()
.filter(|v| v.starts_with("@"))
.unwrap_or(sender.as_str());
let server_name = services().globals.server_name();
let server_user = format!("@conduit:{}", server_name);
let content = serde_json::from_str::<ExtractMembership>(pdu.content.get())
.map_err(|_| Error::bad_database("Invalid content in pdu."))?;
if content.membership == MembershipState::Leave {
if target == &server_user {
warn!("Conduit user cannot leave from admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Conduit user cannot leave from admins room.",
));
}
let count = services()
.rooms
.state_cache
.room_members(room_id)
.filter_map(|m| m.ok())
.filter(|m| m.server_name() == server_name)
.filter(|m| m != target)
.count();
if count < 2 {
warn!("Last admin cannot leave from admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Last admin cannot leave from admins room.",
));
}
}
if content.membership == MembershipState::Ban && pdu.state_key().is_some() {
if target == &server_user {
warn!("Conduit user cannot be banned in admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Conduit user cannot be banned in admins room.",
));
}
let count = services()
.rooms
.state_cache
.room_members(room_id)
.filter_map(|m| m.ok())
.filter(|m| m.server_name() == server_name)
.filter(|m| m != target)
.count();
if count < 2 {
warn!("Last admin cannot be banned in admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Last admin cannot be banned in admins room.",
));
}
}
}
_ => {}
}
}
// We append to state before appending the pdu, so we don't have a moment in time with the
// pdu without it's state. This is okay because append_pdu can't fail.
let statehashid = services().rooms.state.append_to_state(&pdu)?;
@ -711,7 +933,7 @@ impl Service {
.collect();
// In case we are kicking or banning a user, we need to inform their server of the change
if pdu.kind == RoomEventType::RoomMember {
if pdu.kind == TimelineEventType::RoomMember {
if let Some(state_key_uid) = &pdu
.state_key
.as_ref()
@ -737,7 +959,7 @@ impl Service {
pdu: &PduEvent,
pdu_json: CanonicalJsonObject,
new_room_leaves: Vec<OwnedEventId>,
state_ids_compressed: HashSet<CompressedStateEvent>,
state_ids_compressed: Arc<HashSet<CompressedStateEvent>>,
soft_fail: bool,
state_lock: &MutexGuard<'_, ()>, // Take mutex guard to make sure users get the room state mutex
) -> Result<Option<Vec<u8>>> {
@ -776,19 +998,8 @@ impl Service {
&'a self,
user_id: &UserId,
room_id: &RoomId,
) -> Result<impl Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a> {
self.pdus_since(user_id, room_id, 0)
}
/// Returns an iterator over all events in a room that happened after the event with id `since`
/// in chronological order.
pub fn pdus_since<'a>(
&'a self,
user_id: &UserId,
room_id: &RoomId,
since: u64,
) -> Result<impl Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a> {
self.db.pdus_since(user_id, room_id, since)
) -> Result<impl Iterator<Item = Result<(PduCount, PduEvent)>> + 'a> {
self.pdus_after(user_id, room_id, PduCount::min())
}
/// Returns an iterator over all events and their tokens in a room that happened before the
@ -798,8 +1009,8 @@ impl Service {
&'a self,
user_id: &UserId,
room_id: &RoomId,
until: u64,
) -> Result<impl Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a> {
until: PduCount,
) -> Result<impl Iterator<Item = Result<(PduCount, PduEvent)>> + 'a> {
self.db.pdus_until(user_id, room_id, until)
}
@ -810,22 +1021,182 @@ impl Service {
&'a self,
user_id: &UserId,
room_id: &RoomId,
from: u64,
) -> Result<impl Iterator<Item = Result<(Vec<u8>, PduEvent)>> + 'a> {
from: PduCount,
) -> Result<impl Iterator<Item = Result<(PduCount, PduEvent)>> + 'a> {
self.db.pdus_after(user_id, room_id, from)
}
/// Replace a PDU with the redacted form.
#[tracing::instrument(skip(self, reason))]
pub fn redact_pdu(&self, event_id: &EventId, reason: &PduEvent) -> Result<()> {
// TODO: Don't reserialize, keep original json
if let Some(pdu_id) = self.get_pdu_id(event_id)? {
let mut pdu = self
.get_pdu_from_id(&pdu_id)?
.ok_or_else(|| Error::bad_database("PDU ID points to invalid PDU."))?;
pdu.redact(reason)?;
self.replace_pdu(&pdu_id, &pdu)?;
self.replace_pdu(
&pdu_id,
&utils::to_canonical_object(&pdu).expect("PDU is an object"),
&pdu,
)?;
}
// If event does not exist, just noop
Ok(())
}
#[tracing::instrument(skip(self, room_id))]
pub async fn backfill_if_required(&self, room_id: &RoomId, from: PduCount) -> Result<()> {
let first_pdu = self
.all_pdus(&user_id!("@doesntmatter:conduit.rs"), &room_id)?
.next()
.expect("Room is not empty")?;
if first_pdu.0 < from {
// No backfill required, there are still events between them
return Ok(());
}
let power_levels: RoomPowerLevelsEventContent = services()
.rooms
.state_accessor
.room_state_get(&room_id, &StateEventType::RoomPowerLevels, "")?
.map(|ev| {
serde_json::from_str(ev.content.get())
.map_err(|_| Error::bad_database("invalid m.room.power_levels event"))
})
.transpose()?
.unwrap_or_default();
let mut admin_servers = power_levels
.users
.iter()
.filter(|(_, level)| **level > power_levels.users_default)
.map(|(user_id, _)| user_id.server_name())
.collect::<HashSet<_>>();
admin_servers.remove(services().globals.server_name());
// Request backfill
for backfill_server in admin_servers {
info!("Asking {backfill_server} for backfill");
let response = services()
.sending
.send_federation_request(
backfill_server,
federation::backfill::get_backfill::v1::Request {
room_id: room_id.to_owned(),
v: vec![first_pdu.1.event_id.as_ref().to_owned()],
limit: uint!(100),
},
)
.await;
match response {
Ok(response) => {
let mut pub_key_map = RwLock::new(BTreeMap::new());
for pdu in response.pdus {
if let Err(e) = self
.backfill_pdu(backfill_server, pdu, &mut pub_key_map)
.await
{
warn!("Failed to add backfilled pdu: {e}");
}
}
return Ok(());
}
Err(e) => {
warn!("{backfill_server} could not provide backfill: {e}");
}
}
}
info!("No servers could backfill");
Ok(())
}
#[tracing::instrument(skip(self, pdu))]
pub async fn backfill_pdu(
&self,
origin: &ServerName,
pdu: Box<RawJsonValue>,
pub_key_map: &RwLock<BTreeMap<String, BTreeMap<String, Base64>>>,
) -> Result<()> {
let (event_id, value, room_id) = server_server::parse_incoming_pdu(&pdu)?;
// Lock so we cannot backfill the same pdu twice at the same time
let mutex = Arc::clone(
services()
.globals
.roomid_mutex_federation
.write()
.unwrap()
.entry(room_id.to_owned())
.or_default(),
);
let mutex_lock = mutex.lock().await;
// Skip the PDU if we already have it as a timeline event
if let Some(pdu_id) = services().rooms.timeline.get_pdu_id(&event_id)? {
info!("We already know {event_id} at {pdu_id:?}");
return Ok(());
}
services()
.rooms
.event_handler
.handle_incoming_pdu(origin, &event_id, &room_id, value, false, &pub_key_map)
.await?;
let value = self.get_pdu_json(&event_id)?.expect("We just created it");
let pdu = self.get_pdu(&event_id)?.expect("We just created it");
let shortroomid = services()
.rooms
.short
.get_shortroomid(&room_id)?
.expect("room exists");
let mutex_insert = Arc::clone(
services()
.globals
.roomid_mutex_insert
.write()
.unwrap()
.entry(room_id.clone())
.or_default(),
);
let insert_lock = mutex_insert.lock().unwrap();
let count = services().globals.next_count()?;
let mut pdu_id = shortroomid.to_be_bytes().to_vec();
pdu_id.extend_from_slice(&0_u64.to_be_bytes());
pdu_id.extend_from_slice(&(u64::MAX - count).to_be_bytes());
// Insert pdu
self.db.prepend_backfill_pdu(&pdu_id, &event_id, &value)?;
drop(insert_lock);
match pdu.kind {
TimelineEventType::RoomMessage => {
#[derive(Deserialize)]
struct ExtractBody {
body: Option<String>,
}
let content = serde_json::from_str::<ExtractBody>(pdu.content.get())
.map_err(|_| Error::bad_database("Invalid content in pdu."))?;
if let Some(body) = content.body {
services()
.rooms
.search
.index_pdu(shortroomid, &pdu_id, &body)?;
}
}
_ => {}
}
drop(mutex_lock);
info!("Prepended backfill pdu");
Ok(())
}
}

View File

@ -18,6 +18,8 @@ use crate::{
use federation::transactions::send_transaction_message;
use futures_util::{stream::FuturesUnordered, StreamExt};
use base64::{engine::general_purpose, Engine as _};
use ruma::{
api::{
appservice,
@ -40,7 +42,7 @@ use tokio::{
select,
sync::{mpsc, Mutex, Semaphore},
};
use tracing::{error, warn};
use tracing::{debug, error, warn};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum OutgoingKind {
@ -497,17 +499,14 @@ impl Service {
})?,
appservice::event::push_events::v1::Request {
events: pdu_jsons,
txn_id: (&*base64::encode_config(
calculate_hash(
&events
.iter()
.map(|e| match e {
SendingEventType::Edu(b) | SendingEventType::Pdu(b) => &**b,
})
.collect::<Vec<_>>(),
),
base64::URL_SAFE_NO_PAD,
))
txn_id: (&*general_purpose::URL_SAFE_NO_PAD.encode(calculate_hash(
&events
.iter()
.map(|e| match e {
SendingEventType::Edu(b) | SendingEventType::Pdu(b) => &**b,
})
.collect::<Vec<_>>(),
)))
.into(),
},
)
@ -642,7 +641,7 @@ impl Service {
pdus: pdu_jsons,
edus: edu_jsons,
origin_server_ts: MilliSecondsSinceUnixEpoch::now(),
transaction_id: (&*base64::encode_config(
transaction_id: (&*general_purpose::URL_SAFE_NO_PAD.encode(
calculate_hash(
&events
.iter()
@ -651,7 +650,6 @@ impl Service {
})
.collect::<Vec<_>>(),
),
base64::URL_SAFE_NO_PAD,
))
.into(),
},
@ -683,8 +681,18 @@ impl Service {
where
T: Debug,
{
debug!("Waiting for permit");
let permit = self.maximum_requests.acquire().await;
let response = server_server::send_request(destination, request).await;
debug!("Got permit");
let response = tokio::time::timeout(
Duration::from_secs(2 * 60),
server_server::send_request(destination, request),
)
.await
.map_err(|_| {
warn!("Timeout waiting for server response of {destination}");
Error::BadServerResponse("Timeout waiting for server response")
})?;
drop(permit);
response

View File

@ -96,6 +96,17 @@ impl Service {
// Password was correct! Let's add it to `completed`
uiaainfo.completed.push(AuthType::Password);
}
AuthData::RegistrationToken(t) => {
if Some(t.token.trim()) == services().globals.config.registration_token.as_deref() {
uiaainfo.completed.push(AuthType::RegistrationToken);
} else {
uiaainfo.auth_error = Some(ruma::api::client::error::StandardErrorBody {
kind: ErrorKind::Forbidden,
message: "Invalid registration token.".to_owned(),
});
return Ok((false, uiaainfo));
}
}
AuthData::Dummy(_) => {
uiaainfo.completed.push(AuthType::Dummy);
}

View File

@ -111,6 +111,7 @@ pub trait Data: Send + Sync {
master_key: &Raw<CrossSigningKey>,
self_signing_key: &Option<Raw<CrossSigningKey>>,
user_signing_key: &Option<Raw<CrossSigningKey>>,
notify: bool,
) -> Result<()>;
fn sign_key(
@ -136,14 +137,30 @@ pub trait Data: Send + Sync {
device_id: &DeviceId,
) -> Result<Option<Raw<DeviceKeys>>>;
fn parse_master_key(
&self,
user_id: &UserId,
master_key: &Raw<CrossSigningKey>,
) -> Result<(Vec<u8>, CrossSigningKey)>;
fn get_key(
&self,
key: &[u8],
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>>;
fn get_master_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>>;
fn get_self_signing_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>>;

View File

@ -1,20 +1,41 @@
mod data;
use std::{collections::BTreeMap, mem};
use std::{
collections::BTreeMap,
mem,
sync::{Arc, Mutex},
};
pub use data::Data;
use ruma::{
api::client::{device::Device, error::ErrorKind, filter::FilterDefinition},
api::client::{
device::Device,
error::ErrorKind,
filter::FilterDefinition,
sync::sync_events::{
self,
v4::{ExtensionsConfig, SyncRequestList},
},
},
encryption::{CrossSigningKey, DeviceKeys, OneTimeKey},
events::AnyToDeviceEvent,
serde::Raw,
DeviceId, DeviceKeyAlgorithm, DeviceKeyId, OwnedDeviceId, OwnedDeviceKeyId, OwnedMxcUri,
OwnedUserId, RoomAliasId, UInt, UserId,
OwnedRoomId, OwnedUserId, RoomAliasId, UInt, UserId,
};
use crate::{services, Error, Result};
pub struct SlidingSyncCache {
lists: BTreeMap<String, SyncRequestList>,
subscriptions: BTreeMap<OwnedRoomId, sync_events::v4::RoomSubscription>,
known_rooms: BTreeMap<String, BTreeMap<OwnedRoomId, bool>>,
extensions: ExtensionsConfig,
}
pub struct Service {
pub db: &'static dyn Data,
pub connections:
Mutex<BTreeMap<(OwnedUserId, OwnedDeviceId, String), Arc<Mutex<SlidingSyncCache>>>>,
}
impl Service {
@ -23,6 +44,193 @@ impl Service {
self.db.exists(user_id)
}
pub fn forget_sync_request_connection(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
conn_id: String,
) {
self.connections
.lock()
.unwrap()
.remove(&(user_id, device_id, conn_id));
}
pub fn update_sync_request_with_cache(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
request: &mut sync_events::v4::Request,
) -> BTreeMap<String, BTreeMap<OwnedRoomId, bool>> {
let Some(conn_id) = request.conn_id.clone() else {
return BTreeMap::new();
};
let mut cache = self.connections.lock().unwrap();
let cached = Arc::clone(
cache
.entry((user_id, device_id, conn_id))
.or_insert_with(|| {
Arc::new(Mutex::new(SlidingSyncCache {
lists: BTreeMap::new(),
subscriptions: BTreeMap::new(),
known_rooms: BTreeMap::new(),
extensions: ExtensionsConfig::default(),
}))
}),
);
let cached = &mut cached.lock().unwrap();
drop(cache);
for (list_id, list) in &mut request.lists {
if let Some(cached_list) = cached.lists.get(list_id) {
if list.sort.is_empty() {
list.sort = cached_list.sort.clone();
};
if list.room_details.required_state.is_empty() {
list.room_details.required_state =
cached_list.room_details.required_state.clone();
};
list.room_details.timeline_limit = list
.room_details
.timeline_limit
.or(cached_list.room_details.timeline_limit);
list.include_old_rooms = list
.include_old_rooms
.clone()
.or(cached_list.include_old_rooms.clone());
match (&mut list.filters, cached_list.filters.clone()) {
(Some(list_filters), Some(cached_filters)) => {
list_filters.is_dm = list_filters.is_dm.or(cached_filters.is_dm);
if list_filters.spaces.is_empty() {
list_filters.spaces = cached_filters.spaces;
}
list_filters.is_encrypted =
list_filters.is_encrypted.or(cached_filters.is_encrypted);
list_filters.is_invite =
list_filters.is_invite.or(cached_filters.is_invite);
if list_filters.room_types.is_empty() {
list_filters.room_types = cached_filters.room_types;
}
if list_filters.not_room_types.is_empty() {
list_filters.not_room_types = cached_filters.not_room_types;
}
list_filters.room_name_like = list_filters
.room_name_like
.clone()
.or(cached_filters.room_name_like);
if list_filters.tags.is_empty() {
list_filters.tags = cached_filters.tags;
}
if list_filters.not_tags.is_empty() {
list_filters.not_tags = cached_filters.not_tags;
}
}
(_, Some(cached_filters)) => list.filters = Some(cached_filters),
(_, _) => {}
}
if list.bump_event_types.is_empty() {
list.bump_event_types = cached_list.bump_event_types.clone();
};
}
cached.lists.insert(list_id.clone(), list.clone());
}
cached
.subscriptions
.extend(request.room_subscriptions.clone().into_iter());
request
.room_subscriptions
.extend(cached.subscriptions.clone().into_iter());
request.extensions.e2ee.enabled = request
.extensions
.e2ee
.enabled
.or(cached.extensions.e2ee.enabled);
request.extensions.to_device.enabled = request
.extensions
.to_device
.enabled
.or(cached.extensions.to_device.enabled);
request.extensions.account_data.enabled = request
.extensions
.account_data
.enabled
.or(cached.extensions.account_data.enabled);
request.extensions.account_data.lists = request
.extensions
.account_data
.lists
.clone()
.or(cached.extensions.account_data.lists.clone());
request.extensions.account_data.rooms = request
.extensions
.account_data
.rooms
.clone()
.or(cached.extensions.account_data.rooms.clone());
cached.extensions = request.extensions.clone();
cached.known_rooms.clone()
}
pub fn update_sync_subscriptions(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
conn_id: String,
subscriptions: BTreeMap<OwnedRoomId, sync_events::v4::RoomSubscription>,
) {
let mut cache = self.connections.lock().unwrap();
let cached = Arc::clone(
cache
.entry((user_id, device_id, conn_id))
.or_insert_with(|| {
Arc::new(Mutex::new(SlidingSyncCache {
lists: BTreeMap::new(),
subscriptions: BTreeMap::new(),
known_rooms: BTreeMap::new(),
extensions: ExtensionsConfig::default(),
}))
}),
);
let cached = &mut cached.lock().unwrap();
drop(cache);
cached.subscriptions = subscriptions;
}
pub fn update_sync_known_rooms(
&self,
user_id: OwnedUserId,
device_id: OwnedDeviceId,
conn_id: String,
list_id: String,
new_cached_rooms: BTreeMap<OwnedRoomId, bool>,
) {
let mut cache = self.connections.lock().unwrap();
let cached = Arc::clone(
cache
.entry((user_id, device_id, conn_id))
.or_insert_with(|| {
Arc::new(Mutex::new(SlidingSyncCache {
lists: BTreeMap::new(),
subscriptions: BTreeMap::new(),
known_rooms: BTreeMap::new(),
extensions: ExtensionsConfig::default(),
}))
}),
);
let cached = &mut cached.lock().unwrap();
drop(cache);
cached.known_rooms.insert(list_id, new_cached_rooms);
}
/// Check if account is deactivated
pub fn is_deactivated(&self, user_id: &UserId) -> Result<bool> {
self.db.is_deactivated(user_id)
@ -190,9 +398,15 @@ impl Service {
master_key: &Raw<CrossSigningKey>,
self_signing_key: &Option<Raw<CrossSigningKey>>,
user_signing_key: &Option<Raw<CrossSigningKey>>,
notify: bool,
) -> Result<()> {
self.db
.add_cross_signing_keys(user_id, master_key, self_signing_key, user_signing_key)
self.db.add_cross_signing_keys(
user_id,
master_key,
self_signing_key,
user_signing_key,
notify,
)
}
pub fn sign_key(
@ -226,20 +440,43 @@ impl Service {
self.db.get_device_keys(user_id, device_id)
}
pub fn get_master_key(
pub fn parse_master_key(
&self,
user_id: &UserId,
master_key: &Raw<CrossSigningKey>,
) -> Result<(Vec<u8>, CrossSigningKey)> {
self.db.parse_master_key(user_id, master_key)
}
pub fn get_key(
&self,
key: &[u8],
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.db.get_master_key(user_id, allowed_signatures)
self.db
.get_key(key, sender_user, user_id, allowed_signatures)
}
pub fn get_master_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.db
.get_master_key(sender_user, user_id, allowed_signatures)
}
pub fn get_self_signing_key(
&self,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: &dyn Fn(&UserId) -> bool,
) -> Result<Option<Raw<CrossSigningKey>>> {
self.db.get_self_signing_key(user_id, allowed_signatures)
self.db
.get_self_signing_key(sender_user, user_id, allowed_signatures)
}
pub fn get_user_signing_key(&self, user_id: &UserId) -> Result<Option<Raw<CrossSigningKey>>> {
@ -342,6 +579,7 @@ impl Service {
/// Ensure that a user only sees signatures from themselves and the target user
pub fn clean_signatures<F: Fn(&UserId) -> bool>(
cross_signing_key: &mut serde_json::Value,
sender_user: Option<&UserId>,
user_id: &UserId,
allowed_signatures: F,
) -> Result<(), Error> {
@ -355,9 +593,9 @@ pub fn clean_signatures<F: Fn(&UserId) -> bool>(
for (user, signature) in
mem::replace(signatures, serde_json::Map::with_capacity(new_capacity))
{
let id = <&UserId>::try_from(user.as_str())
let sid = <&UserId>::try_from(user.as_str())
.map_err(|_| Error::bad_database("Invalid user ID in database."))?;
if id == user_id || allowed_signatures(id) {
if sender_user == Some(user_id) || sid == user_id || allowed_signatures(sid) {
signatures.insert(user, signature);
}
}

View File

@ -9,7 +9,7 @@ use ruma::{
OwnedServerName,
};
use thiserror::Error;
use tracing::{error, warn};
use tracing::{error, info};
#[cfg(feature = "persy")]
use persy::PersyError;
@ -131,13 +131,35 @@ impl Error {
_ => (Unknown, StatusCode::INTERNAL_SERVER_ERROR),
};
warn!("{}: {}", status_code, message);
info!("Returning an error: {}: {}", status_code, message);
RumaResponse(UiaaResponse::MatrixError(RumaError {
body: ErrorBody::Standard { kind, message },
status_code,
}))
}
/// Sanitizes public-facing errors that can leak sensitive information.
pub fn sanitized_error(&self) -> String {
let db_error = String::from("Database or I/O error occurred.");
match self {
#[cfg(feature = "sled")]
Self::SledError { .. } => db_error,
#[cfg(feature = "sqlite")]
Self::SqliteError { .. } => db_error,
#[cfg(feature = "persy")]
Self::PersyError { .. } => db_error,
#[cfg(feature = "heed")]
Self::HeedError => db_error,
#[cfg(feature = "rocksdb")]
Self::RocksDbError { .. } => db_error,
Self::IoError { .. } => db_error,
Self::BadConfig { .. } => db_error,
Self::BadDatabase { .. } => db_error,
_ => self.to_string(),
}
}
}
#[cfg(feature = "persy")]

View File

@ -1,48 +0,0 @@
# For use in our CI only. This requires a build artifact created by a previous run pipline stage to be placed in cached_target/release/conduit
FROM valkum/docker-rust-ci:latest as builder
WORKDIR /workdir
ARG RUSTC_WRAPPER
ARG AWS_ACCESS_KEY_ID
ARG AWS_SECRET_ACCESS_KEY
ARG SCCACHE_BUCKET
ARG SCCACHE_ENDPOINT
ARG SCCACHE_S3_USE_SSL
COPY . .
RUN mkdir -p target/release
RUN test -e cached_target/release/conduit && cp cached_target/release/conduit target/release/conduit || cargo build --release
FROM valkum/docker-rust-ci:latest
WORKDIR /workdir
RUN curl -OL "https://github.com/caddyserver/caddy/releases/download/v2.2.1/caddy_2.2.1_linux_amd64.tar.gz"
RUN tar xzf caddy_2.2.1_linux_amd64.tar.gz
COPY cached_target/release/conduit /workdir/conduit
RUN chmod +x /workdir/conduit
RUN chmod +x /workdir/caddy
COPY conduit-example.toml conduit.toml
ENV SERVER_NAME=localhost
ENV CONDUIT_CONFIG=/workdir/conduit.toml
RUN sed -i "s/port = 6167/port = 8008/g" conduit.toml
RUN echo "allow_federation = true" >> conduit.toml
RUN echo "allow_encryption = true" >> conduit.toml
RUN echo "allow_registration = true" >> conduit.toml
RUN echo "log = \"warn,_=off,sled=off\"" >> conduit.toml
RUN sed -i "s/address = \"127.0.0.1\"/address = \"0.0.0.0\"/g" conduit.toml
# Enabled Caddy auto cert generation for complement provided CA.
RUN echo '{"logging":{"logs":{"default":{"level":"WARN"}}}, "apps":{"http":{"https_port":8448,"servers":{"srv0":{"listen":[":8448"],"routes":[{"match":[{"host":["your.server.name"]}],"handle":[{"handler":"subroute","routes":[{"handle":[{"handler":"reverse_proxy","upstreams":[{"dial":"127.0.0.1:8008"}]}]}]}],"terminal":true}],"tls_connection_policies": [{"match": {"sni": ["your.server.name"]}}]}}},"pki": {"certificate_authorities": {"local": {"name": "Complement CA","root": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"},"intermediate": {"certificate": "/ca/ca.crt","private_key": "/ca/ca.key"}}}},"tls":{"automation":{"policies":[{"subjects":["your.server.name"],"issuer":{"module":"internal"},"on_demand":true},{"issuer":{"module":"internal", "ca": "local"}}]}}}}' > caddy.json
EXPOSE 8008 8448
CMD ([ -z "${COMPLEMENT_CA}" ] && echo "Error: Need Complement PKI support" && true) || \
sed -i "s/#server_name = \"your.server.name\"/server_name = \"${SERVER_NAME}\"/g" conduit.toml && \
sed -i "s/your.server.name/${SERVER_NAME}/g" caddy.json && \
/workdir/caddy start --config caddy.json > /dev/null && \
/workdir/conduit