mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-09 10:41:43 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eabed74d6e | |||
| 614f8adb48 | |||
| 304850147c |
@@ -44,7 +44,7 @@ runs:
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -52,7 +52,7 @@ runs:
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -61,7 +61,7 @@ runs:
|
||||
- name: Extract metadata (tags) for Docker
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
||||
with:
|
||||
flavor: |
|
||||
latest=auto
|
||||
|
||||
@@ -67,7 +67,7 @@ runs:
|
||||
uses: ./.forgejo/actions/rust-toolchain
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@bb05f3f5519dd87d3ba754cc423b652a5edd6d2c # v4
|
||||
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4
|
||||
with:
|
||||
# Use persistent BuildKit if BUILDKIT_ENDPOINT is set (e.g. tcp://buildkit:8125)
|
||||
driver: ${{ env.BUILDKIT_ENDPOINT != '' && 'remote' || 'docker-container' }}
|
||||
@@ -75,11 +75,11 @@ runs:
|
||||
|
||||
- name: Set up QEMU
|
||||
if: ${{ env.BUILDKIT_ENDPOINT == '' }}
|
||||
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4
|
||||
uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4
|
||||
|
||||
- name: Login to builtin registry
|
||||
if: ${{ env.BUILTIN_REGISTRY_ENABLED == 'true' }}
|
||||
uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0 # v4
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4
|
||||
with:
|
||||
registry: ${{ env.BUILTIN_REGISTRY }}
|
||||
username: ${{ inputs.registry_user }}
|
||||
@@ -87,7 +87,7 @@ runs:
|
||||
|
||||
- name: Extract metadata (labels, annotations) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6
|
||||
uses: docker/metadata-action@80c7e94dd9b9319bd5eb7a0e0fe9291e23a2a2e9 # v6
|
||||
with:
|
||||
images: ${{ inputs.images }}
|
||||
# default labels & annotations: https://github.com/docker/metadata-action/blob/master/src/meta.ts#L509
|
||||
|
||||
@@ -120,7 +120,7 @@ runs:
|
||||
|
||||
- name: Install additional packages
|
||||
if: inputs.extra-packages != ''
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@553a35bb8ebd9fcabcb1c9451aa4c98e1b4ca8a9 # latest
|
||||
uses: https://github.com/awalsh128/cache-apt-pkgs-action@2c09a5e66da6c8016428a2172bd76e5e4f14bb17 # latest
|
||||
with:
|
||||
packages: ${{ inputs.extra-packages }}
|
||||
version: 1.0
|
||||
|
||||
@@ -71,7 +71,7 @@ runs:
|
||||
|
||||
- name: Install timelord-cli and git-warp-time
|
||||
if: steps.check-binaries.outputs.need-install == 'true'
|
||||
uses: https://github.com/taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2
|
||||
uses: https://github.com/taiki-e/install-action@9bcaee1dcae34154180f412e2fa69355a7cda9f6 # v2
|
||||
with:
|
||||
tool: git-warp-time,timelord-cli@3.0.1
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ jobs:
|
||||
# repositories: continuwuity
|
||||
|
||||
- name: Install regsync
|
||||
uses: https://github.com/regclient/actions/regsync-installer@39bf30ced4be9d147faaabbde2682e9725fb8bd1 # main
|
||||
uses: https://github.com/regclient/actions/regsync-installer@9a2d4216180dbb3e2dccfa60d2dd4afd98e42ec5 # main
|
||||
|
||||
- name: Check what images need mirroring
|
||||
run: |
|
||||
|
||||
@@ -53,7 +53,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Check for file changes
|
||||
uses: https://github.com/dorny/paths-filter@7b450fff21473bca461d4b92ce414b9d0420d706 # v4
|
||||
uses: https://github.com/dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
|
||||
@@ -62,7 +62,7 @@ jobs:
|
||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push Docker image by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
@@ -149,7 +149,7 @@ jobs:
|
||||
registry_password: ${{ secrets.BUILTIN_REGISTRY_PASSWORD || secrets.GITHUB_TOKEN }}
|
||||
- name: Build and push max-perf Docker image by digest
|
||||
id: build
|
||||
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7
|
||||
uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7
|
||||
with:
|
||||
context: .
|
||||
file: "docker/Dockerfile"
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
|
||||
image: ghcr.io/renovatebot/renovate:43.246.1@sha256:5965c08f8ca5baff8dc9bf3a32c44ca71fef843ad94880e9696d46e1d722b0fa
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -57,15 +57,3 @@ repos:
|
||||
types: [rust]
|
||||
stages:
|
||||
- pre-push
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: docs-build
|
||||
name: docs build
|
||||
entry: npm run docs:build
|
||||
language: system
|
||||
pass_filenames: false
|
||||
files: docs/
|
||||
types: [mdx]
|
||||
stages:
|
||||
- pre-commit
|
||||
|
||||
Generated
+316
-726
File diff suppressed because it is too large
Load Diff
+2
-14
@@ -141,12 +141,6 @@ features = [
|
||||
version = "0.23.25"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.aws-lc-sys]
|
||||
version = "0.41.0"
|
||||
|
||||
[workspace.dependencies.aws-lc-rs]
|
||||
version = "1.17.0"
|
||||
|
||||
[workspace.dependencies.reqwest]
|
||||
version = "0.13.2"
|
||||
default-features = false
|
||||
@@ -170,7 +164,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.29"
|
||||
version = "0.0.28"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -350,7 +344,7 @@ version = "1.1.1"
|
||||
[workspace.dependencies.ruma]
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "89cce34f3d498525769973243122914910c00ce5"
|
||||
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b"
|
||||
features = [
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
@@ -409,9 +403,6 @@ default-features = false
|
||||
version = "0.11.0"
|
||||
default-features = false
|
||||
|
||||
[workspace.dependencies.openidconnect]
|
||||
version = "4.0.1"
|
||||
|
||||
# optional opentelemetry, performance measurements, flamegraphs, etc for performance measurements and monitoring
|
||||
[workspace.dependencies.opentelemetry]
|
||||
version = "0.32.0"
|
||||
@@ -569,9 +560,6 @@ features = ["std"]
|
||||
[workspace.dependencies.nonzero_ext]
|
||||
version = "0.3.0"
|
||||
|
||||
[workspace.dependencies.resolvematrix]
|
||||
version = "0.1.2"
|
||||
|
||||
[workspace.dependencies.serde_urlencoded]
|
||||
version = "0.7.1"
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Improved invite and join reliability in clients using legacy sync. Contributed by @ginger
|
||||
@@ -1 +0,0 @@
|
||||
Removed the `/_conduwuit/` versions of the `local_user_count` and `version` routes. These routes are still accessible under the `/_continuwuity` prefix.
|
||||
@@ -1 +0,0 @@
|
||||
Rewrite the resolver service to use [resolvematrix](https://forgejo.ellis.link/continuwuation/resolvematrix) for server resolution. Rewrite by @s1lv3r, crate by @Jade
|
||||
@@ -1 +0,0 @@
|
||||
Updated deployment docs to account for new RPM package availability across more distros. Contributed by @julian45.
|
||||
@@ -1 +0,0 @@
|
||||
Added support for linking an external identity provider with OIDC. Contributed by @ginger.
|
||||
@@ -2028,96 +2028,3 @@
|
||||
# legacy authentication will be unable to log in.
|
||||
#
|
||||
#compatibility_mode = "hybrid"
|
||||
|
||||
#[global.oauth.oidc]
|
||||
# Uncommenting this section will enable Continuwuity's support for
|
||||
# authenticating users using an OpenID Connect-compatible identity provider.
|
||||
# This is referred to as "delegated authentication".
|
||||
#
|
||||
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
|
||||
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
|
||||
# Matrix clients which do not support OAuth login (also referred to as "next-gen auth") will NOT be able
|
||||
# to log in while delegated authentication is active.
|
||||
|
||||
|
||||
# The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
|
||||
# automatically fetch the identity provider's metadata from this URL.
|
||||
# Generally you should set this to the base domain your identity provider
|
||||
# runs on.
|
||||
#
|
||||
#discovery_url =
|
||||
|
||||
# The OAuth client ID for Continuwuity to use when communicating with the
|
||||
# identity provider.
|
||||
#
|
||||
#client_id =
|
||||
|
||||
# The OAuth client secret for Continuwuity to use when communicating with
|
||||
# the identity provider.
|
||||
#
|
||||
#client_secret =
|
||||
|
||||
# A path to a file which Continuwuity will read the client secret from.
|
||||
# If this option is set, it will override `client_secret`.
|
||||
#
|
||||
# The server will fail to start if the file cannot be read.
|
||||
#
|
||||
#client_secret_file =
|
||||
|
||||
# Additional scopes Continuwuity should request from the IDP. This may be
|
||||
# necessary to access certain claims. Continuwuity always requests the
|
||||
# `openid` scope.
|
||||
#
|
||||
#additional_scopes = []
|
||||
|
||||
# Whether the user should be prompted to choose a localpart
|
||||
# when signing in for the first time. If this is `false`, Continuwuity
|
||||
# will attempt to use the value of the `preferred_username_claim`
|
||||
# (see below) as the user's localpart. Authentication will
|
||||
# fail if this claim is missing or is not a valid localpart.
|
||||
#
|
||||
#prompt_for_localpart = true
|
||||
|
||||
# The claim to use for the user's localpart, if `prompt_for_localpart` is
|
||||
# false.
|
||||
#
|
||||
#preferred_username_claim = "preferred_username"
|
||||
|
||||
# The claim which will be used to set the user's email address,
|
||||
# either on initial registration or on every login depending on
|
||||
# the value of `profile_key_import_mode`. Continuwuity assumes that
|
||||
# the IDP has taken care of verifying that the user controls the email
|
||||
# address it provides.
|
||||
#
|
||||
# This option does nothing if SMTP is not configured.
|
||||
#
|
||||
# If this option is set, and `profile_key_import_mode` is `on_login`,
|
||||
# users will not be able to change their email addresses themselves.
|
||||
#
|
||||
#email_claim = "email"
|
||||
|
||||
# Defines how claims returned from the IDP should be mapped to a user's
|
||||
# profile data. The profile field named in each key will be set from the
|
||||
# claim named in the corresponding value when the user first registers,
|
||||
# and possibly on subsequent logins as well, depending on the value of
|
||||
# `profile_key_import_mode` (see below).
|
||||
#
|
||||
# Per-room overrides to the user's display name or avatar will be
|
||||
# preserved by the import process.
|
||||
#
|
||||
# SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
|
||||
# perform a HTTP GET to the URL in the mapped claim and use the returned
|
||||
# file as the user's profile picture. Make sure your users are not able
|
||||
# to set the value of the mapped claim to an arbitrary URL.
|
||||
#
|
||||
#profile_key_map = { displayname = "name" }
|
||||
|
||||
# When profile keys should be imported from the IDP's claims.
|
||||
#
|
||||
# - "on_registration": Listed keys will be imported once, when the user
|
||||
# logs in for the first time and their shadow account is created.
|
||||
# - "on_login": Listed keys will be imported every time the user logs in.
|
||||
# Additionally, users will not be able to manually edit any listed keys
|
||||
# through their Matrix client.
|
||||
#
|
||||
#profile_key_import_mode = "on_registration"
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "rpm",
|
||||
"label": "RPM"
|
||||
"name": "fedora",
|
||||
"label": "Fedora"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
# RPM Installation Guide
|
||||
|
||||
Continuwuity is available as RPM packages for Fedora and compatible distributions.
|
||||
We do not currently have infrastructure to build RPMs for RHEL and compatible distributions, but this is a work in progress.
|
||||
|
||||
The RPM packaging files are maintained in the `fedora/` directory:
|
||||
- `continuwuity.spec.rpkg` - RPM spec file using rpkg macros for building from git
|
||||
- `continuwuity.service` - Systemd service file for the server
|
||||
- `RPM-GPG-KEY-continuwuity.asc` - GPG public key for verifying signed packages
|
||||
|
||||
RPM packages built by CI are signed with our GPG key (RSA, ID: `6595 E8DB 9191 D39A 46D6 A514 4BA7 F590 DF0B AA1D`). # spellchecker:disable-line
|
||||
|
||||
```bash
|
||||
# Import the signing key
|
||||
sudo rpm --import https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
|
||||
# Verify a downloaded package
|
||||
rpm --checksig continuwuity-*.rpm
|
||||
```
|
||||
|
||||
## Installation methods
|
||||
|
||||
**Stable releases** (recommended)
|
||||
|
||||
```bash
|
||||
# Add the repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Development builds** from main branch
|
||||
|
||||
```bash
|
||||
# Add the dev repository and install
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/dev.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Feature branch builds** (example: `tom/new-feature`)
|
||||
|
||||
```bash
|
||||
# Branch names are sanitized (slashes become hyphens, lowercase only)
|
||||
sudo dnf config-manager addrepo --from-repofile=https://forgejo.ellis.link/api/packages/continuwuation/rpm/tom-new-feature.repo
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
**Manual repository configuration** (alternative method)
|
||||
|
||||
```bash
|
||||
cat << 'EOF' | sudo tee /etc/yum.repos.d/continuwuity.repo
|
||||
[continuwuity]
|
||||
name=Continuwuity - Matrix homeserver
|
||||
baseurl=https://forgejo.ellis.link/api/packages/continuwuation/rpm/stable
|
||||
enabled=1
|
||||
gpgcheck=1
|
||||
gpgkey=https://forgejo.ellis.link/api/packages/continuwuation/rpm/repository.key
|
||||
EOF
|
||||
|
||||
sudo dnf install continuwuity
|
||||
```
|
||||
|
||||
## Package management
|
||||
|
||||
**Automatic updates** with DNF Automatic
|
||||
|
||||
```bash
|
||||
# Install and configure
|
||||
sudo dnf install dnf-automatic
|
||||
sudo nano /etc/dnf/automatic.conf # Set: apply_updates = yes
|
||||
sudo systemctl enable --now dnf-automatic.timer
|
||||
```
|
||||
|
||||
**Manual updates**
|
||||
|
||||
```bash
|
||||
# Check for updates
|
||||
sudo dnf check-update continuwuity
|
||||
|
||||
# Update to latest version
|
||||
sudo dnf update continuwuity
|
||||
```
|
||||
|
||||
**Switching channels** (stable/dev/feature branches)
|
||||
|
||||
```bash
|
||||
# List enabled repositories
|
||||
dnf repolist | grep continuwuation
|
||||
|
||||
# Disable current repository
|
||||
sudo dnf config-manager --set-disabled continuwuation-stable # or -dev, or branch name
|
||||
|
||||
# Enable desired repository
|
||||
sudo dnf config-manager --set-enabled continuwuation-dev # or -stable, or branch name
|
||||
|
||||
# Update to the new channel's version
|
||||
sudo dnf update continuwuity
|
||||
```
|
||||
|
||||
**Verifying installation**
|
||||
|
||||
```bash
|
||||
# Check installed version
|
||||
rpm -q continuwuity
|
||||
|
||||
# View package information
|
||||
rpm -qi continuwuity
|
||||
|
||||
# List installed files
|
||||
rpm -ql continuwuity
|
||||
|
||||
# Verify package integrity
|
||||
rpm -V continuwuity
|
||||
```
|
||||
|
||||
## Service management and removal
|
||||
|
||||
**Systemd service commands**
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start conduwuit
|
||||
|
||||
# Enable on boot
|
||||
sudo systemctl enable conduwuit
|
||||
|
||||
# Check status
|
||||
sudo systemctl status conduwuit
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u conduwuit -f
|
||||
```
|
||||
|
||||
**Uninstallation**
|
||||
|
||||
```bash
|
||||
# Stop and disable the service
|
||||
sudo systemctl stop conduwuit
|
||||
sudo systemctl disable conduwuit
|
||||
|
||||
# Remove the package
|
||||
sudo dnf remove continuwuity
|
||||
|
||||
# Remove the repository (optional)
|
||||
sudo rm /etc/yum.repos.d/continuwuation-*.repo
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**GPG key errors**: Temporarily disable GPG checking
|
||||
|
||||
```bash
|
||||
sudo dnf --nogpgcheck install continuwuity
|
||||
```
|
||||
|
||||
**Repository metadata issues**: Clear and rebuild cache
|
||||
|
||||
```bash
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
**Finding specific versions**
|
||||
|
||||
```bash
|
||||
# List all available versions
|
||||
dnf --showduplicates list continuwuity
|
||||
|
||||
# Install a specific version
|
||||
sudo dnf install continuwuity-<version>
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
Build the RPM locally using rpkg:
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo dnf install rpkg rpm-build cargo-rpm-macros systemd-rpm-macros
|
||||
|
||||
# Clone the repository
|
||||
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
|
||||
cd continuwuity
|
||||
|
||||
# Build SRPM
|
||||
rpkg srpm
|
||||
|
||||
# Build RPM
|
||||
rpmbuild --rebuild *.src.rpm
|
||||
```
|
||||
@@ -207,7 +207,7 @@ ### Other Reverse Proxies
|
||||
|
||||
- `/_matrix/client` - core Client-Server APIs. These should be available on port :443
|
||||
|
||||
- `/_continuwuity/` - Continuwuity's integrated account management interface for OAuth-compatible clients
|
||||
- `/_conduwuit/` and `/_continuwuity/` - ad-hoc Continuwuity routes for password resets, email verification, and server details such as `/local_user_count` and `/server_version`.
|
||||
|
||||
You can optionally reverse proxy the following individual routes:
|
||||
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
import { Tab, Tabs} from '@rspress/core/theme';
|
||||
|
||||
# RPM Installation Guide
|
||||
|
||||
## Overview
|
||||
|
||||
Continuwuity versions v0.5.10+ are available as RPM packages for the following distributions:
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
Available for upstream-supported Fedora versions (including compatible distributions, such as Ultramarine Linux) and Rawhide through Terra.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
Available for Enterprise Linux (RHEL, AlmaLinux, Rocky Linux, etc.) **10+** through Terra.
|
||||
|
||||
:::warning Oracle Linux support
|
||||
Due to upstream limitations, Terra is only usable on Oracle Linux if you use [upstream EPEL](https://docs.fedoraproject.org/en-US/epel/getting-started/)
|
||||
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, as of the time of writing, 10.2 has been
|
||||
available for over a month through other ELs, but Oracle has not yet released corresponding updates—so you may encounter further compatibility issues with EPEL.
|
||||
**For this reason, it is recommended that you use another EL distribution if at all possible.**
|
||||
:::
|
||||
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
Available for openSUSE Tumbleweed and [supported Leap versions](https://en.opensuse.org/Lifetime) through the openSUSE Build Service.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Installation methods
|
||||
|
||||
### Stable releases (recommended)
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#fedora-and-derivatives).
|
||||
2. Install the `continuwuity` package.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#enterprise-linux). Be sure to acquire/enable relevant dependencies!
|
||||
2. Install the `continuwuity` package.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
openSUSE packages are available through an openSUSE Build Service project namespace provided by a community member.
|
||||
|
||||
1. Navigate to [this page](https://software.opensuse.org/download.html?project=home%3Ajulian45&package=continuwuity).
|
||||
2. Click on the "Add repository and install manually" text to expand install instructions.
|
||||
3. Follow the provided instructions for your version of openSUSE.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### Nightly releases
|
||||
Nightly versions are built from the latest commit on the `main` branch every 24 hours.
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#fedora-and-derivatives).
|
||||
2. Install the `continuwuity-nightly` package.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's directions for adding the Terra repo on your distribution](https://docs.terrapkg.com/usage/installing/#enterprise-linux). Be sure to acquire/enable relevant dependencies!
|
||||
2. Install the `continuwuity-nightly` package.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
Nightly packages for openSUSE are **not currently available**.
|
||||
|
||||
If there is an ongoing need for these, please ask in `#continuwuity:continuwuity.org` or [open an issue on Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/issues/new).
|
||||
In the meantime, you may follow the [generic deployment instructions](generic).
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## Service management and removal
|
||||
|
||||
**Systemd service commands**
|
||||
|
||||
```bash
|
||||
# Start the service
|
||||
sudo systemctl start conduwuit
|
||||
|
||||
# Enable on boot
|
||||
sudo systemctl enable conduwuit
|
||||
|
||||
# Check status
|
||||
sudo systemctl status conduwuit
|
||||
|
||||
# View logs
|
||||
sudo journalctl -u conduwuit -f
|
||||
```
|
||||
|
||||
**Uninstallation**
|
||||
|
||||
```bash
|
||||
# Stop and disable the service
|
||||
sudo systemctl stop conduwuit
|
||||
sudo systemctl disable conduwuit
|
||||
|
||||
# Remove the package
|
||||
sudo dnf remove continuwuity # replace `dnf` with `zypper` on openSUSE
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**GPG key errors**: Temporarily disable GPG checking
|
||||
|
||||
```bash
|
||||
sudo dnf --nogpgcheck install continuwuity
|
||||
```
|
||||
|
||||
**Repository metadata issues**: Clear and rebuild cache
|
||||
|
||||
```bash
|
||||
sudo dnf clean all
|
||||
sudo dnf makecache
|
||||
```
|
||||
|
||||
**Finding specific versions**
|
||||
|
||||
```bash
|
||||
# List all available versions
|
||||
dnf --showduplicates list continuwuity
|
||||
|
||||
# Install a specific version
|
||||
sudo dnf install continuwuity-<version>
|
||||
```
|
||||
|
||||
## Building locally
|
||||
|
||||
<Tabs groupId="distro">
|
||||
<Tab label="Fedora">
|
||||
:::details Directly from Continuwuity source
|
||||
|
||||
Build the RPM locally using rpkg:
|
||||
```bash
|
||||
# Install dependencies
|
||||
sudo dnf install rpkg rpm-build cargo-rpm-macros systemd-rpm-macros
|
||||
|
||||
# Clone the repository
|
||||
git clone https://forgejo.ellis.link/continuwuation/continuwuity.git
|
||||
cd continuwuity
|
||||
|
||||
# Build SRPM
|
||||
rpkg srpm
|
||||
|
||||
# Build RPM
|
||||
rpmbuild --rebuild *.src.rpm
|
||||
```
|
||||
:::
|
||||
|
||||
Or, use Terra:
|
||||
1. Follow [Terra's prep directions](https://docs.terrapkg.com/contributing/getting-started/#preparation) to bootstrap your development environment.
|
||||
2. `git clone` the [Terra sources repo](https://github.com/terrapkg/packages).
|
||||
3. Follow [Terra's build instructions](https://docs.terrapkg.com/contributing/getting-started/#building), using the path `anda/misc/continuwuity/nightly/pkg`. If desiring a build of the latest stable version, replace `nightly` with `stable` instead.
|
||||
</Tab>
|
||||
<Tab label="EL">
|
||||
1. Follow [Terra's prep directions](https://docs.terrapkg.com/contributing/getting-started/#preparation) to bootstrap your development environment.
|
||||
2. `git clone` the [Terra sources repo](https://github.com/terrapkg/packages).
|
||||
3. Follow [Terra's build instructions](https://docs.terrapkg.com/contributing/getting-started/#building), using the path `anda/misc/continuwuity/nightly/pkg`. If desiring a build of the latest stable version, replace `nightly` with `stable` instead.
|
||||
</Tab>
|
||||
<Tab label="SUSE">
|
||||
1. Navigate to [this package's location in the openSUSE Build Service web UI](https://build.opensuse.org/package/show/home:julian45/continuwuity).
|
||||
2. Click on the "Checkout package" text and follow the instructions to acquire a local copy of the packaging source.
|
||||
3. In the `_service` file, adjust the revision within the `<param name="revision">` tag (approx. line 6) to your desired git repo revision.
|
||||
4. Run `osc service ra` to update your copy of the Continuwuity sources and vendored packages.
|
||||
5. Run `osc build` to locally build an openSUSE-ready RPM.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
@@ -10,13 +10,7 @@ ## `!admin debug echo`
|
||||
|
||||
## `!admin debug get-auth-chain`
|
||||
|
||||
Loads the auth_chain of a PDU, reporting how long it took
|
||||
|
||||
## `!admin debug show-auth-chain`
|
||||
|
||||
Walks & displays the auth_chain of a PDU in a mermaid graph format.
|
||||
|
||||
This is useless to basically anyone but developers, and is also probably slow and memory hungry.
|
||||
Get the auth_chain of a PDU
|
||||
|
||||
## `!admin debug parse-pdu`
|
||||
|
||||
@@ -50,12 +44,6 @@ ## `!admin debug get-room-state`
|
||||
|
||||
Of course the check is still done on the actual client API.
|
||||
|
||||
## `!admin debug get-state-at`
|
||||
|
||||
Gets all the room state events at the specified event.
|
||||
|
||||
State at event might not be available for some PDUs, such as rejected ones.
|
||||
|
||||
## `!admin debug get-signing-keys`
|
||||
|
||||
Get and display signing keys from local cache or remote server
|
||||
|
||||
@@ -14,7 +14,6 @@ ## Categories
|
||||
- [`!admin appservices`](appservices/): Commands for managing appservices
|
||||
- [`!admin users`](users/): Commands for managing local users
|
||||
- [`!admin token`](token/): Commands for managing registration tokens
|
||||
- [`!admin oidc`](oidc/): Commands for managing OIDC
|
||||
- [`!admin rooms`](rooms/): Commands for managing rooms
|
||||
- [`!admin federation`](federation/): Commands for managing federation
|
||||
- [`!admin server`](server/): Commands for managing the server
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
<!-- This file is generated by `cargo xtask generate-docs`. Do not edit. -->
|
||||
# `!admin oidc`
|
||||
|
||||
Commands for managing OIDC
|
||||
|
||||
|
||||
## `!admin oidc link`
|
||||
|
||||
Link a user ID to the given subject claim
|
||||
|
||||
## `!admin oidc unlink`
|
||||
|
||||
Unlink the given subject claim from its associated user ID
|
||||
@@ -12,6 +12,10 @@ ## `!admin users reset-password`
|
||||
|
||||
Reset user password
|
||||
|
||||
## `!admin users issue-password-reset-link`
|
||||
|
||||
Issue a self-service password reset link for a user
|
||||
|
||||
## `!admin users get-email`
|
||||
|
||||
Get a user's associated email address
|
||||
@@ -92,14 +96,6 @@ ## `!admin users list-users`
|
||||
|
||||
List local users in the database
|
||||
|
||||
## `!admin users list-invited-rooms`
|
||||
|
||||
Lists all the rooms (local and remote) that the specified user is invited to
|
||||
|
||||
## `!admin users reject-all-invites`
|
||||
|
||||
Manually make a user reject all current invites
|
||||
|
||||
## `!admin users list-joined-rooms`
|
||||
|
||||
Lists all the rooms (local and remote) that the specified user is joined in
|
||||
|
||||
Generated
+25
-25
@@ -383,9 +383,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/core": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.16.tgz",
|
||||
"integrity": "sha512-jJcYNNBKY/VLR8oxLqdd5uvm6bHahXeF3wSaoAe2U1hxWWwoP9k4rBTDx9X3JkUWcgnthu7UgtMiHeLs+2fhFg==",
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/core/-/core-2.0.15.tgz",
|
||||
"integrity": "sha512-epLmUXYscNRw/GtQZx2oknoBE9wKbCrUGEOrQEDI4Qq8X32GdM4d7itzuHsliY7q3IbffKx8rMVbvlmygEocTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -393,7 +393,7 @@
|
||||
"@mdx-js/react": "^3.1.1",
|
||||
"@rsbuild/core": "^2.0.15",
|
||||
"@rsbuild/plugin-react": "~2.0.1",
|
||||
"@rspress/shared": "2.0.16",
|
||||
"@rspress/shared": "2.0.15",
|
||||
"@shikijs/rehype": "^4.2.0",
|
||||
"@types/unist": "^3.0.3",
|
||||
"@unhead/react": "^2.1.15",
|
||||
@@ -412,11 +412,11 @@
|
||||
"react-lazy-with-preload": "^2.2.1",
|
||||
"react-reconciler": "0.33.0",
|
||||
"react-render-to-markdown": "19.1.0",
|
||||
"react-router-dom": "^7.18.1",
|
||||
"react-router-dom": "^7.15.1",
|
||||
"rehype-external-links": "^3.0.0",
|
||||
"rehype-raw": "^7.0.0",
|
||||
"remark-cjk-friendly": "^2.3.1",
|
||||
"remark-cjk-friendly-gfm-strikethrough": "^2.3.1",
|
||||
"remark-cjk-friendly-gfm-strikethrough": "^2.1.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"remark-mdx": "^3.1.1",
|
||||
"remark-parse": "^11.0.0",
|
||||
@@ -436,9 +436,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-client-redirects": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.16.tgz",
|
||||
"integrity": "sha512-FEjZb+3lxpkEESdt0uWa4dRQU7d/Okn5SyX7CDaMHdHuqg4XdOidAQ/95ZzgXomN7YVPv40eVe/0is3oWnAjew==",
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-client-redirects/-/plugin-client-redirects-2.0.15.tgz",
|
||||
"integrity": "sha512-bPf/KIHH7Y6huLTtK6JXwRfxM7zKjksoxm46+IBsF1wisw0doKkEKR9HwJydxWnykyKBbA2cuZOaoT4h174Z1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -449,9 +449,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/plugin-sitemap": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.16.tgz",
|
||||
"integrity": "sha512-zRPvKAGF8EexblJvrkhhHtD2Kqlbaw6XFjbIMM07gs0SIUKNg1o3T2I/uo5cvWtk0pGfYyyAJj94HoqLUgUsEw==",
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/plugin-sitemap/-/plugin-sitemap-2.0.15.tgz",
|
||||
"integrity": "sha512-z1hbyGP79ZXdSGJxiWw7ZjmX8qW0q9nXMDxr14cVEg/wdj7ToVzGtZHw0wvTPE0YiKG3BMiGkVNfE1rdOaPXiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -462,9 +462,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@rspress/shared": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.16.tgz",
|
||||
"integrity": "sha512-FjBSfGtgrlR1bRJ0EQLyNo2qXUXxzb2QE3NPRIICf8TKpP413gRCMyMRtzhbIqD4Gn7k+em82VAkWTAAQjQLTw==",
|
||||
"version": "2.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@rspress/shared/-/shared-2.0.15.tgz",
|
||||
"integrity": "sha512-o8aYwEzNuTmWnmKe91ntPv+34u3RbtAe+rcK9XC5MANOlgncwOaCs3bUa8/B1/llwyLoNgrpi+VB9bEiU11ZRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2869,9 +2869,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.1.tgz",
|
||||
"integrity": "sha512-GDLgg3i3uM0aeJO3Fm+TCS+sDQ7gu12T6x0qdTEzcwqEfleci7JwugVNIF3U//0FWKnJT7ptG+20B2jfDqnZAg==",
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.17.0.tgz",
|
||||
"integrity": "sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2892,13 +2892,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/react-router-dom": {
|
||||
"version": "7.18.1",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.1.tgz",
|
||||
"integrity": "sha512-KaZh+X/6UtEp28x51AUYZDMg9NGoz2ja3dNHa+ta/tk40vCzKhQ/RypCWBMLbmDr6//E24Vv5uPsrqXFozdkAg==",
|
||||
"version": "7.17.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.17.0.tgz",
|
||||
"integrity": "sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-router": "7.18.1"
|
||||
"react-router": "7.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
@@ -3081,9 +3081,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/remark-cjk-friendly-gfm-strikethrough": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.3.1.tgz",
|
||||
"integrity": "sha512-JE3TGgouk/sy92SemNMEUhO5mNP4on04cmzOV3s3R5Dbk160ewmpM4tgPiinKKvoJ5UW2fTu7FOYsjVbusSA9w==",
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/remark-cjk-friendly-gfm-strikethrough/-/remark-cjk-friendly-gfm-strikethrough-2.1.0.tgz",
|
||||
"integrity": "sha512-3Kyq2hjY7V7eU8MbVbWW6QQLN81pjJcIvKHvPxr8hZZmcq/9wqm3MJ3iUG34Ch9QTM4WHN+a1JVAVC1fSi5mig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -92,7 +92,6 @@ serde-saphyr.workspace = true
|
||||
tokio.workspace = true
|
||||
tracing-subscriber.workspace = true
|
||||
tracing.workspace = true
|
||||
resolvematrix.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
+10
-25
@@ -1,5 +1,5 @@
|
||||
use clap::Parser;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::{
|
||||
appservice::{self, AppserviceCommand},
|
||||
@@ -8,7 +8,6 @@
|
||||
debug::{self, DebugCommand},
|
||||
federation::{self, FederationCommand},
|
||||
media::{self, MediaCommand},
|
||||
oidc::{self, OidcCommand},
|
||||
query::{self, QueryCommand},
|
||||
room::{self, RoomCommand},
|
||||
server::{self, ServerCommand},
|
||||
@@ -19,48 +18,44 @@
|
||||
#[derive(Debug, Parser)]
|
||||
#[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
|
||||
pub enum AdminCommand {
|
||||
/// Commands for managing appservices
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing appservices
|
||||
Appservices(AppserviceCommand),
|
||||
|
||||
/// Commands for managing local users
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing local users
|
||||
Users(UserCommand),
|
||||
|
||||
/// Commands for managing registration tokens
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing registration tokens
|
||||
Token(TokenCommand),
|
||||
|
||||
/// Commands for managing OIDC
|
||||
#[command(subcommand)]
|
||||
Oidc(OidcCommand),
|
||||
|
||||
/// Commands for managing rooms
|
||||
#[command(subcommand)]
|
||||
Rooms(RoomCommand),
|
||||
|
||||
/// Commands for managing federation
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing federation
|
||||
Federation(FederationCommand),
|
||||
|
||||
/// Commands for managing the server
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing the server
|
||||
Server(ServerCommand),
|
||||
|
||||
/// Commands for managing media
|
||||
#[command(subcommand)]
|
||||
/// Commands for managing media
|
||||
Media(MediaCommand),
|
||||
|
||||
/// Commands for checking integrity
|
||||
#[command(subcommand)]
|
||||
/// Commands for checking integrity
|
||||
Check(CheckCommand),
|
||||
|
||||
/// Commands for debugging things
|
||||
#[command(subcommand)]
|
||||
/// Commands for debugging things
|
||||
Debug(DebugCommand),
|
||||
|
||||
/// Low-level queries for database getters and iterators
|
||||
#[command(subcommand)]
|
||||
/// Low-level queries for database getters and iterators
|
||||
Query(QueryCommand),
|
||||
}
|
||||
|
||||
@@ -85,16 +80,6 @@ pub(super) async fn process(command: AdminCommand, context: &Context<'_>) -> Res
|
||||
context.bail_restricted()?;
|
||||
token::process(command, context).await
|
||||
},
|
||||
| Oidc(command) => {
|
||||
// OIDC commands are all restricted
|
||||
context.bail_restricted()?;
|
||||
|
||||
if !context.services.oidc.enabled() {
|
||||
return Err!("OIDC is not configured");
|
||||
}
|
||||
|
||||
oidc::process(command, context).await
|
||||
},
|
||||
| Rooms(command) => room::process(command, context).await,
|
||||
| Federation(command) => federation::process(command, context).await,
|
||||
| Server(command) => server::process(command, context).await,
|
||||
|
||||
@@ -6,12 +6,7 @@
|
||||
impl Context<'_> {
|
||||
pub(super) async fn check_all_users(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let users = self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
let users = self.services.users.stream().collect::<Vec<_>>().await;
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
let total = users.len();
|
||||
|
||||
@@ -20,10 +20,6 @@
|
||||
};
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use lettre::message::Mailbox;
|
||||
use resolvematrix::{
|
||||
resolution::ResolvedDestination,
|
||||
server::{MatrixResolver, MatrixResolverBuilder},
|
||||
};
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId,
|
||||
OwnedRoomOrAliasId, OwnedServerName, RoomId, RoomVersionId, UInt,
|
||||
@@ -616,7 +612,7 @@ pub(super) async fn force_device_list_updates(&self) -> Result {
|
||||
// Force E2EE device list updates for all users
|
||||
self.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.stream()
|
||||
.for_each(async |user_id| self.services.users.mark_device_key_update(&user_id).await)
|
||||
.await;
|
||||
|
||||
@@ -1011,23 +1007,13 @@ pub(super) async fn resolve_true_destination(
|
||||
);
|
||||
}
|
||||
|
||||
let resolver: &MatrixResolver = if no_cache {
|
||||
&MatrixResolverBuilder::new()
|
||||
.dangerous_tls_accept_invalid_certs(self.services.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
||||
.http_client(self.services.client.default.clone())
|
||||
.build()?
|
||||
} else {
|
||||
&self.services.resolver.resolver
|
||||
};
|
||||
let actual = self
|
||||
.services
|
||||
.resolver
|
||||
.resolve_actual_dest(&server_name, !no_cache)
|
||||
.await?;
|
||||
|
||||
let actual = resolver.resolve_server(server_name.as_str()).await?;
|
||||
|
||||
let destination = match actual.destination {
|
||||
| ResolvedDestination::Literal(addr) => addr.to_string(),
|
||||
| ResolvedDestination::Named(host, port) => format!("{host}:{port}"),
|
||||
};
|
||||
|
||||
let msg = format!("Destination: {}\nHostname URI (SNI): {}", destination, actual.host);
|
||||
let msg = format!("Destination: {}\nHostname URI: {}", actual.dest, actual.host);
|
||||
self.write_str(&msg).await
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
pub(crate) mod debug;
|
||||
pub(crate) mod federation;
|
||||
pub(crate) mod media;
|
||||
pub(crate) mod oidc;
|
||||
pub(crate) mod query;
|
||||
pub(crate) mod room;
|
||||
pub(crate) mod server;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
use conduwuit::Result;
|
||||
|
||||
use crate::utils::parse_active_local_user_id;
|
||||
|
||||
impl crate::Context<'_> {
|
||||
pub(super) async fn oidc_link(&self, user_id: String, subject: String) -> Result {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
self.services.oidc.link_user(&user_id, &subject);
|
||||
|
||||
self.write_str(&format!("Subject `{subject}` linked to account `{user_id}`."))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn oidc_unlink(&self, subject: String) -> Result {
|
||||
self.services.oidc.unlink_user(&subject);
|
||||
|
||||
self.write_str(&format!("Subject `{subject}` unlinked."))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
mod commands;
|
||||
|
||||
use clap::Subcommand;
|
||||
use conduwuit::Result;
|
||||
use conduwuit_macros::admin_command_dispatch;
|
||||
|
||||
#[admin_command_dispatch]
|
||||
#[derive(Debug, Subcommand)]
|
||||
pub enum OidcCommand {
|
||||
/// Link a user ID to the given subject claim.
|
||||
#[clap(name = "link")]
|
||||
OidcLink {
|
||||
user_id: String,
|
||||
subject: String,
|
||||
},
|
||||
|
||||
/// Unlink the given subject claim from its associated user ID.
|
||||
#[clap(name = "unlink")]
|
||||
OidcUnlink {
|
||||
subject: String,
|
||||
},
|
||||
}
|
||||
@@ -46,7 +46,7 @@ async fn destinations_cache(&self, server_name: Option<OwnedServerName>) -> Resu
|
||||
writeln!(self, "| Server Name | Destination | Hostname | Expires |").await?;
|
||||
writeln!(self, "| ----------- | ----------- | -------- | ------- |").await?;
|
||||
|
||||
let mut destinations = self.services.resolver.dns.cache.destinations().boxed();
|
||||
let mut destinations = self.services.resolver.cache.destinations().boxed();
|
||||
|
||||
while let Some((name, CachedDest { dest, host, expire })) = destinations.next().await {
|
||||
if let Some(server_name) = server_name.as_ref() {
|
||||
@@ -69,7 +69,7 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
|
||||
writeln!(self, "| Server Name | IP | Port | Expires | Overriding |").await?;
|
||||
writeln!(self, "| ----------- | --- | ----:| ------- | ---------- |").await?;
|
||||
|
||||
let mut overrides = self.services.resolver.dns.cache.overrides().boxed();
|
||||
let mut overrides = self.services.resolver.cache.overrides().boxed();
|
||||
|
||||
while let Some((name, CachedOverride { ips, port, expire, overriding })) =
|
||||
overrides.next().await
|
||||
@@ -92,16 +92,11 @@ async fn overrides_cache(&self, server_name: Option<String>) -> Result {
|
||||
|
||||
async fn flush_cache(&self, name: Option<OwnedServerName>, all: bool) -> Result {
|
||||
if all {
|
||||
self.services.resolver.resolver.clear_cache();
|
||||
self.services.resolver.dns.cache.clear().await;
|
||||
self.services.resolver.cache.clear().await;
|
||||
writeln!(self, "Resolver caches cleared!").await
|
||||
} else if let Some(name) = name {
|
||||
self.services
|
||||
.resolver
|
||||
.resolver
|
||||
.remove_cache_entry(name.as_str());
|
||||
self.services.resolver.dns.cache.del_destination(&name);
|
||||
self.services.resolver.dns.cache.del_override(&name);
|
||||
self.services.resolver.cache.del_destination(&name);
|
||||
self.services.resolver.cache.del_override(&name);
|
||||
self.write_str(&format!("Cleared {name} from resolver caches!"))
|
||||
.await
|
||||
} else {
|
||||
|
||||
@@ -191,13 +191,8 @@ async fn get_latest_backup(&self, user_id: OwnedUserId) -> Result {
|
||||
|
||||
async fn iter_users(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result: Vec<OwnedUserId> = self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.map(Into::into)
|
||||
.collect()
|
||||
.await;
|
||||
let result: Vec<OwnedUserId> =
|
||||
self.services.users.stream().map(Into::into).collect().await;
|
||||
|
||||
let query_time = timer.elapsed();
|
||||
|
||||
@@ -207,7 +202,7 @@ async fn iter_users(&self) -> Result {
|
||||
|
||||
async fn iter_users2(&self) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
let result: Vec<_> = self.services.users.stream_local_users().collect().await;
|
||||
let result: Vec<_> = self.services.users.stream().collect().await;
|
||||
let result: Vec<_> = result
|
||||
.into_iter()
|
||||
.map(|user_id| String::from_utf8_lossy(user_id.as_bytes()).into_owned())
|
||||
|
||||
@@ -44,7 +44,7 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
|
||||
.services
|
||||
.config
|
||||
.oauth
|
||||
.compatibility_mode()
|
||||
.compatibility_mode
|
||||
.oauth_available()
|
||||
{
|
||||
self.write_str(&format!(
|
||||
|
||||
+65
-55
@@ -20,7 +20,7 @@
|
||||
tag::{TagEvent, TagEventContent, TagInfo},
|
||||
},
|
||||
};
|
||||
use service::users::{AccountStatus, HashedPassword};
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::{
|
||||
get_room_info,
|
||||
@@ -35,7 +35,7 @@ pub(super) async fn list_users(&self) -> Result {
|
||||
let users: Vec<_> = self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.list_local_users()
|
||||
.map(|id| id.as_str().to_owned())
|
||||
.collect()
|
||||
.await;
|
||||
@@ -54,16 +54,16 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
.determine_registration_user_id(Some(username), None, None)
|
||||
.await?;
|
||||
|
||||
let password =
|
||||
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
let password = HashedPassword::new(
|
||||
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)),
|
||||
)?;
|
||||
|
||||
self.services
|
||||
.users
|
||||
.create_local_account(&user_id, Some(HashedPassword::new(password)?), None)
|
||||
.await?;
|
||||
.create_local_account(&user_id, password, None)
|
||||
.await;
|
||||
|
||||
self.write_str(&format!("Created user {user_id} with password `{password}`"))
|
||||
.await
|
||||
self.write_str(&format!("Created user {user_id}")).await
|
||||
}
|
||||
|
||||
pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) -> Result {
|
||||
@@ -103,12 +103,15 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
|
||||
|
||||
pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to suspend the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("Admin users cannot be suspended.");
|
||||
}
|
||||
@@ -124,12 +127,15 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn unsuspend(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to unsuspend the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
self.services.users.unsuspend_account(&user_id).await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been unsuspended."))
|
||||
@@ -141,7 +147,6 @@ pub(super) async fn reset_password(
|
||||
logout: bool,
|
||||
username: String,
|
||||
password: Option<String>,
|
||||
convert_to_local_account: bool,
|
||||
) -> Result {
|
||||
let user_id = parse_local_user_id(self.services, &username)?;
|
||||
|
||||
@@ -154,37 +159,15 @@ pub(super) async fn reset_password(
|
||||
|
||||
let new_password =
|
||||
password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
|
||||
let new_password_hash = HashedPassword::new(&new_password)?;
|
||||
|
||||
if convert_to_local_account {
|
||||
self.services
|
||||
.users
|
||||
.convert_to_local_account(&user_id, new_password_hash)
|
||||
.await?;
|
||||
} else {
|
||||
match self.services.users.status(&user_id).await {
|
||||
| AccountStatus::Active if !self.services.users.is_shadow(&user_id).await => {
|
||||
self.services
|
||||
.users
|
||||
.set_password(&user_id, new_password_hash)
|
||||
.await?;
|
||||
},
|
||||
| AccountStatus::NotFound => {
|
||||
return Err!("The provided user does not exist.");
|
||||
},
|
||||
| _ => {
|
||||
return Err!(
|
||||
"The provided user is a shadow or deactivated account. To convert it to \
|
||||
a local account, pass the --convert-to-local-account flag."
|
||||
);
|
||||
},
|
||||
}
|
||||
self.services
|
||||
.users
|
||||
.set_password(&user_id, Some(HashedPassword::new(&new_password)?));
|
||||
|
||||
self.write_str(&format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))
|
||||
.await?;
|
||||
}
|
||||
self.write_str(&format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))
|
||||
.await?;
|
||||
|
||||
if logout {
|
||||
self.services
|
||||
@@ -588,7 +571,7 @@ pub(super) async fn force_join_all_local_users(
|
||||
for user_id in &self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.list_local_users()
|
||||
.collect::<Vec<_>>()
|
||||
.await
|
||||
{
|
||||
@@ -936,16 +919,21 @@ pub(super) async fn force_leave_remote_room(
|
||||
|
||||
pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to lock the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("Admin users cannot be locked.");
|
||||
}
|
||||
|
||||
self.services
|
||||
.users
|
||||
.lock_account(&user_id, self.sender_or_service_user())
|
||||
@@ -957,8 +945,11 @@ pub(super) async fn lock(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
self.services.users.unlock_account(&user_id).await;
|
||||
|
||||
self.write_str(&format!("User {user_id} has been unlocked."))
|
||||
@@ -967,16 +958,21 @@ pub(super) async fn unlock(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to log out the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("You cannot forcefully log out admin users.");
|
||||
}
|
||||
|
||||
self.services
|
||||
.users
|
||||
.all_device_ids(&user_id)
|
||||
@@ -993,12 +989,18 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if user_id == self.services.globals.server_user {
|
||||
return Err!("Not allowed to disable login for the server service account.",);
|
||||
}
|
||||
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
if self.services.users.is_admin(&user_id).await {
|
||||
return Err!("Admin users cannot have their login disallowed.");
|
||||
}
|
||||
@@ -1012,8 +1014,14 @@ pub(super) async fn disable_login(&self, user_id: String) -> Result {
|
||||
|
||||
pub(super) async fn enable_login(&self, user_id: String) -> Result {
|
||||
self.bail_restricted()?;
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
assert!(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"Parsed user_id must be a local user"
|
||||
);
|
||||
if !self.services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id} does not exist.");
|
||||
}
|
||||
self.services.users.enable_login(&user_id);
|
||||
|
||||
self.write_str(&format!("{user_id} can now log in.")).await
|
||||
@@ -1121,8 +1129,10 @@ pub(super) async fn change_email(&self, user_id: String, email: Option<String>)
|
||||
}
|
||||
|
||||
pub(super) async fn reset_push_rules(&self, user_id: String) -> Result {
|
||||
let user_id = parse_active_local_user_id(self.services, &user_id).await?;
|
||||
|
||||
let user_id = parse_local_user_id(self.services, &user_id)?;
|
||||
if !self.services.users.is_active(&user_id).await {
|
||||
return Err!("User is not active.");
|
||||
}
|
||||
recreate_push_rules_and_return(self.services, &user_id).await?;
|
||||
self.write_str("Reset user's push rules to the server default.")
|
||||
.await
|
||||
|
||||
@@ -27,9 +27,6 @@ pub enum UserCommand {
|
||||
username: String,
|
||||
/// New password for the user, if unspecified one is generated
|
||||
password: Option<String>,
|
||||
|
||||
#[arg(long)]
|
||||
convert_to_local_account: bool,
|
||||
},
|
||||
|
||||
/// Get a user's associated email address.
|
||||
|
||||
+8
-1
@@ -54,7 +54,14 @@ pub(crate) async fn parse_active_local_user_id(
|
||||
user_id: &str,
|
||||
) -> Result<OwnedUserId> {
|
||||
let user_id = parse_local_user_id(services, user_id)?;
|
||||
services.users.status(&user_id).await.ensure_active()?;
|
||||
|
||||
if !services.users.exists(&user_id).await {
|
||||
return Err!("User {user_id:?} does not exist on this server.");
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!("User {user_id:?} is deactivated.");
|
||||
}
|
||||
|
||||
Ok(user_id)
|
||||
}
|
||||
|
||||
@@ -128,8 +128,7 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(&sender_user, HashedPassword::new(&body.new_password)?)
|
||||
.await?;
|
||||
.set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
|
||||
|
||||
if body.logout_devices {
|
||||
// Logout all devices except the current one
|
||||
|
||||
@@ -72,7 +72,7 @@ pub(crate) async fn register_route(
|
||||
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
|
||||
.await?;
|
||||
|
||||
services.users.create_shadow_account(&user_id).await?;
|
||||
services.users.create(&user_id, None)?;
|
||||
|
||||
user_id
|
||||
} else {
|
||||
@@ -97,8 +97,8 @@ pub(crate) async fn register_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.create_local_account(&user_id, Some(password), identity.email)
|
||||
.await?;
|
||||
.create_local_account(&user_id, password, identity.email)
|
||||
.await;
|
||||
|
||||
user_id
|
||||
};
|
||||
@@ -106,7 +106,7 @@ pub(crate) async fn register_route(
|
||||
let (token, device) = if !body.inhibit_login {
|
||||
// If UIAA is disabled, we can't create a device. In that case only appservices
|
||||
// can reach this point in the first place, so we return an error for them.
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
||||
return Err!(Request(AppserviceLoginUnsupported(
|
||||
"User-interactive appservice registration is not available on this server."
|
||||
)));
|
||||
|
||||
@@ -25,10 +25,6 @@ pub(crate) async fn third_party_route(
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
let mut threepids = vec![];
|
||||
|
||||
if !services.threepid.email_requirement().may_view() {
|
||||
return Ok(get_3pids::v3::Response::new(vec![]));
|
||||
}
|
||||
|
||||
if let Some(email) = services
|
||||
.threepid
|
||||
.get_email_for_localpart(sender_user.localpart())
|
||||
|
||||
@@ -12,18 +12,20 @@ pub(crate) async fn get_lock_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_locked::v1::Request>,
|
||||
) -> Result<is_user_locked::v1::Response> {
|
||||
let (admin, status) = join(
|
||||
let (admin, active) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only check the lock status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
Ok(is_user_locked::v1::Response::new(
|
||||
services.users.is_locked(&body.user_id).await?,
|
||||
))
|
||||
@@ -38,9 +40,9 @@ pub(crate) async fn put_lock_status(
|
||||
) -> Result<lock_user::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
|
||||
let (sender_admin, status, target_admin) = join3(
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
services.users.is_admin(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
@@ -48,17 +50,18 @@ pub(crate) async fn put_lock_status(
|
||||
if !sender_admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only set the lock status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
if body.user_id == *sender_user {
|
||||
return Err!(Request(Forbidden("You cannot lock yourself")));
|
||||
}
|
||||
|
||||
if target_admin {
|
||||
return Err!(Request(Forbidden("You cannot lock another server administrator")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&body.user_id).await? == body.locked {
|
||||
// No change
|
||||
return Ok(lock_user::v1::Response::new(body.locked));
|
||||
|
||||
@@ -12,18 +12,20 @@ pub(crate) async fn get_suspended_status(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<is_user_suspended::v1::Request>,
|
||||
) -> Result<is_user_suspended::v1::Response> {
|
||||
let (admin, status) = join(
|
||||
let (admin, active) = join(
|
||||
services.users.is_admin(body.identity.expect_sender_user()?),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
|
||||
if !admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only check the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
Ok(is_user_suspended::v1::Response::new(
|
||||
services.users.is_suspended(&body.user_id).await?,
|
||||
))
|
||||
@@ -38,9 +40,9 @@ pub(crate) async fn put_suspended_status(
|
||||
) -> Result<suspend_user::v1::Response> {
|
||||
let sender_user = body.identity.expect_sender_user()?;
|
||||
|
||||
let (sender_admin, status, target_admin) = join3(
|
||||
let (sender_admin, active, target_admin) = join3(
|
||||
services.users.is_admin(sender_user),
|
||||
services.users.status(&body.user_id),
|
||||
services.users.is_active(&body.user_id),
|
||||
services.users.is_admin(&body.user_id),
|
||||
)
|
||||
.await;
|
||||
@@ -48,17 +50,18 @@ pub(crate) async fn put_suspended_status(
|
||||
if !sender_admin {
|
||||
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
|
||||
}
|
||||
|
||||
status.ensure_active()?;
|
||||
|
||||
if !services.globals.user_is_local(&body.user_id) {
|
||||
return Err!(Request(InvalidParam("Can only set the suspended status of local users")));
|
||||
}
|
||||
if !active {
|
||||
return Err!(Request(NotFound("Unknown user")));
|
||||
}
|
||||
if body.user_id == *sender_user {
|
||||
return Err!(Request(Forbidden("You cannot suspend yourself")));
|
||||
}
|
||||
|
||||
if target_admin {
|
||||
return Err!(Request(Forbidden("You cannot suspend another server administrator")));
|
||||
}
|
||||
|
||||
if services.users.is_suspended(&body.user_id).await? == body.suspended {
|
||||
// No change
|
||||
return Ok(suspend_user::v1::Response::new(body.suspended));
|
||||
|
||||
@@ -7,11 +7,10 @@
|
||||
api::client::discovery::get_capabilities::{
|
||||
self,
|
||||
v3::{
|
||||
Capabilities, GetLoginTokenCapability, ProfileFieldsCapability, RoomVersionStability,
|
||||
RoomVersionsCapability, ThirdPartyIdChangesCapability,
|
||||
Capabilities, GetLoginTokenCapability, RoomVersionStability, RoomVersionsCapability,
|
||||
ThirdPartyIdChangesCapability,
|
||||
},
|
||||
},
|
||||
assign,
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
@@ -51,9 +50,5 @@ pub(crate) async fn get_capabilities_route(
|
||||
capabilities.account_moderation.suspend = true;
|
||||
}
|
||||
|
||||
capabilities.profile_fields = Some(
|
||||
assign!(ProfileFieldsCapability::new(true), { disallowed: Some(services.oidc.restricted_profile_fields()) }),
|
||||
);
|
||||
|
||||
Ok(get_capabilities::v3::Response::new(capabilities))
|
||||
}
|
||||
|
||||
@@ -170,7 +170,9 @@ pub async fn leave_room(
|
||||
locally."
|
||||
);
|
||||
|
||||
// return the existing leave state, if one exists
|
||||
// return the existing leave state, if one exists. `mark_as_left` will then
|
||||
// update the `roomuserid_leftcount` table, making the leave come down sync
|
||||
// again.
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
@@ -205,8 +207,6 @@ pub async fn leave_room(
|
||||
.update_joined_count(room_id)
|
||||
.await;
|
||||
|
||||
services.sync.wake(user_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||
.layer(middleware::from_fn_with_state(
|
||||
state,
|
||||
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
|
||||
if state.config.oauth.compatibility_mode().oauth_available() {
|
||||
if state.config.oauth.compatibility_mode.oauth_available() {
|
||||
next.run(request).await
|
||||
} else {
|
||||
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
|
||||
|
||||
@@ -21,7 +21,7 @@ pub(crate) async fn get_authorization_server_metadata_route(
|
||||
State(services): State<crate::State>,
|
||||
_body: Ruma<get_authorization_server_metadata::v1::Request>,
|
||||
) -> Result<get_authorization_server_metadata::v1::Response> {
|
||||
if !services.config.oauth.compatibility_mode().oauth_available() {
|
||||
if !services.config.oauth.compatibility_mode.oauth_available() {
|
||||
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
|
||||
}
|
||||
|
||||
|
||||
+289
-58
@@ -1,8 +1,9 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{Err, Result};
|
||||
use conduwuit::{Err, Result, matrix::pdu::PartialPdu, utils::to_canonical_object};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
use ruma::{
|
||||
UserId,
|
||||
api::{
|
||||
@@ -12,10 +13,11 @@
|
||||
federation,
|
||||
},
|
||||
assign,
|
||||
events::room::member::MembershipState,
|
||||
presence::PresenceState,
|
||||
profile::{ProfileFieldName, ProfileFieldValue},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use service::users::ProfileFieldChange;
|
||||
use serde_json::{Value, to_value};
|
||||
|
||||
use crate::Ruma;
|
||||
|
||||
@@ -63,24 +65,13 @@ pub(crate) async fn set_profile_field_route(
|
||||
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
|
||||
}
|
||||
|
||||
if services
|
||||
.oidc
|
||||
.restricted_profile_fields()
|
||||
.contains(&body.value.field_name())
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This profile field is controlled by your identity provider."
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_profile_field(
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Set(body.value.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
set_profile_field(
|
||||
&services,
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Set(body.value.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(set_profile_field::v3::Response::new())
|
||||
}
|
||||
@@ -103,24 +94,13 @@ pub(crate) async fn delete_profile_field_route(
|
||||
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
|
||||
}
|
||||
|
||||
if services
|
||||
.oidc
|
||||
.restricted_profile_fields()
|
||||
.contains(&body.field)
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"This profile field is controlled by your identity provider."
|
||||
)));
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.set_profile_field(
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Delete(body.field.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
set_profile_field(
|
||||
&services,
|
||||
&body.user_id,
|
||||
ProfileFieldChange::Delete(body.field.clone()),
|
||||
body.propagate_to.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(delete_profile_field::v3::Response::new())
|
||||
}
|
||||
@@ -130,8 +110,8 @@ async fn fetch_full_profile(
|
||||
user_id: &UserId,
|
||||
) -> Option<BTreeMap<String, Value>> {
|
||||
// If the user exists locally, fetch their local profile
|
||||
if services.users.status(user_id).await.is_found() {
|
||||
return Some(services.users.get_local_profile(user_id).await);
|
||||
if services.users.exists(user_id).await {
|
||||
return Some(get_local_profile(services, user_id).await);
|
||||
}
|
||||
|
||||
// Otherwise ask their homeserver
|
||||
@@ -155,10 +135,13 @@ async fn fetch_full_profile(
|
||||
continue;
|
||||
};
|
||||
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(user_id, ProfileFieldChange::Set(value), PropagateTo::None)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Some(BTreeMap::from_iter(response))
|
||||
@@ -171,7 +154,7 @@ async fn fetch_profile_field(
|
||||
) -> Result<Option<ProfileFieldValue>> {
|
||||
// If the user exists locally, fetch their local profile field
|
||||
if services.globals.user_is_local(user_id) {
|
||||
return Ok(services.users.get_local_profile_field(user_id, field).await);
|
||||
return Ok(get_local_profile_field(services, user_id, field).await);
|
||||
}
|
||||
|
||||
// Otherwise ask their homeserver
|
||||
@@ -192,14 +175,13 @@ async fn fetch_profile_field(
|
||||
|
||||
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
|
||||
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value.clone()),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Set(value.clone()),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Some(value))
|
||||
} else {
|
||||
@@ -208,11 +190,260 @@ async fn fetch_profile_field(
|
||||
)))
|
||||
}
|
||||
} else {
|
||||
let _ = services
|
||||
.users
|
||||
.set_profile_field(user_id, ProfileFieldChange::Delete(field), PropagateTo::None)
|
||||
.await;
|
||||
let _ = set_profile_field(
|
||||
services,
|
||||
user_id,
|
||||
ProfileFieldChange::Delete(field),
|
||||
PropagateTo::None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_local_profile(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
) -> BTreeMap<String, Value> {
|
||||
let mut profile = BTreeMap::new();
|
||||
|
||||
// Get displayname and avatar_url independently because `all_profile_keys`
|
||||
// doesn't include them
|
||||
for field in [ProfileFieldName::AvatarUrl, ProfileFieldName::DisplayName] {
|
||||
let key = field.as_str().to_owned();
|
||||
|
||||
if let Some(value) = get_local_profile_field(services, user_id, field).await {
|
||||
profile.insert(key, value.value().into_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// Insert all other profile fields
|
||||
let mut all_fields = services.users.all_profile_keys(user_id);
|
||||
|
||||
while let Some((key, value)) = all_fields.next().await {
|
||||
profile.insert(key, value);
|
||||
}
|
||||
|
||||
profile
|
||||
}
|
||||
|
||||
pub(crate) async fn get_local_profile_field(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
field: ProfileFieldName,
|
||||
) -> Option<ProfileFieldValue> {
|
||||
let value = match field.clone() {
|
||||
| ProfileFieldName::AvatarUrl => services
|
||||
.users
|
||||
.avatar_url(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(to_value)
|
||||
.transpose()
|
||||
.expect("converting avatar url to value should succeed"),
|
||||
| ProfileFieldName::DisplayName => services
|
||||
.users
|
||||
.displayname(user_id)
|
||||
.await
|
||||
.ok()
|
||||
.map(to_value)
|
||||
.transpose()
|
||||
.expect("converting displayname to value should succeed"),
|
||||
| other => services
|
||||
.users
|
||||
.profile_key(user_id, other.as_str())
|
||||
.await
|
||||
.ok(),
|
||||
}?;
|
||||
|
||||
Some(
|
||||
ProfileFieldValue::new(field.as_str(), value)
|
||||
.expect("local profile field should be valid"),
|
||||
)
|
||||
}
|
||||
|
||||
enum ProfileFieldChange {
|
||||
Set(ProfileFieldValue),
|
||||
Delete(ProfileFieldName),
|
||||
}
|
||||
|
||||
impl ProfileFieldChange {
|
||||
fn field_name(&self) -> ProfileFieldName {
|
||||
match self {
|
||||
| &Self::Delete(ref name) => name.clone(),
|
||||
| &Self::Set(ref value) => value.field_name(),
|
||||
}
|
||||
}
|
||||
|
||||
fn value(&self) -> Option<Value> {
|
||||
if let Self::Set(value) = self {
|
||||
Some(value.value().into_owned())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_profile_field(
|
||||
services: &Services,
|
||||
user_id: &UserId,
|
||||
change: ProfileFieldChange,
|
||||
propagate_to: PropagateTo,
|
||||
) -> Result<()> {
|
||||
const MAX_KEY_LENGTH_BYTES: usize = 255;
|
||||
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
|
||||
|
||||
let field_name = change.field_name();
|
||||
|
||||
// TODO: The spec mentions special error codes (M_PROFILE_TOO_LARGE,
|
||||
// M_KEY_TOO_LARGE) for profile field size limits, but they're not in its list
|
||||
// of error codes and Ruma doesn't have them. Should we return those, or is
|
||||
// M_TOO_LARGE okay?
|
||||
if field_name.as_str().len() > MAX_KEY_LENGTH_BYTES {
|
||||
return Err!(Request(TooLarge(
|
||||
"Individual profile keys must not exceed {MAX_KEY_LENGTH_BYTES} bytes in length."
|
||||
)));
|
||||
}
|
||||
|
||||
// Serialize the entire profile as canonical JSON, including the new change,
|
||||
// to check if it exceeds 64 KiB
|
||||
{
|
||||
let mut full_profile = get_local_profile(services, user_id).await;
|
||||
|
||||
match &change {
|
||||
| ProfileFieldChange::Set(value) => {
|
||||
full_profile.insert(
|
||||
value.field_name().as_str().to_owned(),
|
||||
value.value().clone().into_owned(),
|
||||
);
|
||||
},
|
||||
| ProfileFieldChange::Delete(key) => {
|
||||
full_profile.remove(key.as_str());
|
||||
},
|
||||
}
|
||||
|
||||
if let Ok(canonical_profile) = to_canonical_object(full_profile) {
|
||||
if serde_json::to_string(&canonical_profile)
|
||||
.expect("should be able to serialize to string")
|
||||
.len() > MAX_PROFILE_LENGTH_BYTES
|
||||
{
|
||||
return Err!(
|
||||
"Profile data must not exceed {MAX_PROFILE_LENGTH_BYTES} bytes in length."
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return Err!(Request(BadJson("Failed to canonicalize profile.")));
|
||||
}
|
||||
}
|
||||
|
||||
// If the user is local and changed their displayname or avatar_url, update it
|
||||
// in all their joined rooms. This is done before updating their profile data
|
||||
// so we can check the old value of the field if `propagate_to` is `unchanged`.
|
||||
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
|
||||
&& matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged)
|
||||
&& services.globals.user_is_local(user_id)
|
||||
{
|
||||
let current_displayname = services.users.displayname(user_id).await.ok();
|
||||
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
|
||||
|
||||
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
|
||||
|
||||
while let Some(room_id) = all_joined_rooms.next().await {
|
||||
// TODO: this clobbers any custom fields on the event content
|
||||
let mut current_membership = services
|
||||
.rooms
|
||||
.state_accessor
|
||||
.get_member(&room_id, user_id)
|
||||
.await
|
||||
.expect("should be able to fetch membership event for joined room");
|
||||
|
||||
assert_eq!(
|
||||
current_membership.membership,
|
||||
MembershipState::Join,
|
||||
"user should be joined"
|
||||
);
|
||||
|
||||
// If `propagate_to` is `unchanged`, and the current value of the field we're
|
||||
// updating was changed from its global value in this room, skip it.
|
||||
if matches!(propagate_to, PropagateTo::Unchanged) {
|
||||
let field_changed_from_global = match field_name {
|
||||
| ProfileFieldName::AvatarUrl =>
|
||||
current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(),
|
||||
| ProfileFieldName::DisplayName =>
|
||||
current_membership.displayname.as_ref() != current_displayname.as_ref(),
|
||||
| _ => unreachable!(),
|
||||
};
|
||||
|
||||
if field_changed_from_global {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
|
||||
|
||||
// Preserve keys in accordance with the key copying rules
|
||||
current_membership.reason = None;
|
||||
current_membership.join_authorized_via_users_server = None;
|
||||
match &change {
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
||||
current_membership.avatar_url = Some(avatar_url.clone());
|
||||
},
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
||||
current_membership.displayname = Some(displayname.clone());
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
||||
current_membership.avatar_url = None;
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
||||
current_membership.displayname = None;
|
||||
},
|
||||
| _ => unreachable!(),
|
||||
}
|
||||
|
||||
let _ = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PartialPdu::state(user_id.to_string(), ¤t_membership),
|
||||
user_id,
|
||||
Some(&room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if services.config.allow_local_presence {
|
||||
// Send a presence EDU to indicate the profile changed
|
||||
let _ = services
|
||||
.presence
|
||||
.ping_presence(user_id, &PresenceState::Online)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
match change {
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
|
||||
services
|
||||
.users
|
||||
.set_displayname(user_id, Some(displayname).filter(|dn| !dn.is_empty()));
|
||||
},
|
||||
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
|
||||
services
|
||||
.users
|
||||
.set_avatar_url(user_id, Some(avatar_url).filter(|av| av.is_valid()));
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
|
||||
services.users.set_displayname(user_id, None);
|
||||
},
|
||||
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
|
||||
services.users.set_avatar_url(user_id, None);
|
||||
},
|
||||
| other =>
|
||||
services
|
||||
.users
|
||||
.set_profile_key(user_id, other.field_name().as_str(), other.value()),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -107,8 +107,6 @@ pub(crate) async fn set_read_marker_route(
|
||||
.private_read_set(&body.room_id, sender_user, count);
|
||||
}
|
||||
|
||||
services.sync.wake(sender_user).await;
|
||||
|
||||
Ok(set_read_marker::v3::Response::new())
|
||||
}
|
||||
|
||||
@@ -211,7 +209,5 @@ pub(crate) async fn create_receipt_route(
|
||||
},
|
||||
}
|
||||
|
||||
services.sync.wake(sender_user).await;
|
||||
|
||||
Ok(create_receipt::v3::Response::new())
|
||||
}
|
||||
|
||||
@@ -149,7 +149,7 @@ pub(crate) async fn report_user_route(
|
||||
|
||||
delay_response().await;
|
||||
|
||||
if !services.users.status(&body.user_id).await.is_found() {
|
||||
if !services.users.is_active_local(&body.user_id).await {
|
||||
// return 200 as to not reveal if the user exists. Recommended by spec.
|
||||
return Ok(report_user::v3::Response::new());
|
||||
}
|
||||
|
||||
@@ -337,21 +337,14 @@ pub(crate) async fn create_room_route(
|
||||
});
|
||||
|
||||
let mut power_levels_to_grant = BTreeMap::from_iter([(sender_user.to_owned(), int!(100))]);
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
|
||||
if preset == RoomPreset::TrustedPrivateChat {
|
||||
for recipient_user in invitees.iter().cloned() {
|
||||
if room_version_rules
|
||||
.authorization
|
||||
.explicitly_privilege_room_creators
|
||||
{
|
||||
creators.push(recipient_user);
|
||||
} else {
|
||||
power_levels_to_grant.insert(recipient_user, int!(100));
|
||||
}
|
||||
for recipient_user in &invitees {
|
||||
power_levels_to_grant.insert(recipient_user.clone(), int!(100));
|
||||
}
|
||||
}
|
||||
|
||||
let mut creators: Vec<OwnedUserId> = vec![sender_user.to_owned()];
|
||||
// Do we care about additional_creators?
|
||||
if room_version_rules
|
||||
.authorization
|
||||
|
||||
@@ -43,7 +43,7 @@ pub(crate) async fn get_login_types_route(
|
||||
ClientIp(client): ClientIp,
|
||||
_body: Ruma<get_login_types::v3::Request>,
|
||||
) -> Result<get_login_types::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
||||
return Err!(Request(Unrecognized(
|
||||
"User-interactive authentication is not available on this server."
|
||||
)));
|
||||
@@ -88,6 +88,14 @@ pub async fn handle_login(
|
||||
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
|
||||
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
|
||||
}
|
||||
|
||||
if services.users.is_deactivated(&user_id).await? {
|
||||
return Err!(Request(UserDeactivated("This account has been deactivated.")));
|
||||
}
|
||||
|
||||
if services.users.is_locked(&user_id).await? {
|
||||
return Err!(Request(UserLocked("This account has been locked.")));
|
||||
}
|
||||
@@ -120,7 +128,7 @@ pub(crate) async fn login_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<login::v3::Request>,
|
||||
) -> Result<login::v3::Response> {
|
||||
if !services.config.oauth.compatibility_mode().uiaa_available() {
|
||||
if !services.config.oauth.compatibility_mode.uiaa_available() {
|
||||
return match body.login_info {
|
||||
| LoginInfo::ApplicationService(_) => {
|
||||
Err!(Request(AppserviceLoginUnsupported(
|
||||
|
||||
@@ -201,6 +201,9 @@ pub(crate) async fn sync_events_route(
|
||||
.update_device_last_seen(sender_user, Some(sender_device), client_ip)
|
||||
.await;
|
||||
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = services.sync.watch(sender_user, sender_device);
|
||||
|
||||
let response = build_sync_events(&services, &body).await?;
|
||||
if body.body.full_state
|
||||
|| !(response.rooms.is_empty()
|
||||
@@ -216,7 +219,7 @@ pub(crate) async fn sync_events_route(
|
||||
// Stop hanging if new info arrives
|
||||
let default = Duration::from_secs(30);
|
||||
let duration = cmp::min(body.body.timeout.unwrap_or(default), default);
|
||||
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
|
||||
_ = tokio::time::timeout(duration, watcher).await;
|
||||
|
||||
// Retry returning data
|
||||
build_sync_events(&services, &body).await
|
||||
|
||||
@@ -80,6 +80,9 @@ pub(crate) async fn sync_events_v5_route(
|
||||
|
||||
let mut body = body.body;
|
||||
|
||||
// Setup watchers, so if there's no response, we can wait for them
|
||||
let watcher = services.sync.watch(sender_user, sender_device);
|
||||
|
||||
let next_batch = services.globals.next_count()?;
|
||||
|
||||
let conn_id = body.conn_id.clone();
|
||||
@@ -217,7 +220,7 @@ pub(crate) async fn sync_events_v5_route(
|
||||
// Stop hanging if new info arrives
|
||||
let default = Duration::from_secs(30);
|
||||
let duration = cmp::min(body.timeout.unwrap_or(default), default);
|
||||
_ = tokio::time::timeout(duration, services.sync.wait_for_wake(sender_user)).await;
|
||||
_ = tokio::time::timeout(duration, watcher).await;
|
||||
}
|
||||
|
||||
let typing = collect_typing_events(services, sender_user, &body, &todo_rooms).await?;
|
||||
|
||||
@@ -29,26 +29,26 @@ pub(crate) async fn get_supported_versions_route(
|
||||
))
|
||||
}
|
||||
|
||||
/// # `GET /_continuwuity/server_version`
|
||||
/// # `GET /_conduwuit/server_version`
|
||||
///
|
||||
/// Conduwuit-specific API to get the server version, results akin to
|
||||
/// `/_matrix/federation/v1/version`
|
||||
pub(crate) async fn continuwuity_server_version() -> Result<impl IntoResponse> {
|
||||
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
|
||||
Ok(Json(serde_json::json!({
|
||||
"name": conduwuit::BRANDING,
|
||||
"version": conduwuit::version(),
|
||||
})))
|
||||
}
|
||||
|
||||
/// # `GET /_continuwuity/local_user_count`
|
||||
/// # `GET /_conduwuit/local_user_count`
|
||||
///
|
||||
/// conduwuit-specific API to return the amount of users registered on this
|
||||
/// homeserver. Endpoint is disabled if federation is disabled for privacy. This
|
||||
/// only includes active users (not deactivated, etc)
|
||||
pub(crate) async fn continuwuity_local_user_count(
|
||||
pub(crate) async fn conduwuit_local_user_count(
|
||||
State(services): State<crate::State>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
let user_count = services.users.stream_local_users().count().await;
|
||||
let user_count = services.users.list_local_users().count().await;
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"count": user_count
|
||||
|
||||
@@ -32,26 +32,22 @@ pub(crate) async fn search_users_route(
|
||||
.min(LIMIT_MAX);
|
||||
|
||||
let search_term = body.search_term.to_lowercase();
|
||||
let mut users = services.users.stream().broad_filter_map(async |user_id| {
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
|
||||
let mut users = services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.chain(services.users.stream_remote_users())
|
||||
.broad_filter_map(async |user_id| {
|
||||
let display_name = services.users.displayname(&user_id).await.ok();
|
||||
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
|
||||
|
||||
let user_id_matches = user_id.as_str().to_lowercase().contains(&search_term);
|
||||
let display_name_matches = display_name
|
||||
.as_deref()
|
||||
.map(str::to_lowercase)
|
||||
.is_some_and(|display_name| display_name.contains(&search_term));
|
||||
|
||||
let display_name_matches = display_name
|
||||
.as_deref()
|
||||
.map(str::to_lowercase)
|
||||
.is_some_and(|display_name| display_name.contains(&search_term));
|
||||
if !user_id_matches && !display_name_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
if !user_id_matches && !display_name_matches {
|
||||
return None;
|
||||
}
|
||||
|
||||
let user_in_public_room = services
|
||||
let user_in_public_room =
|
||||
services
|
||||
.rooms
|
||||
.state_cache
|
||||
.rooms_joined(&user_id)
|
||||
@@ -64,22 +60,22 @@ pub(crate) async fn search_users_route(
|
||||
.await
|
||||
});
|
||||
|
||||
let user_sees_user = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.user_sees_user(sender_user, &user_id);
|
||||
let user_sees_user = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.user_sees_user(sender_user, &user_id);
|
||||
|
||||
pin_mut!(user_in_public_room, user_sees_user);
|
||||
pin_mut!(user_in_public_room, user_sees_user);
|
||||
|
||||
if user_in_public_room.or(user_sees_user).await {
|
||||
Some(assign!(search_users::v3::User::new(user_id.clone()), {
|
||||
display_name,
|
||||
avatar_url: services.users.avatar_url(&user_id).await.ok(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if user_in_public_room.or(user_sees_user).await {
|
||||
Some(assign!(search_users::v3::User::new(user_id.clone()), {
|
||||
display_name,
|
||||
avatar_url: services.users.avatar_url(&user_id).await.ok(),
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let results = users.by_ref().take(limit).collect().await;
|
||||
let limited = users.next().await.is_some();
|
||||
|
||||
+5
-2
@@ -190,7 +190,8 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&client::room_initial_sync_route)
|
||||
.ruma_route(&client::get_authorization_server_metadata_route)
|
||||
.merge(client::oauth::router(state))
|
||||
.route("/_continuwuity/server_version", get(client::continuwuity_server_version))
|
||||
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
|
||||
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
|
||||
.ruma_route(&admin::rooms::ban::ban_room)
|
||||
.ruma_route(&admin::rooms::list::list_rooms);
|
||||
|
||||
@@ -228,12 +229,14 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
|
||||
.ruma_route(&server::well_known_server)
|
||||
.ruma_route(&server::get_content_route)
|
||||
.ruma_route(&server::get_content_thumbnail_route)
|
||||
.route("/_continuwuity/local_user_count", get(client::continuwuity_local_user_count));
|
||||
.route("/_conduwuit/local_user_count", get(client::conduwuit_local_user_count))
|
||||
.route("/_continuwuity/local_user_count", get(client::conduwuit_local_user_count));
|
||||
} else {
|
||||
router = router
|
||||
.route("/_matrix/federation/{*path}", any(federation_disabled))
|
||||
.route("/.well-known/matrix/server", any(federation_disabled))
|
||||
.route("/_matrix/key/{*path}", any(federation_disabled))
|
||||
.route("/_conduwuit/local_user_count", any(federation_disabled))
|
||||
.route("/_continuwuity/local_user_count", any(federation_disabled));
|
||||
}
|
||||
|
||||
|
||||
@@ -60,9 +60,8 @@ pub(crate) fn sender_device(&self) -> Option<&DeviceId> {
|
||||
|
||||
pub(crate) fn expect_sender_device(&self) -> Result<&DeviceId> {
|
||||
match self {
|
||||
| Self::User { sender_device, .. }
|
||||
| Self::Appservice { sender_device: Some(sender_device), .. } => Ok(sender_device),
|
||||
| Self::Appservice { sender_device: None, .. } =>
|
||||
| Self::User { sender_device, .. } => Ok(sender_device),
|
||||
| Self::Appservice { .. } =>
|
||||
Err!(Request(Forbidden("Appservices must masquerade to use this endpoint."))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -203,8 +203,6 @@ pub(crate) async fn create_invite_route(
|
||||
.update_joined_count(&body.room_id)
|
||||
.await;
|
||||
|
||||
services.sync.wake(&recipient_user).await;
|
||||
|
||||
for appservice in services.appservice.read().await.values() {
|
||||
if appservice.is_user_match(&recipient_user) {
|
||||
let transaction_id = general_purpose::URL_SAFE_NO_PAD
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
api::federation::query::{get_profile_information, get_room_information},
|
||||
};
|
||||
|
||||
use crate::Ruma;
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::{get_local_profile, get_local_profile_field},
|
||||
};
|
||||
|
||||
/// # `GET /_matrix/federation/v1/query/directory`
|
||||
///
|
||||
@@ -72,19 +75,15 @@ pub(crate) async fn get_profile_information_route(
|
||||
let response = if let Some(field) = &body.field {
|
||||
let mut response = get_profile_information::v1::Response::new();
|
||||
|
||||
if let Some(value) = services
|
||||
.users
|
||||
.get_local_profile_field(&body.user_id, field.to_owned())
|
||||
.await
|
||||
if let Some(value) =
|
||||
get_local_profile_field(&services, &body.user_id, field.to_owned()).await
|
||||
{
|
||||
response.set(value.field_name().as_str().to_owned(), value.value().into_owned());
|
||||
}
|
||||
|
||||
response
|
||||
} else {
|
||||
services
|
||||
.users
|
||||
.get_local_profile(&body.user_id)
|
||||
get_local_profile(&services, &body.user_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.collect()
|
||||
|
||||
@@ -620,9 +620,8 @@ async fn handle_edu_direct_to_device(
|
||||
.broad_filter_map(|(target_user_id, map)| async move {
|
||||
services
|
||||
.users
|
||||
.status(&target_user_id)
|
||||
.is_active_local(&target_user_id)
|
||||
.await
|
||||
.is_active()
|
||||
.then_some((target_user_id, map))
|
||||
})
|
||||
.for_each_concurrent(automatic_width(), |(target_user_id, map)| {
|
||||
|
||||
@@ -118,8 +118,6 @@ url.workspace = true
|
||||
parking_lot.workspace = true
|
||||
lock_api.workspace = true
|
||||
hyper-util.workspace = true
|
||||
resolvematrix.workspace = true
|
||||
openidconnect.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
nix.workspace = true
|
||||
|
||||
@@ -289,15 +289,6 @@ pub fn check(config: &Config) -> Result {
|
||||
| _ => (),
|
||||
}
|
||||
|
||||
if let Some(oidc) = &config.oauth.oidc
|
||||
&& oidc.client_secret.is_none()
|
||||
&& oidc.client_secret_file.is_none()
|
||||
{
|
||||
return Err!(
|
||||
"Either `client_secret` or `client_secret_file` must be set if OIDC is configured."
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+3
-140
@@ -4,7 +4,7 @@
|
||||
pub mod proxy;
|
||||
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap},
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr},
|
||||
path::PathBuf,
|
||||
};
|
||||
@@ -17,12 +17,10 @@
|
||||
use figment::providers::{Env, Format, Toml};
|
||||
pub use figment::{Figment, value::Value as FigmentValue};
|
||||
use lettre::message::Mailbox;
|
||||
use openidconnect::{ClientId, ClientSecret, Scope};
|
||||
use regex::RegexSet;
|
||||
use ruma::{
|
||||
OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
|
||||
api::client::{discovery::discover_support::ContactRole, rtc::RtcTransport},
|
||||
profile::ProfileFieldName,
|
||||
serde::Base64,
|
||||
};
|
||||
use serde::{Deserialize, Serialize, de::IgnoredAny};
|
||||
@@ -2421,24 +2419,10 @@ pub struct OauthConfig {
|
||||
/// legacy authentication will be unable to log in.
|
||||
///
|
||||
/// default: "hybrid"
|
||||
compatibility_mode: OAuthMode,
|
||||
|
||||
/// display: hidden
|
||||
pub oidc: Option<OidcConfig>,
|
||||
pub compatibility_mode: OAuthMode,
|
||||
}
|
||||
|
||||
impl OauthConfig {
|
||||
#[must_use]
|
||||
pub fn compatibility_mode(&self) -> OAuthMode {
|
||||
if self.oidc.is_some() {
|
||||
OAuthMode::Exclusive
|
||||
} else {
|
||||
self.compatibility_mode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Deserialize)]
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OAuthMode {
|
||||
Disabled,
|
||||
@@ -2455,121 +2439,6 @@ pub fn uiaa_available(&self) -> bool { matches!(self, Self::Disabled | Self::Hyb
|
||||
pub fn oauth_available(&self) -> bool { matches!(self, Self::Hybrid | Self::Exclusive) }
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[config_example_generator(
|
||||
filename = "conduwuit-example.toml",
|
||||
section = "global.oauth.oidc",
|
||||
optional = "true",
|
||||
subheader = "\
|
||||
# Uncommenting this section will enable Continuwuity's support for
|
||||
# authenticating users using an OpenID Connect-compatible identity provider.
|
||||
# This is referred to as \"delegated authentication\".
|
||||
#
|
||||
# IMPORTANT NOTE: When delegated authentication is active, Continuwuity will behave as if
|
||||
# the `global.oauth.compatibility_mode` setting is set to `exclusive`.
|
||||
# Matrix clients which do not support OAuth login (also referred to as \"next-gen auth\") will \
|
||||
NOT be able
|
||||
# to log in while delegated authentication is active."
|
||||
)]
|
||||
pub struct OidcConfig {
|
||||
/// The OIDC issuer URL. Continuwuity will use OpenID Connect Discovery to
|
||||
/// automatically fetch the identity provider's metadata from this URL.
|
||||
/// Generally you should set this to the base domain your identity provider
|
||||
/// runs on.
|
||||
pub discovery_url: Url,
|
||||
|
||||
/// The OAuth client ID for Continuwuity to use when communicating with the
|
||||
/// identity provider.
|
||||
pub client_id: ClientId,
|
||||
|
||||
/// The OAuth client secret for Continuwuity to use when communicating with
|
||||
/// the identity provider.
|
||||
pub client_secret: Option<ClientSecret>,
|
||||
|
||||
/// A path to a file which Continuwuity will read the client secret from.
|
||||
/// If this option is set, it will override `client_secret`.
|
||||
///
|
||||
/// The server will fail to start if the file cannot be read.
|
||||
pub client_secret_file: Option<PathBuf>,
|
||||
|
||||
/// Additional scopes Continuwuity should request from the IDP. This may be
|
||||
/// necessary to access certain claims. Continuwuity always requests the
|
||||
/// `openid` scope.
|
||||
///
|
||||
/// default: []
|
||||
#[serde(default)]
|
||||
pub additional_scopes: Vec<Scope>,
|
||||
|
||||
/// Whether the user should be prompted to choose a localpart
|
||||
/// when signing in for the first time. If this is `false`, Continuwuity
|
||||
/// will attempt to use the value of the `preferred_username_claim`
|
||||
/// (see below) as the user's localpart. Authentication will
|
||||
/// fail if this claim is missing or is not a valid localpart.
|
||||
///
|
||||
/// default: true
|
||||
#[serde(default = "true_fn")]
|
||||
pub prompt_for_localpart: bool,
|
||||
|
||||
/// The claim to use for the user's localpart, if `prompt_for_localpart` is
|
||||
/// false.
|
||||
///
|
||||
/// default: "preferred_username"
|
||||
#[serde(default = "default_preferred_username_claim")]
|
||||
pub preferred_username_claim: String,
|
||||
|
||||
/// The claim which will be used to set the user's email address,
|
||||
/// either on initial registration or on every login depending on
|
||||
/// the value of `profile_key_import_mode`. Continuwuity assumes that
|
||||
/// the IDP has taken care of verifying that the user controls the email
|
||||
/// address it provides.
|
||||
///
|
||||
/// This option does nothing if SMTP is not configured.
|
||||
///
|
||||
/// If this option is set, and `profile_key_import_mode` is `on_login`,
|
||||
/// users will not be able to change their email addresses themselves.
|
||||
///
|
||||
/// default: "email"
|
||||
pub email_claim: Option<String>,
|
||||
|
||||
/// Defines how claims returned from the IDP should be mapped to a user's
|
||||
/// profile data. The profile field named in each key will be set from the
|
||||
/// claim named in the corresponding value when the user first registers,
|
||||
/// and possibly on subsequent logins as well, depending on the value of
|
||||
/// `profile_key_import_mode` (see below).
|
||||
///
|
||||
/// Per-room overrides to the user's display name or avatar will be
|
||||
/// preserved by the import process.
|
||||
///
|
||||
/// SECURITY NOTE: If the `avatar_url` field is set, Continuwuity will
|
||||
/// perform a HTTP GET to the URL in the mapped claim and use the returned
|
||||
/// file as the user's profile picture. Make sure your users are not able
|
||||
/// to set the value of the mapped claim to an arbitrary URL.
|
||||
///
|
||||
/// default: { displayname = "name" }
|
||||
#[serde(default = "default_profile_key_map")]
|
||||
pub profile_key_map: HashMap<String, String>,
|
||||
|
||||
/// When profile keys should be imported from the IDP's claims.
|
||||
///
|
||||
/// - "on_registration": Listed keys will be imported once, when the user
|
||||
/// logs in for the first time and their shadow account is created.
|
||||
/// - "on_login": Listed keys will be imported every time the user logs in.
|
||||
/// Additionally, users will not be able to manually edit any listed keys
|
||||
/// through their Matrix client.
|
||||
///
|
||||
/// default: "on_registration"
|
||||
#[serde(default)]
|
||||
pub profile_key_import_mode: OidcProfileKeyImportMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum OidcProfileKeyImportMode {
|
||||
#[default]
|
||||
OnRegistration,
|
||||
OnLogin,
|
||||
}
|
||||
|
||||
const DEPRECATED_KEYS: &[&str] = &[
|
||||
"cache_capacity",
|
||||
"conduit_cache_capacity_modifier",
|
||||
@@ -2954,9 +2823,3 @@ fn default_client_shutdown_timeout() -> u64 { 15 }
|
||||
fn default_sender_shutdown_timeout() -> u64 { 5 }
|
||||
|
||||
fn default_terms_language() -> String { "en".to_owned() }
|
||||
|
||||
fn default_preferred_username_claim() -> String { "preferred_username".to_owned() }
|
||||
|
||||
fn default_profile_key_map() -> HashMap<String, String> {
|
||||
HashMap::from_iter([(ProfileFieldName::DisplayName.to_string(), "name".to_owned())])
|
||||
}
|
||||
|
||||
@@ -87,8 +87,6 @@ pub enum Error {
|
||||
YamlDe(#[from] serde_saphyr::Error),
|
||||
#[error(transparent)]
|
||||
YamlSer(#[from] serde_saphyr::ser_error::Error),
|
||||
#[error(transparent)]
|
||||
ResolveServer(#[from] resolvematrix::error::ResolveServerError),
|
||||
|
||||
// ruma/conduwuit
|
||||
#[error("Arithmetic operation failed: {0}")]
|
||||
@@ -119,6 +117,8 @@ pub enum Error {
|
||||
Mxc(#[from] ruma::MxcUriError),
|
||||
#[error(transparent)]
|
||||
Mxid(#[from] ruma::IdParseError),
|
||||
#[error("from {0}: {1}")]
|
||||
Redaction(ruma::OwnedServerName, ruma::canonical_json::RedactionError),
|
||||
#[error("{0:?}: {1}")]
|
||||
Request(ErrorKind, Cow<'static, str>, http::StatusCode),
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeSet;
|
||||
use std::{borrow::Borrow, collections::BTreeSet};
|
||||
|
||||
use futures::{
|
||||
Future,
|
||||
@@ -824,7 +824,7 @@ struct GetThirdPartyInvite {
|
||||
|
||||
let prev_event_is_create_event = prev_events
|
||||
.next()
|
||||
.is_some_and(|event_id| event_id == create_room.event_id());
|
||||
.is_some_and(|event_id| event_id.borrow() == create_room.event_id().borrow());
|
||||
let no_more_prev_events = prev_events.next().is_none();
|
||||
|
||||
if prev_event_is_create_event && no_more_prev_events {
|
||||
|
||||
@@ -33,7 +33,7 @@ pub fn unstable_features() -> BTreeMap<String, bool> {
|
||||
BTreeMap::from_iter([
|
||||
// query mutual rooms (https://github.com/matrix-org/matrix-spec-proposals/pull/2666)
|
||||
// Expected for spec v1.19
|
||||
("uk.half-shot.msc2666.query_mutual_rooms".to_owned(), true),
|
||||
("uk.half-shot.msc2666.query_mutual_rooms.stable".to_owned(), true),
|
||||
// Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186)
|
||||
// Expected for spec v1.19
|
||||
("org.matrix.simplified_msc3575".to_owned(), true),
|
||||
|
||||
@@ -124,14 +124,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "onetimekeyid_onetimekeys",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "openidsubject_localpart",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "openidsubject_currentpictureurl",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "fallbackkeyid_fallbackkey",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -177,10 +169,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "registrationtoken_info",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "remoteuserid_remoteuser",
|
||||
..descriptor::RANDOM
|
||||
},
|
||||
Descriptor {
|
||||
name: "roomid_invitedcount",
|
||||
..descriptor::RANDOM_SMALL
|
||||
@@ -410,10 +398,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "userid_blurhash",
|
||||
..descriptor::DROPPED
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_deactivated",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "userid_dehydrateddevice",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
@@ -81,12 +81,6 @@ fn generate_example(input: &ItemStruct, args: &[Meta], write: bool) -> Result<To
|
||||
};
|
||||
file.write_fmt(format_args!("{section_header}"))
|
||||
.expect("written to config file");
|
||||
|
||||
if let Some(subheader) = settings.get("subheader") {
|
||||
file.write_all(subheader.as_bytes())
|
||||
.expect("written to config file");
|
||||
file.write_all(b"\n\n").expect("written to config file");
|
||||
}
|
||||
}
|
||||
|
||||
let mut summary: Vec<TokenStream2> = Vec::new();
|
||||
|
||||
@@ -119,9 +119,7 @@ recaptcha-verify = { version = "0.2.0", default-features = false }
|
||||
reqwest_recaptcha = { package = "reqwest", version = "0.12.28", default-features = false, features = ["rustls-tls-native-roots-no-provider"] } # As long as recaptcha-verify's reqwest is outdated
|
||||
yansi.workspace = true
|
||||
lettre.workspace = true
|
||||
resolvematrix.workspace = true
|
||||
serde_urlencoded.workspace = true
|
||||
openidconnect.workspace = true
|
||||
|
||||
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
|
||||
sd-notify.workspace = true
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{Dep, globals, sync};
|
||||
use crate::{Dep, globals};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AnyRawAccountDataEvent {
|
||||
@@ -36,7 +36,6 @@ struct Data {
|
||||
|
||||
struct Services {
|
||||
globals: Dep<globals::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
}
|
||||
|
||||
impl crate::Service for Service {
|
||||
@@ -44,7 +43,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
},
|
||||
db: Data {
|
||||
roomuserdataid_accountdata: args.db["roomuserdataid_accountdata"].clone(),
|
||||
@@ -86,8 +84,6 @@ pub async fn update(
|
||||
self.db.roomuserdataid_accountdata.remove(&prev);
|
||||
}
|
||||
|
||||
self.services.sync.wake(user_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ pub async fn create_admin_room(services: &Services) -> Result {
|
||||
|
||||
// Create a user for the server
|
||||
let server_user = services.globals.server_user.as_ref();
|
||||
services.users.create_shadow_account(server_user).await?;
|
||||
services.users.create(server_user, None)?;
|
||||
|
||||
let mut create_content = if room_version_rules.authorization.use_room_create_sender {
|
||||
RoomCreateEventContent::new_v1(server_user.into())
|
||||
|
||||
@@ -108,17 +108,17 @@ async fn start_appservice(&self, id: String, registration: Registration) -> Resu
|
||||
self.services.globals.server_name(),
|
||||
)?;
|
||||
|
||||
if !self
|
||||
if !self.services.users.exists(&appservice_user_id).await {
|
||||
self.services.users.create(&appservice_user_id, None)?;
|
||||
} else if self
|
||||
.services
|
||||
.users
|
||||
.status(&appservice_user_id)
|
||||
.is_deactivated(&appservice_user_id)
|
||||
.await
|
||||
.is_found()
|
||||
.unwrap_or(false)
|
||||
{
|
||||
self.services
|
||||
.users
|
||||
.create_shadow_account(&appservice_user_id)
|
||||
.await?;
|
||||
// Reactivate the appservice user if it was accidentally deactivated
|
||||
self.services.users.set_password(&appservice_user_id, None);
|
||||
}
|
||||
|
||||
self.registration_info
|
||||
|
||||
@@ -11,6 +11,7 @@ pub struct Service {
|
||||
pub default: reqwest::Client,
|
||||
pub url_preview: reqwest::Client,
|
||||
pub extern_media: reqwest::Client,
|
||||
pub well_known: reqwest::Client,
|
||||
pub federation: reqwest::Client,
|
||||
pub federation_slow: reqwest::Client,
|
||||
pub sender: reqwest::Client,
|
||||
@@ -42,7 +43,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
|
||||
Ok(Arc::new(Self {
|
||||
default: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.clone())
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.build()?,
|
||||
|
||||
url_preview: base(config)
|
||||
@@ -50,19 +51,28 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
builder_interface(builder, url_preview_bind_iface.as_deref())
|
||||
})?
|
||||
.local_address(url_preview_bind_addr)
|
||||
.dns_resolver(resolver.dns.resolver.clone())
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.timeout(Duration::from_secs(config.url_preview_timeout))
|
||||
.redirect(redirect::Policy::limited(3))
|
||||
.user_agent(url_preview_user_agent)
|
||||
.build()?,
|
||||
|
||||
extern_media: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.clone())
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.redirect(redirect::Policy::limited(3))
|
||||
.build()?,
|
||||
|
||||
well_known: base(config)?
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.connect_timeout(Duration::from_secs(config.well_known_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(config.well_known_timeout))
|
||||
.timeout(Duration::from_secs(config.well_known_timeout))
|
||||
.pool_max_idle_per_host(0)
|
||||
.redirect(redirect::Policy::limited(4))
|
||||
.build()?,
|
||||
|
||||
federation: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.hooked.clone())
|
||||
.dns_resolver(resolver.resolver.hooked.clone())
|
||||
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(config.federation_timeout))
|
||||
.timeout(Duration::from_secs(
|
||||
@@ -76,7 +86,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.build()?,
|
||||
|
||||
federation_slow: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.hooked.clone())
|
||||
.dns_resolver(resolver.resolver.hooked.clone())
|
||||
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(config.federation_timeout.saturating_mul(6)))
|
||||
.timeout(Duration::from_secs(
|
||||
@@ -90,7 +100,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.build()?,
|
||||
|
||||
sender: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.hooked.clone())
|
||||
.dns_resolver(resolver.resolver.hooked.clone())
|
||||
.connect_timeout(Duration::from_secs(config.federation_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(config.sender_timeout))
|
||||
.timeout(Duration::from_secs(config.sender_timeout))
|
||||
@@ -100,7 +110,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.build()?,
|
||||
|
||||
appservice: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.clone())
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.connect_timeout(Duration::from_secs(5))
|
||||
.read_timeout(Duration::from_secs(config.appservice_timeout))
|
||||
.timeout(Duration::from_secs(config.appservice_timeout))
|
||||
@@ -110,7 +120,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.build()?,
|
||||
|
||||
pusher: base(config)?
|
||||
.dns_resolver(resolver.dns.resolver.clone())
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
|
||||
.timeout(Duration::from_secs(config.pusher_timeout))
|
||||
.pool_max_idle_per_host(1)
|
||||
@@ -141,7 +151,7 @@ pub fn valid_cidr_range(&self, ip: &IPAddress) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
|
||||
fn base(config: &Config) -> Result<reqwest::ClientBuilder> {
|
||||
let mut builder = reqwest::Client::builder()
|
||||
.hickory_dns(true)
|
||||
.connect_timeout(Duration::from_secs(config.request_conn_timeout))
|
||||
|
||||
@@ -54,22 +54,15 @@ impl Service {
|
||||
async fn set_emergency_access(&self) -> Result {
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
match &self.services.config.emergency_password {
|
||||
| Some(emergency_password) => {
|
||||
let emergency_password = HashedPassword::new(emergency_password)?;
|
||||
|
||||
self.services
|
||||
.users
|
||||
.convert_to_local_account(server_user, emergency_password)
|
||||
.await?;
|
||||
},
|
||||
| None => {
|
||||
self.services
|
||||
.users
|
||||
.convert_to_shadow_account(server_user)
|
||||
.await?;
|
||||
},
|
||||
}
|
||||
self.services.users.set_password(
|
||||
server_user,
|
||||
self.services
|
||||
.config
|
||||
.emergency_password
|
||||
.as_deref()
|
||||
.map(HashedPassword::new)
|
||||
.transpose()?,
|
||||
);
|
||||
|
||||
let (ruleset, pwd_set) = match self.services.config.emergency_password {
|
||||
| Some(_) => (Ruleset::server_default(server_user), true),
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
};
|
||||
use ipaddress::IPAddress;
|
||||
use reqwest::{Client, Method, Request, Response, Url};
|
||||
use resolvematrix::resolution::Resolution;
|
||||
use ruma::{
|
||||
ServerName,
|
||||
api::{
|
||||
@@ -19,7 +18,7 @@
|
||||
},
|
||||
};
|
||||
|
||||
use crate::SUPPORTED_VERSIONS;
|
||||
use crate::{SUPPORTED_VERSIONS, resolver::actual::ActualDest};
|
||||
|
||||
impl super::Service {
|
||||
/// Sends a signed request to a remote server over federation.
|
||||
@@ -131,15 +130,10 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
|
||||
))));
|
||||
}
|
||||
|
||||
let actual = self
|
||||
.services
|
||||
.resolver
|
||||
.resolver
|
||||
.resolve_server(dest.as_str())
|
||||
.await?;
|
||||
let actual = self.services.resolver.get_actual_dest(dest).await?;
|
||||
|
||||
let request = Request::try_from(request.try_into_http_request::<Vec<u8>>(
|
||||
actual.base_url().as_str(),
|
||||
actual.string().as_str(),
|
||||
authentication,
|
||||
PathBuilderInput::create(),
|
||||
)?)?;
|
||||
@@ -156,7 +150,7 @@ pub async fn execute_on<'i, T, PathBuilderInput>(
|
||||
async fn perform<T>(
|
||||
&self,
|
||||
dest: &ServerName,
|
||||
actual: &Resolution,
|
||||
actual: &ActualDest,
|
||||
request: Request,
|
||||
client: &Client,
|
||||
) -> Result<T::IncomingResponse>
|
||||
@@ -192,9 +186,7 @@ fn validate_url(&self, url: &Url) -> Result<()> {
|
||||
if let Some(url_host) = url.host_str() {
|
||||
if let Ok(ip) = IPAddress::parse(url_host) {
|
||||
trace!("Checking request URL IP {ip:?}");
|
||||
if !self.services.client.valid_cidr_range(&ip) {
|
||||
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
|
||||
}
|
||||
self.services.resolver.validate_ip(&ip)?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,7 +198,7 @@ fn validate_url(&self, url: &Url) -> Result<()> {
|
||||
async fn handle_response<T>(
|
||||
&self,
|
||||
dest: &ServerName,
|
||||
actual: &Resolution,
|
||||
actual: &ActualDest,
|
||||
method: &Method,
|
||||
url: &Url,
|
||||
response: Response,
|
||||
@@ -239,7 +231,7 @@ async fn handle_response<T>(
|
||||
|
||||
async fn into_http_response(
|
||||
dest: &ServerName,
|
||||
actual: &Resolution,
|
||||
actual: &ActualDest,
|
||||
method: &Method,
|
||||
url: &Url,
|
||||
mut response: Response,
|
||||
@@ -251,7 +243,7 @@ async fn into_http_response(
|
||||
request_url = %url,
|
||||
response_url = %response.url(),
|
||||
"Received response from {}",
|
||||
actual.base_url(),
|
||||
actual.string(),
|
||||
);
|
||||
|
||||
let mut http_response_builder = http::Response::builder()
|
||||
@@ -288,7 +280,7 @@ async fn into_http_response(
|
||||
}
|
||||
|
||||
fn handle_error(
|
||||
actual: &Resolution,
|
||||
actual: &ActualDest,
|
||||
method: &Method,
|
||||
url: &Url,
|
||||
mut e: reqwest::Error,
|
||||
|
||||
@@ -73,7 +73,7 @@ async fn worker(self: Arc<Self>) -> Result {
|
||||
&& self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.list_local_users()
|
||||
.ready_filter(|user| *user != self.services.globals.server_user)
|
||||
.next()
|
||||
.await
|
||||
|
||||
@@ -211,6 +211,7 @@ pub async fn download_audio(
|
||||
Ok(preview_data)
|
||||
}
|
||||
|
||||
#[cfg(feature = "url_preview")]
|
||||
pub async fn download_media(&self, url: &str) -> Result<(OwnedMxcUri, usize)> {
|
||||
use conduwuit::utils::random_string;
|
||||
use http::header::CONTENT_TYPE;
|
||||
@@ -267,6 +268,11 @@ pub async fn download_audio(
|
||||
Err!(FeatureDisabled("url_preview"))
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "url_preview"))]
|
||||
pub async fn download_media(&self, _url: &str) -> Result<UrlPreviewData> {
|
||||
Err!(FeatureDisabled("url_preview"))
|
||||
}
|
||||
|
||||
#[cfg(feature = "url_preview")]
|
||||
async fn download_html(&self, url: &str) -> Result<UrlPreviewData> {
|
||||
use webpage::HTML;
|
||||
|
||||
@@ -41,7 +41,7 @@ pub(crate) async fn migrations(services: &Services) -> Result<()> {
|
||||
// requires recreating the database from scratch.
|
||||
if users_count > 0 {
|
||||
let server_user = &services.globals.server_user;
|
||||
if !services.users.status(server_user).await.is_found() {
|
||||
if !services.users.exists(server_user).await {
|
||||
error!("The {server_user} server user does not exist, and the database is not new.");
|
||||
return Err!(Database(
|
||||
"Cannot reuse an existing database after changing the server name, please \
|
||||
@@ -228,18 +228,6 @@ async fn migrate(services: &Services) -> Result<()> {
|
||||
.map_err(|e| err!("Failed to run 'fix_local_invite_state' migration': {e}"))?;
|
||||
}
|
||||
|
||||
if services.globals.db.database_version().await < 18 {
|
||||
services.globals.db.bump_database_version(18);
|
||||
info!("Migration: Bumped database version to 18");
|
||||
}
|
||||
|
||||
if db["global"].get(SPLIT_USERID_PASSWORD).await.is_not_found() {
|
||||
info!("Running migration 'split_userid_password'");
|
||||
split_userid_password(services)
|
||||
.await
|
||||
.map_err(|e| err!("Failed to run 'split_userid_password' migration': {e}"))?;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
services.globals.db.database_version().await,
|
||||
DATABASE_VERSION,
|
||||
@@ -254,9 +242,9 @@ async fn migrate(services: &Services) -> Result<()> {
|
||||
if !patterns.is_empty() {
|
||||
services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.stream()
|
||||
.filter_map(async |user_id| {
|
||||
if services.users.status(&user_id).await.is_found() {
|
||||
if services.users.is_active_local(&user_id).await {
|
||||
Some(user_id)
|
||||
} else {
|
||||
None
|
||||
@@ -315,8 +303,7 @@ async fn migrate(services: &Services) -> Result<()> {
|
||||
async fn db_lt_12(services: &Services) -> Result<()> {
|
||||
for username in &services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.ready_filter(|user| services.globals.user_is_local(user))
|
||||
.list_local_users()
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
@@ -395,8 +382,7 @@ async fn db_lt_12(services: &Services) -> Result<()> {
|
||||
async fn db_lt_13(services: &Services) -> Result<()> {
|
||||
for username in &services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.ready_filter(|user| services.globals.user_is_local(user))
|
||||
.list_local_users()
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
@@ -788,7 +774,7 @@ async fn fix_local_invite_state(services: &Services) -> Result {
|
||||
|
||||
let db = &services.db;
|
||||
let cork = db.cork_and_sync();
|
||||
let userroomid_invitestate = db["userroomid_invitestate"].clone();
|
||||
let userroomid_invitestate = services.db["userroomid_invitestate"].clone();
|
||||
|
||||
// for each user invited to a room
|
||||
let fixed = userroomid_invitestate.stream()
|
||||
@@ -832,44 +818,3 @@ async fn fix_local_invite_state(services: &Services) -> Result {
|
||||
db.db.sort()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
const SPLIT_USERID_PASSWORD: &str = "split_userid_password";
|
||||
async fn split_userid_password(services: &Services) -> Result {
|
||||
// Split remote and deactivated users out from the `userid_password` table
|
||||
|
||||
let db = &services.db;
|
||||
let cork = db.cork_and_sync();
|
||||
let userid_password = db["userid_password"].clone();
|
||||
let remoteuserid_remoteuser = db["remoteuserid_remoteuser"].clone();
|
||||
let userid_deactivated = db["userid_deactivated"].clone();
|
||||
|
||||
let remote_users = userid_password
|
||||
.stream::<OwnedUserId, String>()
|
||||
.ignore_err()
|
||||
.fold(0_usize, async |mut remote_users, (user_id, hash)| {
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
assert!(hash.is_empty(), "non-empty hash {hash} for remote user {user_id}");
|
||||
|
||||
remoteuserid_remoteuser.insert(&user_id, "");
|
||||
userid_password.remove(&user_id);
|
||||
remote_users = remote_users.saturating_add(1);
|
||||
} else if hash.is_empty() {
|
||||
if !(services.appservice.is_exclusive_user_id(&user_id).await
|
||||
|| user_id == services.globals.server_user)
|
||||
{
|
||||
info!("Marking {user_id} as deactivated");
|
||||
userid_deactivated.insert(&user_id, "");
|
||||
}
|
||||
}
|
||||
|
||||
remote_users
|
||||
})
|
||||
.await;
|
||||
|
||||
drop(cork);
|
||||
info!(?remote_users, "Split userid_password.");
|
||||
|
||||
db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []);
|
||||
db.db.sort()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
pub mod media;
|
||||
pub mod moderation;
|
||||
pub mod oauth;
|
||||
pub mod oidc;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
pub mod registration_tokens;
|
||||
|
||||
@@ -160,7 +160,7 @@ pub enum ErrorCode {
|
||||
InvalidClientMetadata,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[derive(Serialize)]
|
||||
pub struct AuthorizationCodeResponse {
|
||||
pub state: String,
|
||||
pub code: String,
|
||||
|
||||
@@ -1,484 +0,0 @@
|
||||
use std::{collections::HashMap, str::FromStr, sync::Arc, time::Duration};
|
||||
|
||||
use async_trait::async_trait;
|
||||
use conduwuit::{
|
||||
Result,
|
||||
config::{OidcConfig, OidcProfileKeyImportMode},
|
||||
debug, err, error, info, warn,
|
||||
};
|
||||
use database::{Deserialized, Map};
|
||||
use lettre::Address;
|
||||
use openidconnect::{
|
||||
AdditionalClaims, AuthorizationCode, ClientSecret, CsrfToken, EmptyExtraTokenFields,
|
||||
EndpointMaybeSet, EndpointNotSet, EndpointSet, IdTokenClaims, IdTokenFields, IssuerUrl,
|
||||
Nonce, PkceCodeChallenge, PkceCodeVerifier, RedirectUrl, StandardErrorResponse,
|
||||
StandardTokenResponse, TokenResponse,
|
||||
core::{
|
||||
CoreAuthDisplay, CoreAuthPrompt, CoreAuthenticationFlow, CoreErrorResponseType,
|
||||
CoreGenderClaim, CoreJsonWebKey, CoreJweContentEncryptionAlgorithm,
|
||||
CoreJwsSigningAlgorithm, CoreProviderMetadata, CoreRevocableToken,
|
||||
CoreRevocationErrorResponse, CoreTokenIntrospectionResponse, CoreTokenType,
|
||||
},
|
||||
reqwest,
|
||||
};
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::profile::PropagateTo,
|
||||
profile::{ProfileFieldName, ProfileFieldValue},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::{runtime, sync::SetOnce};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
Dep, config, globals, media,
|
||||
oauth::grant::AuthorizationCodeResponse,
|
||||
threepid,
|
||||
users::{self, AccountStatus, ProfileFieldChange},
|
||||
};
|
||||
|
||||
pub struct Service {
|
||||
services: Services,
|
||||
runtime: runtime::Handle,
|
||||
db: Data,
|
||||
client: Option<OidcClient>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
openidsubject_localpart: Arc<Map>,
|
||||
openidsubject_currentpictureurl: Arc<Map>,
|
||||
}
|
||||
struct Services {
|
||||
config: Dep<config::Service>,
|
||||
globals: Dep<globals::Service>,
|
||||
media: Dep<media::Service>,
|
||||
threepid: Dep<threepid::Service>,
|
||||
users: Dep<users::Service>,
|
||||
}
|
||||
|
||||
struct OidcClient {
|
||||
config: OidcConfig,
|
||||
client_secret: ClientSecret,
|
||||
machine: SetOnce<OidcClientMachine>,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
type OidcClientMachine = openidconnect::Client<
|
||||
AllClaims,
|
||||
CoreAuthDisplay,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJsonWebKey,
|
||||
CoreAuthPrompt,
|
||||
StandardErrorResponse<CoreErrorResponseType>,
|
||||
StandardTokenResponse<
|
||||
IdTokenFields<
|
||||
AllClaims,
|
||||
EmptyExtraTokenFields,
|
||||
CoreGenderClaim,
|
||||
CoreJweContentEncryptionAlgorithm,
|
||||
CoreJwsSigningAlgorithm,
|
||||
>,
|
||||
CoreTokenType,
|
||||
>,
|
||||
CoreTokenIntrospectionResponse,
|
||||
CoreRevocableToken,
|
||||
CoreRevocationErrorResponse,
|
||||
EndpointSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointNotSet,
|
||||
EndpointMaybeSet,
|
||||
EndpointMaybeSet,
|
||||
>;
|
||||
|
||||
pub type Claims = IdTokenClaims<AllClaims, CoreGenderClaim>;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AllClaims {
|
||||
#[serde(flatten)]
|
||||
pub claims: HashMap<String, Value>,
|
||||
}
|
||||
|
||||
impl AdditionalClaims for AllClaims {}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct PendingSession {
|
||||
pkce_verifier: PkceCodeVerifier,
|
||||
nonce: Nonce,
|
||||
csrf_token: CsrfToken,
|
||||
}
|
||||
|
||||
pub enum SessionCompletionStatus {
|
||||
NeedsUserId,
|
||||
Complete(OwnedUserId),
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl crate::Service for Service {
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
config: args.depend::<config::Service>("config"),
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
media: args.depend::<media::Service>("media"),
|
||||
threepid: args.depend::<threepid::Service>("threepid"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
},
|
||||
runtime: args.server.runtime().clone(),
|
||||
db: Data {
|
||||
openidsubject_localpart: args.db["openidsubject_localpart"].clone(),
|
||||
openidsubject_currentpictureurl: args.db["openidsubject_currentpictureurl"].clone(),
|
||||
},
|
||||
client: args.server.config.oauth.oidc.as_ref().map(|config| -> Result<OidcClient> {
|
||||
Ok(OidcClient {
|
||||
config: config.clone(),
|
||||
client_secret: if let Some(client_secret_file) = &config.client_secret_file {
|
||||
std::fs::read_to_string(client_secret_file)
|
||||
.map(ClientSecret::new)
|
||||
.map_err(|err| err!("Failed to read OIDC client secret file: {err}"))?
|
||||
} else if let Some(client_secret) = &config.client_secret {
|
||||
client_secret.clone()
|
||||
} else {
|
||||
// The config check function should cause an early exit before this happens
|
||||
panic!("neither client secret or client secret file were set");
|
||||
},
|
||||
machine: SetOnce::new(),
|
||||
// This isn't in the client service because it has to use the `reqwest` shipped by `openidconnect`
|
||||
client: reqwest::ClientBuilder::new()
|
||||
.connect_timeout(Duration::from_secs(args.server.config.request_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(args.server.config.request_timeout))
|
||||
.timeout(Duration::from_secs(args.server.config.request_total_timeout))
|
||||
.pool_idle_timeout(Duration::from_secs(args.server.config.request_idle_timeout))
|
||||
.pool_max_idle_per_host(args.server.config.request_idle_per_host.into())
|
||||
.user_agent(conduwuit::user_agent())
|
||||
.redirect(reqwest::redirect::Policy::none())
|
||||
.danger_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
||||
.build()
|
||||
.expect("client should build")
|
||||
})}
|
||||
).transpose()?,
|
||||
}))
|
||||
}
|
||||
|
||||
async fn worker(self: Arc<Self>) -> Result {
|
||||
if let Some(OidcClient { config, client_secret, machine, client }) = &self.client {
|
||||
let redirect_url = self
|
||||
.services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(&format!("{}/oidc/complete", conduwuit::ROUTE_PREFIX))
|
||||
.expect("redirect url should be valid");
|
||||
|
||||
let provider_metadata = CoreProviderMetadata::discover_async(
|
||||
IssuerUrl::from_url(config.discovery_url.clone()),
|
||||
client,
|
||||
)
|
||||
.await
|
||||
.map_err(|err| err!("Failed to discover OIDC provider metadata: {err}"))?;
|
||||
|
||||
machine
|
||||
.set(
|
||||
OidcClientMachine::from_provider_metadata(
|
||||
provider_metadata,
|
||||
config.client_id.clone(),
|
||||
Some(client_secret.clone()),
|
||||
)
|
||||
.set_redirect_uri(RedirectUrl::from_url(redirect_url)),
|
||||
)
|
||||
.expect("machine should be empty");
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
impl Service {
|
||||
const SERVER_MISCONFIGURED: &str =
|
||||
"Identity server is misconfigured. Contact your homeserver's administrator.";
|
||||
|
||||
pub fn enabled(&self) -> bool { self.client.is_some() }
|
||||
|
||||
pub fn restricted_profile_fields(&self) -> Vec<ProfileFieldName> {
|
||||
if let Some(config) = self.client.as_ref().map(|client| &client.config)
|
||||
&& matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin)
|
||||
{
|
||||
config
|
||||
.profile_key_map
|
||||
.keys()
|
||||
.map(|key| ProfileFieldName::from(key.as_str()))
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn begin_session(&self, prompt: Option<CoreAuthPrompt>) -> (PendingSession, Url) {
|
||||
let OidcClient { machine, config, .. } =
|
||||
self.client.as_ref().expect("oidc should be configured");
|
||||
let machine = machine.wait().await;
|
||||
|
||||
let (pkce_challenge, pkce_verifier) = PkceCodeChallenge::new_random_sha256();
|
||||
|
||||
let mut auth_url = machine
|
||||
.authorize_url(
|
||||
CoreAuthenticationFlow::AuthorizationCode,
|
||||
CsrfToken::new_random,
|
||||
Nonce::new_random,
|
||||
)
|
||||
.add_scopes(config.additional_scopes.iter().cloned())
|
||||
.set_pkce_challenge(pkce_challenge);
|
||||
|
||||
if let Some(prompt) = prompt {
|
||||
auth_url = auth_url.add_prompt(prompt);
|
||||
}
|
||||
|
||||
let (auth_url, csrf_token, nonce) = auth_url.url();
|
||||
|
||||
(PendingSession { pkce_verifier, nonce, csrf_token }, auth_url)
|
||||
}
|
||||
|
||||
pub async fn exchange_code(
|
||||
&self,
|
||||
session: PendingSession,
|
||||
response: AuthorizationCodeResponse,
|
||||
) -> Result<Claims, &'static str> {
|
||||
let Some(OidcClient { machine, client, .. }) = self.client.as_ref() else {
|
||||
return Err("Delegated authentication is not enabled on this server.");
|
||||
};
|
||||
|
||||
let machine = machine.wait().await;
|
||||
|
||||
if session.csrf_token.into_secret() != response.state {
|
||||
return Err("State mismatch.");
|
||||
}
|
||||
|
||||
let token_response = machine
|
||||
.exchange_code(AuthorizationCode::new(response.code))
|
||||
.expect("machine should be configured correctly")
|
||||
.set_pkce_verifier(session.pkce_verifier)
|
||||
.request_async(client)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to exchange OIDC authorization code: {err}");
|
||||
"Code exchange failed."
|
||||
})?;
|
||||
|
||||
let Some(id_token) = token_response.id_token() else {
|
||||
error!("Identity server did not return an id token");
|
||||
return Err(Self::SERVER_MISCONFIGURED);
|
||||
};
|
||||
|
||||
let claims = id_token
|
||||
.claims(&machine.id_token_verifier(), &session.nonce)
|
||||
.map_err(|err| {
|
||||
error!("Failed to verify id token claims: {err}");
|
||||
Self::SERVER_MISCONFIGURED
|
||||
})?
|
||||
.to_owned();
|
||||
|
||||
info!(subject = claims.subject().as_str(), "Authenticated subject");
|
||||
|
||||
Ok(claims)
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip(self, claims), fields(subject = claims.subject().to_string()))]
|
||||
pub async fn complete_session(
|
||||
&self,
|
||||
claims: &Claims,
|
||||
supplied_user_id: Option<OwnedUserId>,
|
||||
) -> Result<SessionCompletionStatus, &'static str> {
|
||||
let Some(OidcClient { config, .. }) = self.client.as_ref() else {
|
||||
return Err("Delegated authentication is not enabled on this server.");
|
||||
};
|
||||
|
||||
// this is a truly awful hack but we really need all the claims in a map
|
||||
let all_claims = serde_json::to_value(claims)
|
||||
.expect("should be able to serialize claims")
|
||||
.as_object()
|
||||
.expect("claims should be an object")
|
||||
.to_owned();
|
||||
|
||||
debug!(?all_claims);
|
||||
|
||||
let subject = claims.subject().as_str();
|
||||
|
||||
let user_id = if let Ok(localpart) = self
|
||||
.db
|
||||
.openidsubject_localpart
|
||||
.get(subject)
|
||||
.await
|
||||
.deserialized::<String>()
|
||||
{
|
||||
UserId::parse(format!("@{localpart}:{}", self.services.globals.server_name()))
|
||||
.expect("saved localpart should be valid")
|
||||
} else if config.prompt_for_localpart {
|
||||
if let Some(supplied_user_id) = supplied_user_id {
|
||||
supplied_user_id
|
||||
} else {
|
||||
return Ok(SessionCompletionStatus::NeedsUserId);
|
||||
}
|
||||
} else if let Some(preferred_username) = all_claims
|
||||
.get(&config.preferred_username_claim)
|
||||
.and_then(|claim| claim.as_str())
|
||||
{
|
||||
self.services
|
||||
.users
|
||||
.determine_registration_user_id(Some(preferred_username.to_owned()), None, None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Preferred username claim is not a valid localpart: {err}");
|
||||
"Your preferred username is not a valid Matrix user ID localpart. Contact \
|
||||
your homeserver's administrator."
|
||||
})?
|
||||
} else {
|
||||
error!("Preferred username claim was not present or was not a string");
|
||||
return Err(Self::SERVER_MISCONFIGURED);
|
||||
};
|
||||
|
||||
info!(?subject, ?user_id, "User {user_id} successfully authorized with OIDC");
|
||||
|
||||
// Create a shadow account for the user if necessary
|
||||
let new_account_registered = match self.services.users.status(&user_id).await {
|
||||
| AccountStatus::Active => {
|
||||
// Do nothing, an account already exists
|
||||
false
|
||||
},
|
||||
| AccountStatus::NotFound => {
|
||||
// Create a new shadow user
|
||||
self.services
|
||||
.users
|
||||
.create_local_account(&user_id, None, None)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
error!("Failed to create a shadow user for {user_id}: {err}");
|
||||
Self::SERVER_MISCONFIGURED
|
||||
})?;
|
||||
|
||||
info!(?subject, ?user_id, "Shadow user created for {user_id}");
|
||||
true
|
||||
},
|
||||
| AccountStatus::Deactivated => {
|
||||
return Err("Your account has been deactivated.");
|
||||
},
|
||||
};
|
||||
|
||||
self.link_user(&user_id, subject);
|
||||
|
||||
// Import profile fields
|
||||
if matches!(config.profile_key_import_mode, OidcProfileKeyImportMode::OnLogin)
|
||||
|| (matches!(
|
||||
config.profile_key_import_mode,
|
||||
OidcProfileKeyImportMode::OnRegistration
|
||||
) && new_account_registered)
|
||||
{
|
||||
if let Some(email_claim) = &config.email_claim {
|
||||
if let Some(email) = claims.email().map(|email| email.as_str())
|
||||
&& let Ok(address) = Address::from_str(email)
|
||||
{
|
||||
if let Err(err) = self
|
||||
.services
|
||||
.threepid
|
||||
.associate_localpart_email(user_id.localpart(), &address)
|
||||
.await
|
||||
{
|
||||
warn!(?email_claim, ?address, "Failed to associate email address: {err}");
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
?email_claim,
|
||||
"Email claim was not present or was not a valid email address"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let user_id = user_id.clone();
|
||||
let subject = claims.subject().to_string();
|
||||
let profile_key_map = config.profile_key_map.clone();
|
||||
let openidsubject_currentpictureurl = self.db.openidsubject_currentpictureurl.clone();
|
||||
let users = self.services.users.clone();
|
||||
let media = self.services.media.clone();
|
||||
|
||||
let import_task = self.runtime.spawn(async move {
|
||||
for (field, claim) in &profile_key_map {
|
||||
let Some(value) = all_claims.get(claim).cloned() else {
|
||||
warn!(?field, ?claim, "IDP provided no value for this mapped claim");
|
||||
continue;
|
||||
};
|
||||
|
||||
let value = if let Some(picture_url) = value.as_str()
|
||||
&& field == ProfileFieldName::AvatarUrl.as_str()
|
||||
&& openidsubject_currentpictureurl
|
||||
.get(&subject)
|
||||
.await
|
||||
.deserialized::<String>()
|
||||
.ok()
|
||||
.is_none_or(|current_picture| current_picture != picture_url)
|
||||
{
|
||||
match media.download_media(picture_url).await {
|
||||
| Ok((mxc, size)) => {
|
||||
openidsubject_currentpictureurl.insert(&subject, picture_url);
|
||||
info!(?picture_url, ?mxc, ?size, "Downloaded profile picture");
|
||||
|
||||
ProfileFieldValue::AvatarUrl(mxc)
|
||||
},
|
||||
| Err(err) => {
|
||||
warn!(
|
||||
?claim,
|
||||
?picture_url,
|
||||
"Failed to download profile picture: {err}"
|
||||
);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
} else {
|
||||
match ProfileFieldValue::new(field, value.clone()) {
|
||||
| Ok(value) => value,
|
||||
| Err(err) => {
|
||||
warn!(
|
||||
?field,
|
||||
?claim,
|
||||
?value,
|
||||
"Failed to parse claim value for profile field: {err}"
|
||||
);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = users
|
||||
.set_profile_field(
|
||||
&user_id,
|
||||
ProfileFieldChange::Set(value),
|
||||
PropagateTo::Unchanged,
|
||||
)
|
||||
.await
|
||||
{
|
||||
warn!(?field, ?claim, "Error while setting profile field: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
info!("Profile import complete");
|
||||
});
|
||||
|
||||
// Only wait for import to complete if this is a new account,
|
||||
// so they see the correct profile information in the account panel
|
||||
if new_account_registered {
|
||||
let _ = import_task.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(SessionCompletionStatus::Complete(user_id))
|
||||
}
|
||||
|
||||
pub fn link_user(&self, user_id: &UserId, subject: &str) {
|
||||
self.db
|
||||
.openidsubject_localpart
|
||||
.insert(subject, user_id.localpart());
|
||||
}
|
||||
|
||||
pub fn unlink_user(&self, subject: &str) { self.db.openidsubject_localpart.remove(subject); }
|
||||
}
|
||||
@@ -182,7 +182,7 @@ pub async fn unset_all_presence(&self) {
|
||||
for user_id in &self
|
||||
.services
|
||||
.users
|
||||
.stream_local_users()
|
||||
.list_local_users()
|
||||
.collect::<Vec<OwnedUserId>>()
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
|
||||
use conduwuit::{Err, Result, debug, debug_info, err, error, trace};
|
||||
use futures::{FutureExt, TryFutureExt};
|
||||
use hickory_resolver::{
|
||||
net::{DnsError, NetError},
|
||||
proto::rr::rdata::SRV,
|
||||
};
|
||||
use ipaddress::IPAddress;
|
||||
use ruma::ServerName;
|
||||
|
||||
use super::{
|
||||
cache::{CachedDest, CachedOverride, MAX_IPS},
|
||||
fed::{FedDest, PortString, add_port_to_hostname, get_ip_with_port},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ActualDest {
|
||||
pub(crate) dest: FedDest,
|
||||
pub(crate) host: String,
|
||||
}
|
||||
|
||||
impl ActualDest {
|
||||
#[inline]
|
||||
pub(crate) fn string(&self) -> String { self.dest.https_string() }
|
||||
}
|
||||
|
||||
impl super::Service {
|
||||
#[tracing::instrument(skip_all, level = "debug", name = "resolve")]
|
||||
pub(crate) async fn get_actual_dest(&self, server_name: &ServerName) -> Result<ActualDest> {
|
||||
let (CachedDest { dest, host, .. }, _cached) =
|
||||
self.lookup_actual_dest(server_name).await?;
|
||||
|
||||
Ok(ActualDest { dest, host })
|
||||
}
|
||||
|
||||
pub(crate) async fn lookup_actual_dest(
|
||||
&self,
|
||||
server_name: &ServerName,
|
||||
) -> Result<(CachedDest, bool)> {
|
||||
if let Ok(result) = self.cache.get_destination(server_name).await {
|
||||
return Ok((result, true));
|
||||
}
|
||||
|
||||
let _dedup = self.resolving.lock(server_name.as_str());
|
||||
if let Ok(result) = self.cache.get_destination(server_name).await {
|
||||
return Ok((result, true));
|
||||
}
|
||||
|
||||
self.resolve_actual_dest(server_name, true)
|
||||
.inspect_ok(|result| self.cache.set_destination(server_name, result))
|
||||
.map_ok(|result| (result, false))
|
||||
.boxed()
|
||||
.await
|
||||
}
|
||||
|
||||
/// Returns: `actual_destination`, host header
|
||||
/// 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
|
||||
#[tracing::instrument(name = "actual", level = "debug", skip(self, cache))]
|
||||
pub async fn resolve_actual_dest(
|
||||
&self,
|
||||
dest: &ServerName,
|
||||
cache: bool,
|
||||
) -> Result<CachedDest> {
|
||||
self.validate_dest(dest)?;
|
||||
let mut host = dest.as_str().to_owned();
|
||||
let actual_dest = match get_ip_with_port(dest.as_str()) {
|
||||
| Some(host_port) => Self::actual_dest_1(host_port)?,
|
||||
| None =>
|
||||
if let Some(pos) = dest.as_str().find(':') {
|
||||
self.actual_dest_2(dest, cache, pos).await?
|
||||
} else {
|
||||
self.services.server.check_running()?;
|
||||
match self.request_well_known(dest.as_str()).await? {
|
||||
| Some(delegated) =>
|
||||
self.actual_dest_3(&mut host, cache, delegated).await?,
|
||||
| _ => match self.query_srv_record(dest.as_str()).await? {
|
||||
| Some(overrider) =>
|
||||
self.actual_dest_4(&host, cache, overrider).await?,
|
||||
| _ => self.actual_dest_5(dest, cache).await?,
|
||||
},
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 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
|
||||
let host = if let Ok(addr) = host.parse::<SocketAddr>() {
|
||||
FedDest::Literal(addr)
|
||||
} else if let Ok(addr) = host.parse::<IpAddr>() {
|
||||
FedDest::Named(addr.to_string(), FedDest::default_port())
|
||||
} else if let Some(pos) = host.find(':') {
|
||||
let (host, port) = host.split_at(pos);
|
||||
FedDest::Named(
|
||||
host.to_owned(),
|
||||
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
|
||||
)
|
||||
} else {
|
||||
FedDest::Named(host, FedDest::default_port())
|
||||
};
|
||||
|
||||
debug!("Actual destination: {actual_dest:?} hostname: {host:?}");
|
||||
Ok(CachedDest {
|
||||
dest: actual_dest,
|
||||
host: host.uri_string(),
|
||||
expire: CachedDest::default_expire(),
|
||||
})
|
||||
}
|
||||
|
||||
fn actual_dest_1(host_port: FedDest) -> Result<FedDest> {
|
||||
debug!("1: IP literal with provided or default port");
|
||||
Ok(host_port)
|
||||
}
|
||||
|
||||
async fn actual_dest_2(&self, dest: &ServerName, cache: bool, pos: usize) -> Result<FedDest> {
|
||||
debug!("2: Hostname with included port");
|
||||
let (host, port) = dest.as_str().split_at(pos);
|
||||
self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
|
||||
.await?;
|
||||
|
||||
Ok(FedDest::Named(
|
||||
host.to_owned(),
|
||||
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn actual_dest_3(
|
||||
&self,
|
||||
host: &mut String,
|
||||
cache: bool,
|
||||
delegated: String,
|
||||
) -> Result<FedDest> {
|
||||
debug!("3: A .well-known file is available");
|
||||
*host = add_port_to_hostname(&delegated).uri_string();
|
||||
match get_ip_with_port(&delegated) {
|
||||
| Some(host_and_port) => Self::actual_dest_3_1(host_and_port),
|
||||
| None =>
|
||||
if let Some(pos) = delegated.find(':') {
|
||||
self.actual_dest_3_2(cache, delegated, pos).await
|
||||
} else {
|
||||
trace!("Delegated hostname has no port in this branch");
|
||||
match self.query_srv_record(&delegated).await? {
|
||||
| Some(overrider) =>
|
||||
self.actual_dest_3_3(cache, delegated, overrider).await,
|
||||
| _ => self.actual_dest_3_4(cache, delegated).await,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn actual_dest_3_1(host_and_port: FedDest) -> Result<FedDest> {
|
||||
debug!("3.1: IP literal in .well-known file");
|
||||
Ok(host_and_port)
|
||||
}
|
||||
|
||||
async fn actual_dest_3_2(
|
||||
&self,
|
||||
cache: bool,
|
||||
delegated: String,
|
||||
pos: usize,
|
||||
) -> Result<FedDest> {
|
||||
debug!("3.2: Hostname with port in .well-known file");
|
||||
let (host, port) = delegated.split_at(pos);
|
||||
self.conditional_query_and_cache(host, port.parse::<u16>().unwrap_or(8448), cache)
|
||||
.await?;
|
||||
|
||||
Ok(FedDest::Named(
|
||||
host.to_owned(),
|
||||
port.try_into().unwrap_or_else(|_| FedDest::default_port()),
|
||||
))
|
||||
}
|
||||
|
||||
async fn actual_dest_3_3(
|
||||
&self,
|
||||
cache: bool,
|
||||
delegated: String,
|
||||
overrider: FedDest,
|
||||
) -> Result<FedDest> {
|
||||
debug!("3.3: SRV lookup successful");
|
||||
let force_port = overrider.port();
|
||||
self.conditional_query_and_cache_override(
|
||||
&delegated,
|
||||
&overrider.hostname(),
|
||||
force_port.unwrap_or(8448),
|
||||
cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(port) = force_port {
|
||||
return Ok(FedDest::Named(
|
||||
delegated,
|
||||
format!(":{port}")
|
||||
.as_str()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| FedDest::default_port()),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(add_port_to_hostname(&delegated))
|
||||
}
|
||||
|
||||
async fn actual_dest_3_4(&self, cache: bool, delegated: String) -> Result<FedDest> {
|
||||
debug!("3.4: No SRV records, just use the hostname from .well-known");
|
||||
self.conditional_query_and_cache(&delegated, 8448, cache)
|
||||
.await?;
|
||||
Ok(add_port_to_hostname(&delegated))
|
||||
}
|
||||
|
||||
async fn actual_dest_4(
|
||||
&self,
|
||||
host: &str,
|
||||
cache: bool,
|
||||
overrider: FedDest,
|
||||
) -> Result<FedDest> {
|
||||
debug!("4: No .well-known; SRV record found");
|
||||
let force_port = overrider.port();
|
||||
self.conditional_query_and_cache_override(
|
||||
host,
|
||||
&overrider.hostname(),
|
||||
force_port.unwrap_or(8448),
|
||||
cache,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(port) = force_port {
|
||||
let port = format!(":{port}");
|
||||
|
||||
return Ok(FedDest::Named(
|
||||
host.to_owned(),
|
||||
PortString::from(port.as_str()).unwrap_or_else(|_| FedDest::default_port()),
|
||||
));
|
||||
}
|
||||
|
||||
Ok(add_port_to_hostname(host))
|
||||
}
|
||||
|
||||
async fn actual_dest_5(&self, dest: &ServerName, cache: bool) -> Result<FedDest> {
|
||||
debug!("5: No SRV record found");
|
||||
self.conditional_query_and_cache(dest.as_str(), 8448, cache)
|
||||
.await?;
|
||||
|
||||
Ok(add_port_to_hostname(dest.as_str()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn conditional_query_and_cache(
|
||||
&self,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
cache: bool,
|
||||
) -> Result {
|
||||
self.conditional_query_and_cache_override(hostname, hostname, port, cache)
|
||||
.await
|
||||
}
|
||||
|
||||
#[inline]
|
||||
async fn conditional_query_and_cache_override(
|
||||
&self,
|
||||
untername: &str,
|
||||
hostname: &str,
|
||||
port: u16,
|
||||
cache: bool,
|
||||
) -> Result {
|
||||
if !cache {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.cache.has_override(untername).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.query_and_cache_override(untername, hostname, port)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "ip", level = "debug", skip(self))]
|
||||
async fn query_and_cache_override(
|
||||
&self,
|
||||
untername: &'_ str,
|
||||
hostname: &'_ str,
|
||||
port: u16,
|
||||
) -> Result {
|
||||
self.services.server.check_running()?;
|
||||
|
||||
debug!("querying IP for {untername:?} ({hostname:?}:{port})");
|
||||
match self.resolver.resolver.lookup_ip(hostname.to_owned()).await {
|
||||
| Err(e) => Self::handle_resolve_error(&e, hostname),
|
||||
| Ok(override_ip) => {
|
||||
self.cache.set_override(untername, &CachedOverride {
|
||||
ips: override_ip.iter().take(MAX_IPS).collect(),
|
||||
port,
|
||||
expire: CachedOverride::default_expire(),
|
||||
overriding: (hostname != untername)
|
||||
.then_some(hostname.into())
|
||||
.inspect(|_| debug_info!("{untername:?} overridden by {hostname:?}")),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(name = "srv", level = "debug", skip(self))]
|
||||
async fn query_srv_record(&self, hostname: &'_ str) -> Result<Option<FedDest>> {
|
||||
let hostnames =
|
||||
[format!("_matrix-fed._tcp.{hostname}."), format!("_matrix._tcp.{hostname}.")];
|
||||
|
||||
for hostname in hostnames {
|
||||
self.services.server.check_running()?;
|
||||
|
||||
debug!("querying SRV for {hostname:?}");
|
||||
let hostname = hostname.trim_end_matches('.');
|
||||
match self.resolver.resolver.srv_lookup(hostname).await {
|
||||
| Err(e) => Self::handle_resolve_error(&e, hostname)?,
|
||||
| Ok(result) => {
|
||||
return Ok(result.answers().iter().next().map(|result| {
|
||||
let data = result.try_borrow::<SRV>().expect("should be SRV response");
|
||||
|
||||
FedDest::Named(
|
||||
data.data()
|
||||
.target
|
||||
.to_string()
|
||||
.trim_end_matches('.')
|
||||
.to_owned(),
|
||||
format!(":{}", data.data().port)
|
||||
.as_str()
|
||||
.try_into()
|
||||
.unwrap_or_else(|_| FedDest::default_port()),
|
||||
)
|
||||
}));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn handle_resolve_error(err: &NetError, host: &'_ str) -> Result<()> {
|
||||
match err {
|
||||
| NetError::NoConnections => {
|
||||
error!(
|
||||
"Your DNS server is overloaded and has ran out of connections. It is \
|
||||
strongly recommended you remediate this issue to ensure proper federation \
|
||||
connectivity."
|
||||
);
|
||||
|
||||
Err!(error!(%host, "DNS error: {err}"))
|
||||
},
|
||||
| NetError::Timeout => Err!(error!(%host, "DNS query timed out")),
|
||||
| NetError::Dns(DnsError::NoRecordsFound(..)) => {
|
||||
// Raise to debug_warn if we can find out the result wasn't from cache
|
||||
debug!(%host, "No DNS records found: {err}");
|
||||
Ok(())
|
||||
},
|
||||
| _ => Err!(error!(%host, "DNS error: {err}")),
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_dest(&self, dest: &ServerName) -> Result<()> {
|
||||
if dest == self.services.server.name && !self.services.server.config.federation_loopback {
|
||||
return Err!("Won't send federation request to ourselves");
|
||||
}
|
||||
|
||||
if dest.is_ip_literal() || IPAddress::is_valid(dest.host()) {
|
||||
self.validate_dest_ip_literal(dest)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_dest_ip_literal(&self, dest: &ServerName) -> Result<()> {
|
||||
trace!("Destination is an IP literal, checking against IP range denylist.",);
|
||||
debug_assert!(
|
||||
dest.is_ip_literal() || !IPAddress::is_valid(dest.host()),
|
||||
"Destination is not an IP literal."
|
||||
);
|
||||
let ip = IPAddress::parse(dest.host()).map_err(|e| {
|
||||
err!(BadServerResponse(debug_error!("Failed to parse IP literal from string: {e}")))
|
||||
})?;
|
||||
|
||||
self.validate_ip(&ip)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn validate_ip(&self, ip: &IPAddress) -> Result<()> {
|
||||
if !self.services.client.valid_cidr_range(ip) {
|
||||
return Err!(BadServerResponse("Not allowed to send requests to this IP"));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
Result,
|
||||
arrayvec::ArrayVec,
|
||||
at, err,
|
||||
utils::{math::Expected, stream::TryIgnore},
|
||||
utils::{math::Expected, rand, stream::TryIgnore},
|
||||
};
|
||||
use database::{Cbor, Deserialized, Map};
|
||||
use futures::{Stream, StreamExt, future::join};
|
||||
@@ -114,6 +114,11 @@ impl CachedDest {
|
||||
#[must_use]
|
||||
pub fn valid(&self) -> bool { self.expire > SystemTime::now() }
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn default_expire() -> SystemTime {
|
||||
rand::time_from_now_secs(60 * 60 * 18..60 * 60 * 36)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn size(&self) -> usize {
|
||||
@@ -129,6 +134,11 @@ impl CachedOverride {
|
||||
#[must_use]
|
||||
pub fn valid(&self) -> bool { self.expire > SystemTime::now() }
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn default_expire() -> SystemTime {
|
||||
rand::time_from_now_secs(60 * 60 * 6..60 * 60 * 12)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn size(&self) -> usize { size_of_val(self) }
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
use std::{fmt, net::SocketAddr};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt,
|
||||
net::{IpAddr, SocketAddr},
|
||||
};
|
||||
|
||||
use conduwuit::{arrayvec::ArrayString, utils::math::Expected};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize)]
|
||||
pub enum FedDest {
|
||||
Literal(SocketAddr), // "ip:port"
|
||||
Named(String, PortString), // ("hostname", ":port")
|
||||
Literal(SocketAddr),
|
||||
Named(String, PortString),
|
||||
}
|
||||
|
||||
/// numeric or service-name
|
||||
@@ -14,7 +18,36 @@ pub enum FedDest {
|
||||
|
||||
const DEFAULT_PORT: &str = ":8448";
|
||||
|
||||
pub(crate) fn get_ip_with_port(dest_str: &str) -> Option<FedDest> {
|
||||
if let Ok(dest) = dest_str.parse::<SocketAddr>() {
|
||||
Some(FedDest::Literal(dest))
|
||||
} else if let Ok(ip_addr) = dest_str.parse::<IpAddr>() {
|
||||
Some(FedDest::Literal(SocketAddr::new(ip_addr, 8448)))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_port_to_hostname(dest: &str) -> FedDest {
|
||||
let (host, port) = match dest.find(':') {
|
||||
| None => (dest, DEFAULT_PORT),
|
||||
| Some(pos) => dest.split_at(pos),
|
||||
};
|
||||
|
||||
FedDest::Named(
|
||||
host.to_owned(),
|
||||
PortString::from(port).unwrap_or_else(|_| FedDest::default_port()),
|
||||
)
|
||||
}
|
||||
|
||||
impl FedDest {
|
||||
pub(crate) fn https_string(&self) -> String {
|
||||
match self {
|
||||
| Self::Literal(addr) => format!("https://{addr}"),
|
||||
| Self::Named(host, port) => format!("https://{host}{port}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn uri_string(&self) -> String {
|
||||
match self {
|
||||
| Self::Literal(addr) => addr.to_string(),
|
||||
@@ -22,6 +55,23 @@ pub(crate) fn uri_string(&self) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn hostname(&self) -> Cow<'_, str> {
|
||||
match &self {
|
||||
| Self::Literal(addr) => addr.ip().to_string().into(),
|
||||
| Self::Named(host, _) => host.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[allow(clippy::string_slice)]
|
||||
pub(crate) fn port(&self) -> Option<u16> {
|
||||
match &self {
|
||||
| Self::Literal(addr) => Some(addr.port()),
|
||||
| Self::Named(_, port) => port[1..].parse().ok(),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn default_port() -> PortString {
|
||||
|
||||
+24
-34
@@ -1,64 +1,54 @@
|
||||
pub mod actual;
|
||||
pub mod cache;
|
||||
mod dns;
|
||||
pub mod fed;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod well_known;
|
||||
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use std::sync::Arc;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use conduwuit::Result;
|
||||
use reqwest::redirect;
|
||||
use resolvematrix::server::{MatrixResolver, MatrixResolverBuilder};
|
||||
use conduwuit::{Result, Server, arrayvec::ArrayString, utils::MutexMap};
|
||||
|
||||
use self::{cache::Cache, dns::Resolver};
|
||||
use crate::client::base;
|
||||
use crate::{Dep, client};
|
||||
|
||||
pub struct Service {
|
||||
pub resolver: MatrixResolver,
|
||||
pub dns: Dns,
|
||||
#[allow(dead_code)] // This service doesn't access services after construction
|
||||
pub cache: Arc<Cache>,
|
||||
pub resolver: Arc<Resolver>,
|
||||
resolving: Resolving,
|
||||
services: Services,
|
||||
}
|
||||
|
||||
struct Services;
|
||||
|
||||
pub struct Dns {
|
||||
pub cache: Arc<Cache>,
|
||||
pub resolver: Arc<Resolver>,
|
||||
struct Services {
|
||||
server: Arc<Server>,
|
||||
client: Dep<client::Service>,
|
||||
}
|
||||
|
||||
type Resolving = MutexMap<NameBuf, ()>;
|
||||
type NameBuf = ArrayString<256>;
|
||||
|
||||
#[async_trait]
|
||||
impl crate::Service for Service {
|
||||
#[allow(clippy::as_conversions, clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
let cache = Cache::new(&args);
|
||||
let resolver = Resolver::build(args.server, cache.clone())?;
|
||||
Ok(Arc::new(Self {
|
||||
resolver: MatrixResolverBuilder::new()
|
||||
.dangerous_tls_accept_invalid_certs(args.server.config.allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure)
|
||||
.http_client(
|
||||
base(&args.server.config)?
|
||||
.connect_timeout(Duration::from_secs(args.server.config.well_known_conn_timeout))
|
||||
.read_timeout(Duration::from_secs(args.server.config.well_known_timeout))
|
||||
.timeout(Duration::from_secs(args.server.config.well_known_timeout))
|
||||
.pool_max_idle_per_host(0)
|
||||
.redirect(redirect::Policy::limited(4))
|
||||
.build()?
|
||||
)
|
||||
.dns_resolver(resolver.resolver.clone())
|
||||
.build()?,
|
||||
dns: Dns {
|
||||
cache,
|
||||
resolver,
|
||||
cache: cache.clone(),
|
||||
resolver: Resolver::build(args.server, cache)?,
|
||||
resolving: MutexMap::new(),
|
||||
services: Services {
|
||||
server: args.server.clone(),
|
||||
client: args.depend::<client::Service>("client"),
|
||||
},
|
||||
services: Services {},
|
||||
}))
|
||||
}
|
||||
|
||||
async fn clear_cache(&self) {
|
||||
self.resolver.clear_cache();
|
||||
self.dns.resolver.clear_cache();
|
||||
self.dns.cache.clear().await;
|
||||
self.cache.clear().await;
|
||||
}
|
||||
|
||||
fn name(&self) -> &str { crate::service::make_name(module_path!()) }
|
||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
use super::fed::{FedDest, add_port_to_hostname, get_ip_with_port};
|
||||
|
||||
#[test]
|
||||
fn ips_get_default_ports() {
|
||||
assert_eq!(
|
||||
get_ip_with_port("1.1.1.1"),
|
||||
Some(FedDest::Literal("1.1.1.1:8448".parse().unwrap()))
|
||||
);
|
||||
assert_eq!(
|
||||
get_ip_with_port("dead:beef::"),
|
||||
Some(FedDest::Literal("[dead:beef::]:8448".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ips_keep_custom_ports() {
|
||||
assert_eq!(
|
||||
get_ip_with_port("1.1.1.1:1234"),
|
||||
Some(FedDest::Literal("1.1.1.1:1234".parse().unwrap()))
|
||||
);
|
||||
assert_eq!(
|
||||
get_ip_with_port("[dead::beef]:8933"),
|
||||
Some(FedDest::Literal("[dead::beef]:8933".parse().unwrap()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostnames_get_default_ports() {
|
||||
assert_eq!(
|
||||
add_port_to_hostname("example.com"),
|
||||
FedDest::Named(String::from("example.com"), ":8448".try_into().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hostnames_keep_custom_ports() {
|
||||
assert_eq!(
|
||||
add_port_to_hostname("example.com:1337"),
|
||||
FedDest::Named(String::from("example.com"), ":1337".try_into().unwrap())
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use conduwuit::{Result, debug, debug_error, debug_info, trace, utils::response::LimitReadExt};
|
||||
use ruma::ServerName;
|
||||
|
||||
impl super::Service {
|
||||
#[tracing::instrument(name = "well-known", level = "debug", skip(self, dest))]
|
||||
pub(super) async fn request_well_known(&self, dest: &str) -> Result<Option<String>> {
|
||||
trace!("Requesting well known for {dest}");
|
||||
let response = self
|
||||
.services
|
||||
.client
|
||||
.well_known
|
||||
.get(format!("https://{dest}/.well-known/matrix/server"))
|
||||
.send()
|
||||
.await;
|
||||
|
||||
trace!("response: {response:?}");
|
||||
if let Err(e) = &response {
|
||||
debug!("error: {e:?}");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let response = response?;
|
||||
if !response.status().is_success() {
|
||||
debug!("response not 2XX");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let Ok(text) = response.limit_read_text(8192).await else {
|
||||
debug!("failed to read well-known response (too large or non-text content)");
|
||||
return Ok(None);
|
||||
};
|
||||
trace!("response text: {text:?}");
|
||||
|
||||
let body: serde_json::Value = serde_json::from_str(&text).unwrap_or_default();
|
||||
|
||||
let m_server = body
|
||||
.get("m.server")
|
||||
.unwrap_or(&serde_json::Value::Null)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
if ServerName::parse(m_server).is_err() {
|
||||
debug_error!("response content missing or invalid");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
debug_info!("{dest:?} found at {m_server:?}");
|
||||
Ok(Some(m_server.to_owned()))
|
||||
}
|
||||
}
|
||||
@@ -194,14 +194,10 @@ pub async fn handle_incoming_pdu<'a>(
|
||||
"Invite to {room_id} appears to have been rescinded by {sender}, \
|
||||
marking as left"
|
||||
);
|
||||
|
||||
self.services
|
||||
.state_cache
|
||||
.mark_as_left(&sender, room_id, Some(pdu))
|
||||
.await;
|
||||
|
||||
self.services.sync.wake(&sender).await;
|
||||
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
};
|
||||
use tokio::sync::{Notify, mpsc};
|
||||
|
||||
use crate::{Dep, globals, rooms, sending, server_keys, sync};
|
||||
use crate::{Dep, globals, rooms, sending, server_keys};
|
||||
pub struct Service {
|
||||
pub mutex_federation: RoomMutexMap,
|
||||
pub federation_handletime: SyncRwLock<HandleTimeMap>,
|
||||
@@ -44,7 +44,6 @@ struct Services {
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_compressor: Dep<rooms::state_compressor::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
timeline: Dep<rooms::timeline::Service>,
|
||||
server: Arc<Server>,
|
||||
}
|
||||
@@ -75,7 +74,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
state_compressor: args
|
||||
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||
server: args.server.clone(),
|
||||
},
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
state_compressor::{self, CompressedState, HashSetCompressStateEvent},
|
||||
timeline::{self, pdu_fits},
|
||||
},
|
||||
sending, server_keys, sync, users,
|
||||
sending, server_keys, users,
|
||||
};
|
||||
|
||||
pub struct Service {
|
||||
@@ -63,7 +63,6 @@ struct Services {
|
||||
state_accessor: Dep<state_accessor::Service>,
|
||||
state_cache: Dep<state_cache::Service>,
|
||||
state_compressor: Dep<state_compressor::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
timeline: Dep<timeline::Service>,
|
||||
users: Dep<users::Service>,
|
||||
}
|
||||
@@ -88,7 +87,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
state_cache: args.depend::<state_cache::Service>("rooms::state_cache"),
|
||||
state_compressor: args
|
||||
.depend::<state_compressor::Service>("rooms::state_compressor"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
timeline: args.depend::<timeline::Service>("rooms::timeline"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
},
|
||||
@@ -674,8 +672,6 @@ pub async fn join_remote_room(
|
||||
}
|
||||
drop(cork);
|
||||
|
||||
self.services.sync.wake_all_joined(room_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
};
|
||||
|
||||
use self::data::{Data, ReceiptItem};
|
||||
use crate::{Dep, rooms, sending, sync};
|
||||
use crate::{Dep, rooms, sending};
|
||||
|
||||
pub struct Service {
|
||||
services: Services,
|
||||
@@ -31,7 +31,6 @@ pub struct Service {
|
||||
struct Services {
|
||||
sending: Dep<sending::Service>,
|
||||
short: Dep<rooms::short::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
timeline: Dep<rooms::timeline::Service>,
|
||||
}
|
||||
|
||||
@@ -41,7 +40,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
services: Services {
|
||||
sending: args.depend::<sending::Service>("sending"),
|
||||
short: args.depend::<rooms::short::Service>("rooms::short"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||
},
|
||||
db: Data::new(&args),
|
||||
@@ -65,7 +63,6 @@ pub async fn readreceipt_update(
|
||||
.flush_room(room_id)
|
||||
.await
|
||||
.expect("room flush failed");
|
||||
self.services.sync.wake_all_joined(room_id).await;
|
||||
}
|
||||
|
||||
/// Gets the latest private read receipt from the user in the room
|
||||
|
||||
@@ -24,13 +24,11 @@
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Dep, globals,
|
||||
Dep, globals, rooms,
|
||||
rooms::{
|
||||
self,
|
||||
short::{ShortEventId, ShortStateHash},
|
||||
state_compressor::{CompressedState, parse_compressed_state_event},
|
||||
},
|
||||
sync,
|
||||
};
|
||||
|
||||
pub struct Service {
|
||||
@@ -45,7 +43,6 @@ struct Services {
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_compressor: Dep<rooms::state_compressor::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
timeline: Dep<rooms::timeline::Service>,
|
||||
}
|
||||
|
||||
@@ -71,7 +68,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
state_compressor: args
|
||||
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||
},
|
||||
db: Data {
|
||||
@@ -139,8 +135,6 @@ pub async fn force_state(
|
||||
|
||||
self.set_room_state(room_id, shortstatehash, state_lock);
|
||||
|
||||
self.services.sync.wake_all_joined(room_id).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -302,19 +296,23 @@ pub async fn append_to_state(&self, new_pdu: &PduEvent, room_id: &RoomId) -> Res
|
||||
}
|
||||
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
pub async fn summary_stripped(
|
||||
pub async fn summary_stripped<'a, E>(
|
||||
&self,
|
||||
event: &PduEvent,
|
||||
event: &'a E,
|
||||
room_id: &RoomId,
|
||||
target_user: &UserId,
|
||||
) -> Vec<RawStrippedState> {
|
||||
) -> Vec<RawStrippedState>
|
||||
where
|
||||
E: Event + Send + Sync,
|
||||
&'a E: Event + Send,
|
||||
{
|
||||
let mut state_events = [
|
||||
(&StateEventType::RoomCreate, ""),
|
||||
(&StateEventType::RoomJoinRules, ""),
|
||||
(&StateEventType::RoomCanonicalAlias, ""),
|
||||
(&StateEventType::RoomName, ""),
|
||||
(&StateEventType::RoomAvatar, ""),
|
||||
(&StateEventType::RoomMember, event.sender().as_str()),
|
||||
(&StateEventType::RoomMember, event.sender().as_str()), // Add recommended events
|
||||
(&StateEventType::RoomEncryption, ""),
|
||||
(&StateEventType::RoomTopic, ""),
|
||||
]
|
||||
@@ -324,20 +322,11 @@ pub async fn summary_stripped(
|
||||
state_events.push((&StateEventType::RoomMember, target_user.as_str()));
|
||||
}
|
||||
|
||||
let fetches = state_events
|
||||
.into_iter()
|
||||
.map(async |(event_type, state_key)| {
|
||||
if event.event_type() == &TimelineEventType::from(event_type.clone())
|
||||
&& event.state_key() == Some(state_key)
|
||||
{
|
||||
Ok(event.clone())
|
||||
} else {
|
||||
self.services
|
||||
.state_accessor
|
||||
.room_state_get(room_id, event_type, state_key)
|
||||
.await
|
||||
}
|
||||
});
|
||||
let fetches = state_events.into_iter().map(|(event_type, state_key)| {
|
||||
self.services
|
||||
.state_accessor
|
||||
.room_state_get(room_id, event_type, state_key)
|
||||
});
|
||||
|
||||
join_all(fetches)
|
||||
.await
|
||||
|
||||
@@ -16,9 +16,7 @@
|
||||
serde::Raw,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Dep, account_data, appservice::RegistrationInfo, config, globals, rooms, sync, users,
|
||||
};
|
||||
use crate::{Dep, account_data, appservice::RegistrationInfo, config, globals, rooms, users};
|
||||
|
||||
pub struct Service {
|
||||
appservice_in_room_cache: AppServiceInRoomCache,
|
||||
@@ -33,7 +31,6 @@ struct Services {
|
||||
metadata: Dep<rooms::metadata::Service>,
|
||||
state: Dep<rooms::state::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
users: Dep<users::Service>,
|
||||
}
|
||||
|
||||
@@ -70,7 +67,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
state: args.depend::<rooms::state::Service>("rooms::state"),
|
||||
state_accessor: args
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
},
|
||||
db: Data {
|
||||
@@ -252,7 +248,7 @@ pub fn active_local_users_in_room<'a>(
|
||||
) -> impl Stream<Item = OwnedUserId> + Send + 'a {
|
||||
self.local_users_in_room(room_id)
|
||||
.filter_map(async |user_id| {
|
||||
if self.services.users.status(&user_id).await.is_active() {
|
||||
if self.services.users.is_active(&user_id).await {
|
||||
Some(user_id)
|
||||
} else {
|
||||
None
|
||||
|
||||
@@ -29,10 +29,16 @@ pub async fn update_membership(
|
||||
update_joined_count: bool,
|
||||
) -> Result {
|
||||
let membership = pdu.get_content::<RoomMemberEventContent>()?;
|
||||
let is_local = self.services.globals.user_is_local(user_id);
|
||||
|
||||
if !is_local {
|
||||
self.services.users.record_remote_user(user_id);
|
||||
// Keep track what remote users exist by adding them as "deactivated" users
|
||||
//
|
||||
// TODO: use futures to update remote profiles without blocking the membership
|
||||
// update
|
||||
#[allow(clippy::collapsible_if)]
|
||||
if !self.services.globals.user_is_local(user_id)
|
||||
&& !self.services.users.exists(user_id).await
|
||||
{
|
||||
self.services.users.create(user_id, None)?;
|
||||
}
|
||||
|
||||
match &membership.membership {
|
||||
@@ -42,14 +48,13 @@ pub async fn update_membership(
|
||||
// Add the user ID to the join list then
|
||||
self.mark_as_once_joined(user_id, room_id);
|
||||
|
||||
// Copy data from the predecessor if the user is local
|
||||
if is_local
|
||||
&& let Ok(Some(predecessor)) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.map(|content: RoomCreateEventContent| content.predecessor)
|
||||
// Check if the room has a predecessor
|
||||
if let Ok(Some(predecessor)) = self
|
||||
.services
|
||||
.state_accessor
|
||||
.room_state_get_content(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
.map(|content: RoomCreateEventContent| content.predecessor)
|
||||
{
|
||||
// Copy old tags to new room
|
||||
if let Ok(tag_event) = self
|
||||
@@ -111,16 +116,13 @@ pub async fn update_membership(
|
||||
self.mark_as_joined(user_id, room_id);
|
||||
},
|
||||
| MembershipState::Invite => {
|
||||
let invite_state = if is_local {
|
||||
self.services
|
||||
.state
|
||||
.summary_stripped(pdu, room_id, user_id)
|
||||
.await
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let last_state = self
|
||||
.services
|
||||
.state
|
||||
.summary_stripped(pdu, room_id, user_id)
|
||||
.await;
|
||||
|
||||
self.mark_as_invited(user_id, room_id, pdu.sender(), invite_state, None)
|
||||
self.mark_as_invited(user_id, room_id, pdu.sender(), last_state, None)
|
||||
.await?;
|
||||
},
|
||||
| MembershipState::Leave | MembershipState::Ban => {
|
||||
@@ -133,13 +135,6 @@ pub async fn update_membership(
|
||||
self.update_joined_count(room_id).await;
|
||||
}
|
||||
|
||||
// Kick the target user's sync loop if they're local and this isn't a join to
|
||||
// make sure that membership changes like invites or invite rejections get
|
||||
// synced
|
||||
if is_local && !matches!(membership.membership, MembershipState::Join) {
|
||||
self.services.sync.wake(user_id).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -86,8 +86,6 @@ pub async fn append_incoming_pdu<'a, Leaves>(
|
||||
}
|
||||
}
|
||||
|
||||
self.services.sync.wake_all_joined(room_id).await;
|
||||
|
||||
Ok(Some(pdu_id))
|
||||
}
|
||||
|
||||
@@ -189,6 +187,18 @@ pub async fn append_pdu<'a, Leaves>(
|
||||
|
||||
let count1 = self.services.globals.next_count().unwrap();
|
||||
|
||||
// Mark as read first so the sending client doesn't get a notification even if
|
||||
// appending fails
|
||||
// TODO: Is this necessary? appending doesn't seem that fallible, and if it is,
|
||||
// there's bigger issues than ghost notifications.
|
||||
self.services
|
||||
.read_receipt
|
||||
.private_read_set(room_id, pdu.sender(), count1);
|
||||
|
||||
self.services
|
||||
.user
|
||||
.reset_notification_counts(pdu.sender(), room_id);
|
||||
|
||||
let count2 = PduCount::Normal(self.services.globals.next_count().unwrap());
|
||||
let pdu_id: RawPduId = PduId { shortroomid, shorteventid: count2 }.into();
|
||||
|
||||
@@ -213,14 +223,6 @@ pub async fn append_pdu<'a, Leaves>(
|
||||
);
|
||||
}
|
||||
|
||||
self.services
|
||||
.read_receipt
|
||||
.private_read_set(room_id, pdu.sender(), count1);
|
||||
|
||||
self.services
|
||||
.user
|
||||
.reset_notification_counts(pdu.sender(), room_id);
|
||||
|
||||
self.send_to_interested_appservices(pdu, &pdu_id, room_id)
|
||||
.await;
|
||||
|
||||
@@ -253,13 +255,7 @@ async fn notify_local_users(&self, pdu: &PduEvent, pdu_id: &RawPduId, room_id: &
|
||||
if let Some(state_key) = pdu.state_key() {
|
||||
match UserId::parse(state_key) {
|
||||
| Ok(target_user_id) => {
|
||||
if self
|
||||
.services
|
||||
.users
|
||||
.status(&target_user_id)
|
||||
.await
|
||||
.is_active()
|
||||
{
|
||||
if self.services.users.is_active_local(&target_user_id).await {
|
||||
push_targets.insert(target_user_id.clone());
|
||||
}
|
||||
},
|
||||
|
||||
@@ -153,8 +153,6 @@ pub async fn build_and_append_pdu(
|
||||
.state
|
||||
.set_room_state(&room_id, statehashid, state_lock);
|
||||
|
||||
self.services.sync.wake_all_joined(&room_id).await;
|
||||
|
||||
let mut servers: HashSet<OwnedServerName> = self
|
||||
.services
|
||||
.state_cache
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
use self::data::Data;
|
||||
pub use self::{create::pdu_fits, data::PdusIterItem};
|
||||
use crate::{
|
||||
Dep, account_data, admin, appservice, globals, pusher, rooms, sending, server_keys, sync,
|
||||
users,
|
||||
Dep, account_data, admin, appservice, globals, pusher, rooms, sending, server_keys, users,
|
||||
};
|
||||
|
||||
// Update Relationships
|
||||
@@ -66,22 +65,21 @@ struct Services {
|
||||
appservice: Dep<appservice::Service>,
|
||||
admin: Dep<admin::Service>,
|
||||
alias: Dep<rooms::alias::Service>,
|
||||
event_handler: Dep<rooms::event_handler::Service>,
|
||||
globals: Dep<globals::Service>,
|
||||
pdu_metadata: Dep<rooms::pdu_metadata::Service>,
|
||||
pusher: Dep<pusher::Service>,
|
||||
read_receipt: Dep<rooms::read_receipt::Service>,
|
||||
search: Dep<rooms::search::Service>,
|
||||
sending: Dep<sending::Service>,
|
||||
server_keys: Dep<server_keys::Service>,
|
||||
short: Dep<rooms::short::Service>,
|
||||
state: Dep<rooms::state::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
state_cache: Dep<rooms::state_cache::Service>,
|
||||
sync: Dep<sync::Service>,
|
||||
threads: Dep<rooms::threads::Service>,
|
||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||
pdu_metadata: Dep<rooms::pdu_metadata::Service>,
|
||||
read_receipt: Dep<rooms::read_receipt::Service>,
|
||||
sending: Dep<sending::Service>,
|
||||
server_keys: Dep<server_keys::Service>,
|
||||
user: Dep<rooms::user::Service>,
|
||||
users: Dep<users::Service>,
|
||||
pusher: Dep<pusher::Service>,
|
||||
threads: Dep<rooms::threads::Service>,
|
||||
search: Dep<rooms::search::Service>,
|
||||
event_handler: Dep<rooms::event_handler::Service>,
|
||||
}
|
||||
|
||||
type RoomMutexMap = MutexMap<OwnedRoomId, ()>;
|
||||
@@ -97,24 +95,23 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
appservice: args.depend::<appservice::Service>("appservice"),
|
||||
admin: args.depend::<admin::Service>("admin"),
|
||||
alias: args.depend::<rooms::alias::Service>("rooms::alias"),
|
||||
event_handler: args
|
||||
.depend::<rooms::event_handler::Service>("rooms::event_handler"),
|
||||
globals: args.depend::<globals::Service>("globals"),
|
||||
pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"),
|
||||
pusher: args.depend::<pusher::Service>("pusher"),
|
||||
read_receipt: args.depend::<rooms::read_receipt::Service>("rooms::read_receipt"),
|
||||
search: args.depend::<rooms::search::Service>("rooms::search"),
|
||||
sending: args.depend::<sending::Service>("sending"),
|
||||
server_keys: args.depend::<server_keys::Service>("server_keys"),
|
||||
short: args.depend::<rooms::short::Service>("rooms::short"),
|
||||
state: args.depend::<rooms::state::Service>("rooms::state"),
|
||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||
state_accessor: args
|
||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||
sync: args.depend::<sync::Service>("sync"),
|
||||
threads: args.depend::<rooms::threads::Service>("rooms::threads"),
|
||||
pdu_metadata: args.depend::<rooms::pdu_metadata::Service>("rooms::pdu_metadata"),
|
||||
read_receipt: args.depend::<rooms::read_receipt::Service>("rooms::read_receipt"),
|
||||
sending: args.depend::<sending::Service>("sending"),
|
||||
server_keys: args.depend::<server_keys::Service>("server_keys"),
|
||||
user: args.depend::<rooms::user::Service>("rooms::user"),
|
||||
users: args.depend::<users::Service>("users"),
|
||||
pusher: args.depend::<pusher::Service>("pusher"),
|
||||
threads: args.depend::<rooms::threads::Service>("rooms::threads"),
|
||||
search: args.depend::<rooms::search::Service>("rooms::search"),
|
||||
event_handler: args
|
||||
.depend::<rooms::event_handler::Service>("rooms::event_handler"),
|
||||
},
|
||||
db: Data::new(&args),
|
||||
mutex_insert: RoomMutexMap::new(),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user