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
|
||||
|
||||
Generated
+295
-704
File diff suppressed because it is too large
Load Diff
+1
-13
@@ -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]
|
||||
@@ -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 @@
|
||||
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"
|
||||
|
||||
+2
-2
@@ -36,8 +36,8 @@
|
||||
},
|
||||
{
|
||||
"type": "dir",
|
||||
"name": "guides",
|
||||
"label": "Guides"
|
||||
"name": "advanced",
|
||||
"label": "Advanced"
|
||||
},
|
||||
"security",
|
||||
{
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
[
|
||||
{
|
||||
"text": "Documentation",
|
||||
"text": "Guide",
|
||||
"link": "/introduction",
|
||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting|guides)"
|
||||
"activeMatch": "^/(introduction|configuration|deploying|calls|appservices|maintenance|troubleshooting|advanced)"
|
||||
},
|
||||
{
|
||||
"text": "Development",
|
||||
|
||||
@@ -13,10 +13,6 @@
|
||||
"type": "file",
|
||||
"name": "performance",
|
||||
"label": "Performance tuning"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "oidc",
|
||||
"label": "Delegated authentication"
|
||||
}
|
||||
|
||||
]
|
||||
@@ -58,13 +58,6 @@ # Defaults to members of the admin room if unset
|
||||
|
||||
To enable full discovery, you will need to reverse proxy these paths from the base domain back to Continuwuity.
|
||||
|
||||
:::warning
|
||||
If you choose to serve the well-known files directly through your reverse proxy, and you are
|
||||
using delegation, you must still set the `global.well_known.client` configuration option to
|
||||
the domain you're delegating to. Continuwuity needs to know the domain it runs on for OAuth-compatible
|
||||
clients to work correctly.
|
||||
:::
|
||||
|
||||
## Reverse proxying well-known files to Continuwuity
|
||||
|
||||
<details>
|
||||
@@ -16,8 +16,8 @@
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
"name": "rpm",
|
||||
"label": "RPM"
|
||||
"name": "fedora",
|
||||
"label": "Fedora"
|
||||
},
|
||||
{
|
||||
"type": "file",
|
||||
|
||||
@@ -44,7 +44,7 @@ ### Prerequisites
|
||||
- Alternatively, if you want both client and federation traffic on `:443`, you can configure `CONTINUWUITY_WELL_KNOWN` following some of the [examples](#choose-your-reverse-proxy) below.
|
||||
|
||||
:::tip Split-domain setups
|
||||
For more setups with `.well-known` delegation and split-domain deployments, consult the [Delegation/Split-domain](../guides/delegation) page.
|
||||
For more setups with `.well-known` delegation and split-domain deployments, consult the [Delegation/Split-domain](../advanced/delegation) page.
|
||||
:::
|
||||
|
||||
## Docker Compose
|
||||
@@ -79,7 +79,7 @@ ### Choose Your Reverse Proxy
|
||||
nameserver 1.1.1.1
|
||||
```
|
||||
|
||||
Consult the [**DNS tuning guide (recommended)**](../guides/dns.mdx) for full solutions to this issue.
|
||||
Consult the [**DNS tuning guide (recommended)**](../advanced/dns.mdx) for full solutions to this issue.
|
||||
:::
|
||||
|
||||
#### Caddy (using Caddyfile)
|
||||
@@ -269,7 +269,7 @@ ### Accessing the Server's Console
|
||||
|
||||
## Next steps
|
||||
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../guides/dns.mdx) (recommended)
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
|
||||
- To set up Audio/Video communication, see the [**Calls**](../calls.mdx) page.
|
||||
- Consult the [Maintenance](../maintenance.mdx) page for guidance on maintaining your homeserver.
|
||||
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.mdx).
|
||||
|
||||
@@ -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
|
||||
```
|
||||
@@ -170,7 +170,7 @@ ## Exposing ports in the firewall or the router
|
||||
are: `ufw allow 8448/tcp` and `ufw allow 443/tcp`.
|
||||
|
||||
:::tip Alternative port/domain setups
|
||||
If you would like to use only port 443, a different port, or a subdomain for the homeserver, you will need to set up `.well-known` delegation. Consult the `[global.well_known]` section of the config file, and the [**Delegation/Split-domain**](../guides/delegation) page to learn more about these kinds of deployments.
|
||||
If you would like to use only port 443, a different port, or a subdomain for the homeserver, you will need to set up `.well-known` delegation. Consult the `[global.well_known]` section of the config file, and the [**Delegation/Split-domain**](../advanced/delegation) page to learn more about these kinds of deployments.
|
||||
:::
|
||||
|
||||
## Setting up the Reverse Proxy
|
||||
@@ -300,8 +300,7 @@ # For client-server endpoints
|
||||
|
||||
## What's next?
|
||||
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../guides/dns.mdx) (recommended)
|
||||
- To configure OIDC login with an identity provider, see the [**delegated authentication guide**](../guides/oidc.mdx).
|
||||
- For smooth federation, set up a caching resolver according to the [**DNS tuning guide**](../advanced/dns.mdx) (recommended)
|
||||
- For Audio/Video call functionality see the [**Calls**](../calls.md) page.
|
||||
- Consult the [Maintenance](../maintenance.mdx) page for guidance on maintaining your homeserver.
|
||||
- If you want to set up an appservice, take a look at the [**Appservice Guide**](../appservices.md).
|
||||
|
||||
@@ -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>
|
||||
@@ -1,56 +0,0 @@
|
||||
# Configuring delegated authentication
|
||||
|
||||
Continuwuity supports delegating user authentication to an external identity provider that implements the OpenID Connect specification, such as Authentik, kanidm, or Keycloak.
|
||||
|
||||
:::warning{title="OIDC versus OAuth, and supported clients"}
|
||||
**OIDC** is not to be confused with **OAuth**. In the context of Matrix, OAuth is the protocol that Matrix clients use to authenticate with the _homeserver_. OIDC is the protocol that the _homeserver_ uses to communicate with the _identity provider_. Continuwuity supports OAuth by default, alongside the legacy **UIAA** authentication framework.
|
||||
|
||||
When OIDC is configured, Continuwuity will disable its support for legacy authentication. **Only clients that support OAuth**, such as the Element family of clients, will be able to log in when OIDC is configured. If your client of choice shows an error when you try to log in after configuring OIDC, it likely does not support OAuth. This is an issue with your client, not Continuwuity, and should be reported to your client's developers.
|
||||
:::
|
||||
|
||||
A simple OIDC configuration is as easy as creating a new OIDC application in your identity provider's settings and supplying Continuwuity with the client ID and client secret. This guide will use kanidm as an example, but the described steps are broadly applicable to other identity providers.
|
||||
|
||||
First, create a new application for Continuwuity in your identity provider.
|
||||
```sh
|
||||
# Here, `c10y` is the client ID that kanidm will use, and `Continuwuity` is the display name.
|
||||
# Other identity providers may generate a client ID for you.
|
||||
# Use the domain that clients can reach Continuwuity at, which may not be the same as your server name
|
||||
# if you have configured well-known delegation.
|
||||
kanidm system oauth2 create c10y Continuwuity https://matrix.yourdomain.com
|
||||
```
|
||||
|
||||
Configure the redirect URL that Continuwuity uses.
|
||||
```sh
|
||||
kanidm system oauth2 add-redirect-url c10y https://matrix.yourdomain.com/_continuwuity/oidc/complete
|
||||
```
|
||||
|
||||
Allow Continuwuity to request the `openid` scope. Other identity providers may not require this step.
|
||||
```sh
|
||||
kanidm system oauth2 update-scope-map c10y idm_all_persons openid
|
||||
```
|
||||
|
||||
Find the client secret that was generated. Other identity providers may show this information in their web UI.
|
||||
```sh
|
||||
kanidm system oauth2 show-basic-secret c10y
|
||||
d1qgx352kkuvs1j70b6w293d65x68jve1f7b27fyk90gjhpr
|
||||
```
|
||||
|
||||
Configure Continuwuity with the client ID, client secret, and discovery URL. kanidm has a different discovery URL for each client, but other identity providers may have a single discovery URL at the root of their domain.
|
||||
|
||||
```toml
|
||||
[global.oauth.oidc]
|
||||
|
||||
# `/.well-known/openid-configuration` will be appended automatically
|
||||
discovery_url = "https://idm.example.com/oauth2/openid/c10y"
|
||||
|
||||
# This may be randomly generated by your identity provider. kanidm requires
|
||||
# you to set it manually when you create the application.
|
||||
client_id = "c10y"
|
||||
|
||||
# From the previous step
|
||||
client_secret = "d1qgx352kkuvs1j70b6w293d65x68jve1f7b27fyk90gjhpr"
|
||||
```
|
||||
|
||||
Finally, restart Continuwuity, and log out and back in again. Your client should prompt you to continue in your web browser and open a webpage with the Continuwuity logo that allows you to continue in your identity provider. Once you log in successfully, you will be prompted to choose a user ID -- to link your existing account, enter its user ID, and then your old password when prompted.
|
||||
|
||||
Continuwuity offers several additional configuration options to tweak its integration with your identity provider. Review the `[global.oauth.oidc]` section towards the bottom of the [reference configuration](../reference/config) for a complete list of options and documentation.
|
||||
@@ -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
|
||||
|
||||
@@ -60,7 +60,7 @@ ### DNS server overload
|
||||
|
||||
Matrix federation is extremely heavy and sends wild amounts of DNS requests. This makes normal resolvers like the ones above unsuitable for its activity. Ultimately, the best solution/fix for this is to selfhost a high quality caching DNS resolver such as Unbound, and configure Continuwuity to use it.
|
||||
|
||||
Follow the [**DNS tuning guide**](./guides/dns) for details on setting it up.
|
||||
Follow the [**DNS tuning guide**](./advanced/dns) for details on setting it up.
|
||||
|
||||
### Intermittent federation failures to a specific server
|
||||
|
||||
|
||||
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!(
|
||||
|
||||
+63
-53
@@ -20,7 +20,7 @@
|
||||
tag::{TagEvent, TagEventContent, TagInfo},
|
||||
},
|
||||
};
|
||||
use service::users::{AccountStatus, HashedPassword};
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::{
|
||||
get_room_info,
|
||||
@@ -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
|
||||
@@ -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?;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}")]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -786,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()
|
||||
@@ -830,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); }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user