mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-18 07:16:17 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba0ec6ba70 | ||
|
|
1ac05838dd | ||
|
|
7ecd2aa5e2 | ||
|
|
69a3145983 | ||
|
|
73b7553b1e | ||
|
|
232ec3f620 | ||
|
|
e52bbeaf94 | ||
|
|
ad1c194fb0 | ||
|
|
5e2dcd8e79 | ||
|
|
24c6474bd1 | ||
|
|
642d05048b | ||
|
|
446ae93ad7 | ||
|
|
9ca0774b2d | ||
|
|
ce494eb0c0 | ||
|
|
1f5e178c3f | ||
|
|
ddd050d402 | ||
|
|
6ad3e679bc | ||
|
|
65812bb246 | ||
|
|
9f3a7994c7 | ||
|
|
baef3289fe | ||
|
|
9a94b93ddd | ||
|
|
0ac0bd93ae | ||
|
|
fe9f718c6e | ||
|
|
e333f5a8e5 | ||
|
|
b8c64a68d4 | ||
|
|
163f049757 | ||
|
|
f48125c37a | ||
|
|
4001b99261 | ||
|
|
b6e5a106ff | ||
|
|
e1eea45fd6 | ||
|
|
7cb53f41d6 | ||
|
|
fbf81fcdeb | ||
|
|
87030a3a22 | ||
|
|
4f509fa113 | ||
|
|
aa7ed885c0 | ||
|
|
d5e6e617d1 | ||
|
|
1f7680ad5f | ||
|
|
0c37a542d4 | ||
|
|
efd07da0d7 | ||
|
|
f0590e882a | ||
|
|
7225bf25ae | ||
|
|
e52bb5db69 | ||
|
|
995726923b | ||
|
|
556141b404 | ||
|
|
6858e1b893 | ||
|
|
07e241f616 | ||
|
|
cfbe590f1c | ||
|
|
d9c2f04d73 | ||
|
|
ada0b3184b | ||
|
|
1d6ad485f6 | ||
|
|
9d2ce72f73 | ||
|
|
13bcb5af13 | ||
|
|
5868510df1 | ||
|
|
c34d1fe76e | ||
|
|
838764b0ac | ||
|
|
b51a34c1c1 | ||
|
|
2fbb779d12 | ||
|
|
e16a62e1ed | ||
|
|
64a08262ca | ||
|
|
70a3905761 | ||
|
|
cf9179e8f6 | ||
|
|
6b899b7f80 | ||
|
|
ad57fa06fc | ||
|
|
ec9efbada5 | ||
|
|
318d2b205a | ||
|
|
99a5f2a9fb | ||
|
|
ace0d52074 |
@@ -71,7 +71,7 @@ runs:
|
|||||||
|
|
||||||
- name: Install timelord-cli and git-warp-time
|
- name: Install timelord-cli and git-warp-time
|
||||||
if: steps.check-binaries.outputs.need-install == 'true'
|
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@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
|
||||||
with:
|
with:
|
||||||
tool: git-warp-time,timelord-cli@3.0.1
|
tool: git-warp-time,timelord-cli@3.0.1
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ jobs:
|
|||||||
path: binaries
|
path: binaries
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Create Release and Upload
|
- name: Create Release and Upload
|
||||||
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
uses: https://github.com/softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
|
if: steps.runner-env.outputs.node_major == '' || steps.runner-env.outputs.node_major < '20'
|
||||||
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
uses: https://github.com/actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: 📦 Setup Node.js
|
- name: 📦 Setup Node.js
|
||||||
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
uses: https://github.com/actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
|
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ jobs:
|
|||||||
path: binaries
|
path: binaries
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Create Release and Upload
|
- name: Create Release and Upload
|
||||||
uses: https://github.com/softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3
|
uses: https://github.com/softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
files: binaries/*
|
files: binaries/*
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
name: Renovate
|
name: Renovate
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
|
image: ghcr.io/renovatebot/renovate:43.260.2@sha256:60d830108d9ff6408d11a55d5f110ee0976bed78a6b82b08211e3ddb72f72952
|
||||||
options: --tmpfs /tmp:exec
|
options: --tmpfs /tmp:exec
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
# Continuwuity 26.6.2 (2026-07-12)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fixed the server returning 500 errors if `admin_console_automatic` is enabled and no TTY is available. Contributed by @s1lv3r. (#1975)
|
||||||
|
- Fixed `global.oauth.compatibility_mode` being required, despite being ignored, when the `[global.oauth.oidc]` config section is provided.
|
||||||
|
- Fixed an issue with a migration that could cause user accounts imported from an identity provider to be marked as deactivated when the server started. If you have accounts affected by this issue, use `!admin users reset-password --convert-to-local-account` to reactivate them.
|
||||||
|
|
||||||
|
|
||||||
|
# Continuwuity 26.6.1 (2026-07-12)
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Added enforcement for new federated invite checks and corrected a bunch of related spec compliance issues along the way. Contributed by @nex. (#1952)
|
||||||
|
|
||||||
|
## Bugfixes
|
||||||
|
|
||||||
|
- Fixed existing accounts failing to link when logging in with OIDC if `prompt_for_localpart` was `false`. (#1942)
|
||||||
|
- Authentication is no longer required on the `/_matrix/client/v3/account/3pid/email/requestToken` endpoint. (#1953)
|
||||||
|
- Fixed newly created rooms failing to sync properly in clients using legacy sync.
|
||||||
|
- Stopped appservice users from being erroneously marked as deactivated during a 26.6 database migration.
|
||||||
|
- Whitespace will now automatically be trimmed from the start and end of the `global.oauth.oidc.client_secret_file`.
|
||||||
|
|
||||||
|
|
||||||
# Continuwuity 26.6.0 (2026-07-10)
|
# Continuwuity 26.6.0 (2026-07-10)
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|||||||
+2
-2
@@ -26,7 +26,7 @@ ### Pre-commit Checks
|
|||||||
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install prek using cargo-binstall
|
# Install prek using cargo-binstall https://github.com/cargo-bins/cargo-binstall
|
||||||
cargo binstall prek
|
cargo binstall prek
|
||||||
|
|
||||||
# Install git hooks to run checks automatically
|
# Install git hooks to run checks automatically
|
||||||
@@ -155,7 +155,7 @@ ### Creating pull requests
|
|||||||
|
|
||||||
Before submitting a pull request, please ensure:
|
Before submitting a pull request, please ensure:
|
||||||
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
|
1. Your code passes all CI checks (formatting, linting, typo detection, etc.)
|
||||||
2. Your code follows the [code style guide](/development/code_style.md)
|
2. Your code follows the [code style guide](docs/development/code_style.mdx)
|
||||||
3. Your commit messages follow the conventional commits format
|
3. Your commit messages follow the conventional commits format
|
||||||
4. Tests are added for new functionality
|
4. Tests are added for new functionality
|
||||||
5. Documentation is updated if needed
|
5. Documentation is updated if needed
|
||||||
|
|||||||
Generated
+150
-247
File diff suppressed because it is too large
Load Diff
+4
-10
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
|||||||
# See also `rust-toolchain.toml`
|
# See also `rust-toolchain.toml`
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||||
version = "26.6.0"
|
version = "26.6.2"
|
||||||
|
|
||||||
[workspace.metadata.crane]
|
[workspace.metadata.crane]
|
||||||
name = "conduwuit"
|
name = "conduwuit"
|
||||||
@@ -141,12 +141,6 @@ features = [
|
|||||||
version = "0.23.25"
|
version = "0.23.25"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.aws-lc-sys]
|
|
||||||
version = "0.41.0"
|
|
||||||
|
|
||||||
[workspace.dependencies.aws-lc-rs]
|
|
||||||
version = "1.17.0"
|
|
||||||
|
|
||||||
[workspace.dependencies.reqwest]
|
[workspace.dependencies.reqwest]
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
default-features = false
|
default-features = false
|
||||||
@@ -350,7 +344,7 @@ version = "1.1.1"
|
|||||||
[workspace.dependencies.ruma]
|
[workspace.dependencies.ruma]
|
||||||
# version = "0.14.1"
|
# version = "0.14.1"
|
||||||
git = "https://github.com/ruma/ruma.git"
|
git = "https://github.com/ruma/ruma.git"
|
||||||
rev = "fca1dbc060f730c1aaf84c9c2fb1e8a587455be8"
|
rev = "9b6a2e7323649af926e4b5363d87239ee4247f4a"
|
||||||
features = [
|
features = [
|
||||||
"appservice-api-c",
|
"appservice-api-c",
|
||||||
"client-api",
|
"client-api",
|
||||||
@@ -502,7 +496,7 @@ version = "0.4.9"
|
|||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.termimad]
|
[workspace.dependencies.termimad]
|
||||||
version = "0.34.0"
|
version = "0.35.0"
|
||||||
default-features = false
|
default-features = false
|
||||||
|
|
||||||
[workspace.dependencies.checked_ops]
|
[workspace.dependencies.checked_ops]
|
||||||
@@ -570,7 +564,7 @@ features = ["std"]
|
|||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
|
|
||||||
[workspace.dependencies.resolvematrix]
|
[workspace.dependencies.resolvematrix]
|
||||||
version = "1.1.0"
|
version = "1.2.0"
|
||||||
|
|
||||||
[workspace.dependencies.serde_urlencoded]
|
[workspace.dependencies.serde_urlencoded]
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
Appservices are now properly able to create devices for E2EE.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed newly created rooms failing to sync properly in clients using legacy sync.
|
||||||
@@ -1 +0,0 @@
|
|||||||
Stopped appservice users from being erroneously marked as deactivated during a 26.6 database migration.
|
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Appservices may now specify both the unstable and stable `device_id` query parameters in a request. The stable parameter will take priority. Contributed by @ginger.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fixed the deeplink redirect for deleting devices. Contributed by @koen
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Fix status code for oauth registration. Contributed by @n00byking
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Exempt m.room.create from auth_events check. Contributed by @eleboucher
|
||||||
@@ -6,10 +6,10 @@
|
|||||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 15,
|
"id": 16,
|
||||||
"mention_room": true,
|
"mention_room": true,
|
||||||
"date": "2026-07-10",
|
"date": "2026-07-13",
|
||||||
"message": "[Continuwuity 26.6.0](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.0) is finally out! This is by far our largest release yet, and it wouldn't have been possible without our community. Thank you so much for all of your patience, support, and/or development time over the last three months <3"
|
"message": "[Continuwuity 26.6.2](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.1) has just been released! This release fixes a severe bug with OIDC that could cause users' accounts to be flagged as deactivated. If you use OIDC, please update as soon as possible."
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+21
-21
@@ -3,11 +3,11 @@
|
|||||||
"advisory-db": {
|
"advisory-db": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781566179,
|
"lastModified": 1783840254,
|
||||||
"narHash": "sha256-Tqv8I586fYzWpEW/Smq/JqESFa3DVVzVWsnAMtvhy/I=",
|
"narHash": "sha256-XjyvZk0f3YiinVHmkGOotmLBAzvK+LwEJYj2QqJ5pn8=",
|
||||||
"owner": "rustsec",
|
"owner": "rustsec",
|
||||||
"repo": "advisory-db",
|
"repo": "advisory-db",
|
||||||
"rev": "74e084413d979d52d2f93b1d93b1ab7b9ee648f5",
|
"rev": "6e3286f4efa8c142fb33e5ea4342c8db6693cf34",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -18,11 +18,11 @@
|
|||||||
},
|
},
|
||||||
"crane": {
|
"crane": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1780532242,
|
"lastModified": 1783203018,
|
||||||
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
|
"narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
|
"rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -39,11 +39,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781527054,
|
"lastModified": 1783844668,
|
||||||
"narHash": "sha256-1fX9ev2Fh5QoKQ41G9dYutjo5j/jywu6tZse5Eb1Ck4=",
|
"narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "8c2e51dffefc040a21975da7abf6f252c8c9b783",
|
"rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -74,11 +74,11 @@
|
|||||||
"nixpkgs-lib": "nixpkgs-lib"
|
"nixpkgs-lib": "nixpkgs-lib"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778716662,
|
"lastModified": 1782949081,
|
||||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||||
"owner": "hercules-ci",
|
"owner": "hercules-ci",
|
||||||
"repo": "flake-parts",
|
"repo": "flake-parts",
|
||||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -89,11 +89,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781074563,
|
"lastModified": 1783776592,
|
||||||
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
|
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
||||||
"owner": "NixOS",
|
"owner": "NixOS",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
|
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -105,11 +105,11 @@
|
|||||||
},
|
},
|
||||||
"nixpkgs-lib": {
|
"nixpkgs-lib": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777168982,
|
"lastModified": 1782614948,
|
||||||
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "nixpkgs.lib",
|
"repo": "nixpkgs.lib",
|
||||||
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -132,11 +132,11 @@
|
|||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1781453968,
|
"lastModified": 1783779020,
|
||||||
"narHash": "sha256-+V3nK4pCngbmgyVGXY6Kkrlevp4ocPkJJLf2aqwkDNA=",
|
"narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "cc272809a173c2c11d0e479d639c811c1eacf049",
|
"rev": "5be5e89cf0145d73a85c805726821b4adfc3af48",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ let
|
|||||||
env = {
|
env = {
|
||||||
CARGO_PROFILE = profile;
|
CARGO_PROFILE = profile;
|
||||||
RUSTFLAGS = rustflags;
|
RUSTFLAGS = rustflags;
|
||||||
GIT_COMMIT_HASH = self.rev or self.dirtyRev;
|
GIT_COMMIT_HASH = self.rev or self.dirtyRev or "";
|
||||||
GIT_COMMIT_HASH_SHORT = self.shortRev or self.dirtyShortRev;
|
GIT_COMMIT_HASH_SHORT = self.shortRev or self.dirtyShortRev or "";
|
||||||
}
|
}
|
||||||
// (lib.optionalAttrs (rocksdb != null) {
|
// (lib.optionalAttrs (rocksdb != null) {
|
||||||
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
|
ROCKSDB_INCLUDE_DIR = "${rocksdb}/include";
|
||||||
|
|||||||
Generated
+48
-48
@@ -510,14 +510,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/core": {
|
"node_modules/@shikijs/core": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz",
|
||||||
"integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==",
|
"integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/primitive": "4.2.0",
|
"@shikijs/primitive": "4.3.1",
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"hast-util-to-html": "^9.0.5"
|
"hast-util-to-html": "^9.0.5"
|
||||||
@@ -527,13 +527,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/engine-javascript": {
|
"node_modules/@shikijs/engine-javascript": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz",
|
||||||
"integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==",
|
"integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"oniguruma-to-es": "^4.3.6"
|
"oniguruma-to-es": "^4.3.6"
|
||||||
},
|
},
|
||||||
@@ -542,13 +542,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/engine-oniguruma": {
|
"node_modules/@shikijs/engine-oniguruma": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz",
|
||||||
"integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==",
|
"integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2"
|
"@shikijs/vscode-textmate": "^10.0.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -556,26 +556,26 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/langs": {
|
"node_modules/@shikijs/langs": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz",
|
||||||
"integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==",
|
"integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0"
|
"@shikijs/types": "4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/primitive": {
|
"node_modules/@shikijs/primitive": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz",
|
||||||
"integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==",
|
"integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"@types/hast": "^3.0.4"
|
"@types/hast": "^3.0.4"
|
||||||
},
|
},
|
||||||
@@ -584,16 +584,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/rehype": {
|
"node_modules/@shikijs/rehype": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.3.1.tgz",
|
||||||
"integrity": "sha512-ST3EWye/dwF1gWskczJNBnwFtDzEQ9ceytXZtyc/GfwR5V0qJrkoSGZO55O3SAKDDsXkTDcsfwd9pVe7ROlAHg==",
|
"integrity": "sha512-oshrlfUF3VPUJfnp5K1lLwsS/SRBKrIxONpdWebSKZXdBE3UsZnxgqpvRUA8UsofS7vmjFOCAHIT71ECbmOxTw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@types/hast": "^3.0.4",
|
"@types/hast": "^3.0.4",
|
||||||
"hast-util-to-string": "^3.0.1",
|
"hast-util-to-string": "^3.0.1",
|
||||||
"shiki": "4.2.0",
|
"shiki": "4.3.1",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.1.0"
|
"unist-util-visit": "^5.1.0"
|
||||||
},
|
},
|
||||||
@@ -602,22 +602,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/themes": {
|
"node_modules/@shikijs/themes": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz",
|
||||||
"integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==",
|
"integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/types": "4.2.0"
|
"@shikijs/types": "4.3.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/types": {
|
"node_modules/@shikijs/types": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz",
|
||||||
"integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==",
|
"integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -684,9 +684,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/hast": {
|
"node_modules/@types/hast": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
|
||||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1076,9 +1076,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ungap/structured-clone": {
|
"node_modules/@ungap/structured-clone": {
|
||||||
"version": "1.3.1",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
|
||||||
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
|
"integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -3589,18 +3589,18 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/shiki": {
|
"node_modules/shiki": {
|
||||||
"version": "4.2.0",
|
"version": "4.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz",
|
||||||
"integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==",
|
"integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@shikijs/core": "4.2.0",
|
"@shikijs/core": "4.3.1",
|
||||||
"@shikijs/engine-javascript": "4.2.0",
|
"@shikijs/engine-javascript": "4.3.1",
|
||||||
"@shikijs/engine-oniguruma": "4.2.0",
|
"@shikijs/engine-oniguruma": "4.3.1",
|
||||||
"@shikijs/langs": "4.2.0",
|
"@shikijs/langs": "4.3.1",
|
||||||
"@shikijs/themes": "4.2.0",
|
"@shikijs/themes": "4.3.1",
|
||||||
"@shikijs/types": "4.2.0",
|
"@shikijs/types": "4.3.1",
|
||||||
"@shikijs/vscode-textmate": "^10.0.2",
|
"@shikijs/vscode-textmate": "^10.0.2",
|
||||||
"@types/hast": "^3.0.4"
|
"@types/hast": "^3.0.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -486,7 +486,7 @@ pub(super) async fn get_remote_pdu(
|
|||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.parse_incoming_pdu(&response.pdu)
|
.parse_incoming_pdu(&response.pdu, None)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -832,7 +832,7 @@ pub(super) async fn force_set_room_state_from_server(
|
|||||||
.services
|
.services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.parse_incoming_pdu(&pdu)
|
.parse_incoming_pdu(&pdu, Some(&room_version_rules))
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
| Ok(t) => t,
|
| Ok(t) => t,
|
||||||
@@ -1014,7 +1014,7 @@ pub(super) async fn resolve_true_destination(
|
|||||||
let resolver: &MatrixResolver = if no_cache {
|
let resolver: &MatrixResolver = if no_cache {
|
||||||
&MatrixResolverBuilder::new()
|
&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)
|
.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())
|
.http_client(self.services.client.dns.clone())
|
||||||
.build()?
|
.build()?
|
||||||
} else {
|
} else {
|
||||||
&self.services.client.matrix_resolver
|
&self.services.client.matrix_resolver
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ pub(super) async fn fetch_support_well_known(&self, server_name: OwnedServerName
|
|||||||
let response = self
|
let response = self
|
||||||
.services
|
.services
|
||||||
.client
|
.client
|
||||||
.default
|
.external_resource
|
||||||
.get(format!("https://{server_name}/.well-known/matrix/support"))
|
.get(format!("https://{server_name}/.well-known/matrix/support"))
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
};
|
};
|
||||||
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
|
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::DEVICE_ID_LENGTH;
|
||||||
use crate::{Ruma, router::ClientIdentity};
|
use crate::{Ruma, router::ClientIdentity};
|
||||||
|
|
||||||
pub(crate) mod register;
|
pub(crate) mod register;
|
||||||
|
|||||||
@@ -20,9 +20,12 @@
|
|||||||
assign,
|
assign,
|
||||||
};
|
};
|
||||||
use serde_json::value::RawValue;
|
use serde_json::value::RawValue;
|
||||||
use service::{mailer::messages, users::HashedPassword};
|
use service::{
|
||||||
|
mailer::messages,
|
||||||
|
users::{DeviceToken, HashedPassword},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::DEVICE_ID_LENGTH;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `POST /_matrix/client/v3/register`
|
/// # `POST /_matrix/client/v3/register`
|
||||||
@@ -119,7 +122,7 @@ pub(crate) async fn register_route(
|
|||||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||||
|
|
||||||
// Generate new token for the device
|
// Generate new token for the device
|
||||||
let new_token = utils::random_string(TOKEN_LENGTH);
|
let new_token = DeviceToken::new_random();
|
||||||
|
|
||||||
// Create device for this account
|
// Create device for this account
|
||||||
services
|
services
|
||||||
@@ -127,8 +130,7 @@ pub(crate) async fn register_route(
|
|||||||
.create_device(
|
.create_device(
|
||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
&device_id,
|
||||||
&new_token,
|
Some(new_token.clone()),
|
||||||
None,
|
|
||||||
body.initial_device_display_name.clone(),
|
body.initial_device_display_name.clone(),
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
@@ -142,7 +144,7 @@ pub(crate) async fn register_route(
|
|||||||
debug_info!(%user_id, ?device, "New account created via legacy registration");
|
debug_info!(%user_id, ?device, "New account created via legacy registration");
|
||||||
|
|
||||||
Ok(assign!(register::v3::Response::new(user_id), {
|
Ok(assign!(register::v3::Response::new(user_id), {
|
||||||
access_token: token,
|
access_token: token.map(DeviceToken::into_token),
|
||||||
device_id: device,
|
device_id: device,
|
||||||
refresh_token: None,
|
refresh_token: None,
|
||||||
expires_in: None,
|
expires_in: None,
|
||||||
|
|||||||
@@ -57,13 +57,11 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
|
||||||
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
) -> Result<request_3pid_management_token_via_email::v3::Response> {
|
||||||
// Authentication for this endpoint is technically optional,
|
|
||||||
// but we require the user to be logged in
|
|
||||||
let sender_user = body
|
let sender_user = body
|
||||||
.identity
|
.identity
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(ClientIdentity::expect_sender_user)
|
.map(ClientIdentity::expect_sender_user)
|
||||||
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))??;
|
.transpose()?;
|
||||||
|
|
||||||
if !services.threepid.email_requirement().may_change() {
|
if !services.threepid.email_requirement().may_change() {
|
||||||
return Err!(Request(Forbidden("You may not change your email address.")));
|
return Err!(Request(Forbidden("You may not change your email address.")));
|
||||||
@@ -88,7 +86,7 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
|
|||||||
Mailbox::new(None, email),
|
Mailbox::new(None, email),
|
||||||
|verification_link| messages::ChangeEmail {
|
|verification_link| messages::ChangeEmail {
|
||||||
server_name: services.config.server_name.as_str(),
|
server_name: services.config.server_name.as_str(),
|
||||||
user_id: Some(sender_user),
|
user_id: sender_user,
|
||||||
verification_link,
|
verification_link,
|
||||||
},
|
},
|
||||||
&body.client_secret,
|
&body.client_secret,
|
||||||
|
|||||||
@@ -89,14 +89,7 @@ pub(crate) async fn update_device_route(
|
|||||||
|
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.create_device(
|
.create_device(sender_user, &device_id, None, None, Some(client.to_string()))
|
||||||
sender_user,
|
|
||||||
&device_id,
|
|
||||||
&appservice.registration.as_token,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
Some(client.to_string()),
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
return Ok(update_device::v3::Response::new());
|
return Ok(update_device::v3::Response::new());
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result, debug_error, err, info,
|
Err, Result, debug_error, err, info,
|
||||||
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
|
matrix::{event::gen_event_id_canonical_json, pdu::PartialPdu},
|
||||||
warn,
|
trace, warn,
|
||||||
};
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
@@ -166,7 +166,7 @@ pub(crate) async fn invite_helper(
|
|||||||
let invite_room_state = services
|
let invite_room_state = services
|
||||||
.rooms
|
.rooms
|
||||||
.state
|
.state
|
||||||
.summary_stripped(&pdu, room_id, recipient_user)
|
.summary_stripped(&pdu, room_id, recipient_user, true)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
drop(state_lock);
|
drop(state_lock);
|
||||||
@@ -193,6 +193,7 @@ pub(crate) async fn invite_helper(
|
|||||||
.await
|
.await
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
trace!(?request, "Sending invite");
|
||||||
let response = services
|
let response = services
|
||||||
.sending
|
.sending
|
||||||
.send_federation_request(recipient_user.server_name(), request)
|
.send_federation_request(recipient_user.server_name(), request)
|
||||||
@@ -221,7 +222,7 @@ pub(crate) async fn invite_helper(
|
|||||||
let pdu_id = services
|
let pdu_id = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(recipient_user.server_name(), room_id, &event_id, value, true)
|
.handle_incoming_pdu(recipient_user.server_name(), room_id, &event_id, value, false)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
|
|||||||
@@ -24,5 +24,5 @@ pub(crate) async fn register_client_route(
|
|||||||
.await
|
.await
|
||||||
.map_err(|err| (StatusCode::BAD_REQUEST, Json(err)).into_response())?;
|
.map_err(|err| (StatusCode::BAD_REQUEST, Json(err)).into_response())?;
|
||||||
|
|
||||||
Ok(Json(RegisteredClient { client_id, metadata }).into_response())
|
Ok((StatusCode::CREATED, Json(RegisteredClient { client_id, metadata })).into_response())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,9 @@
|
|||||||
},
|
},
|
||||||
assign,
|
assign,
|
||||||
};
|
};
|
||||||
|
use service::users::DeviceToken;
|
||||||
|
|
||||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
use super::DEVICE_ID_LENGTH;
|
||||||
use crate::Ruma;
|
use crate::Ruma;
|
||||||
|
|
||||||
/// # `GET /_matrix/client/v3/login`
|
/// # `GET /_matrix/client/v3/login`
|
||||||
@@ -196,8 +197,8 @@ pub(crate) async fn login_route(
|
|||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||||
|
|
||||||
// Generate a new token for the device (ensuring no collisions)
|
// Generate a new token for the device
|
||||||
let token = services.users.generate_unique_token().await;
|
let token = DeviceToken::new_random();
|
||||||
|
|
||||||
// Determine if device_id was provided and exists in the db for this user
|
// Determine if device_id was provided and exists in the db for this user
|
||||||
let device_exists = if body.device_id.is_some() {
|
let device_exists = if body.device_id.is_some() {
|
||||||
@@ -213,7 +214,7 @@ pub(crate) async fn login_route(
|
|||||||
if device_exists {
|
if device_exists {
|
||||||
services
|
services
|
||||||
.users
|
.users
|
||||||
.set_token(&user_id, &device_id, &token, None)
|
.set_token(&user_id, &device_id, token.clone())
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
services
|
services
|
||||||
@@ -221,8 +222,7 @@ pub(crate) async fn login_route(
|
|||||||
.create_device(
|
.create_device(
|
||||||
&user_id,
|
&user_id,
|
||||||
&device_id,
|
&device_id,
|
||||||
&token,
|
Some(token.clone()),
|
||||||
None,
|
|
||||||
body.initial_device_display_name.clone(),
|
body.initial_device_display_name.clone(),
|
||||||
Some(client.to_string()),
|
Some(client.to_string()),
|
||||||
)
|
)
|
||||||
@@ -241,7 +241,7 @@ pub(crate) async fn login_route(
|
|||||||
info!("{user_id} logged in");
|
info!("{user_id} logged in");
|
||||||
|
|
||||||
#[allow(deprecated)]
|
#[allow(deprecated)]
|
||||||
Ok(assign!(login::v3::Response::new(user_id, token, device_id), {
|
Ok(assign!(login::v3::Response::new(user_id, token.into_token(), device_id), {
|
||||||
well_known: client_discovery_info,
|
well_known: client_discovery_info,
|
||||||
expires_in: None,
|
expires_in: None,
|
||||||
home_server: Some(services.config.server_name.clone()),
|
home_server: Some(services.config.server_name.clone()),
|
||||||
@@ -273,7 +273,7 @@ pub(crate) async fn login_token_route(
|
|||||||
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
|
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let login_token = utils::random_string(TOKEN_LENGTH);
|
let login_token = DeviceToken::new_random().into_token();
|
||||||
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
let expires_in = services.users.create_login_token(sender_user, &login_token);
|
||||||
|
|
||||||
Ok(get_login_token::v1::Response::new(
|
Ok(get_login_token::v1::Response::new(
|
||||||
|
|||||||
@@ -381,16 +381,21 @@ async fn fetch_shortstatehashes(
|
|||||||
| Some((_, pdu_after_last_sync_end)) => {
|
| Some((_, pdu_after_last_sync_end)) => {
|
||||||
trace!(?pdu_after_last_sync_end.event_id, "pdu at last sync end");
|
trace!(?pdu_after_last_sync_end.event_id, "pdu at last sync end");
|
||||||
|
|
||||||
Some(
|
if pdu_after_last_sync_end.kind == RoomCreate {
|
||||||
services
|
// Room create events have no previous state
|
||||||
.rooms
|
None
|
||||||
.state_accessor
|
} else {
|
||||||
.pdu_shortstatehash(&pdu_after_last_sync_end.event_id)
|
Some(
|
||||||
.await
|
services
|
||||||
.map_err(|err| {
|
.rooms
|
||||||
err!("Last sync end PDU has no shortstatehash: {err}")
|
.state_accessor
|
||||||
}),
|
.pdu_shortstatehash(&pdu_after_last_sync_end.event_id)
|
||||||
)
|
.await
|
||||||
|
.map_err(|err| {
|
||||||
|
err!("Last sync end PDU has no shortstatehash: {err}")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
| None => {
|
| None => {
|
||||||
// No events have been sent since the last sync, or we just joined this room
|
// No events have been sent since the last sync, or we just joined this room
|
||||||
|
|||||||
@@ -15,10 +15,9 @@
|
|||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub(crate) struct AuthQueryParams {
|
pub(crate) struct AuthQueryParams {
|
||||||
pub(super) user_id: Option<String>,
|
pub(super) user_id: Option<String>,
|
||||||
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
|
|
||||||
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
|
|
||||||
#[serde(alias = "org.matrix.msc3202.device_id")]
|
|
||||||
pub(super) device_id: Option<String>,
|
pub(super) device_id: Option<String>,
|
||||||
|
#[serde(rename = "org.matrix.msc3202.device_id")]
|
||||||
|
pub(super) legacy_device_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extractor for Ruma request structs
|
/// Extractor for Ruma request structs
|
||||||
|
|||||||
+22
-18
@@ -219,25 +219,29 @@ async fn verify<B: AsRef<[u8]> + Sync>(
|
|||||||
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
|
||||||
// The device_id can be provided via `device_id` or
|
// The device_id can be provided via `device_id` or
|
||||||
// `org.matrix.msc3202.device_id` query parameter.
|
// `org.matrix.msc3202.device_id` query parameter.
|
||||||
let sender_device =
|
let sender_device = if let Some(device_id) = query
|
||||||
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
.device_id
|
||||||
// Verify the device exists for this user
|
.or(query.legacy_device_id)
|
||||||
if services
|
.as_deref()
|
||||||
.users
|
.map(Into::into)
|
||||||
.get_device_metadata(&sender_user, device_id)
|
{
|
||||||
.await
|
// Verify the device exists for this user
|
||||||
.is_err()
|
if services
|
||||||
{
|
.users
|
||||||
return Err!(Request(Forbidden(
|
.get_device_metadata(&sender_user, device_id)
|
||||||
"Device does not exist for user or appservice cannot masquerade as \
|
.await
|
||||||
this device."
|
.is_err()
|
||||||
)));
|
{
|
||||||
}
|
return Err!(Request(Forbidden(
|
||||||
|
"Device does not exist for user or appservice cannot masquerade as this \
|
||||||
|
device."
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
Some(device_id.to_owned())
|
Some(device_id.to_owned())
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(ClientIdentity::Appservice {
|
Ok(ClientIdentity::Appservice {
|
||||||
sender_user,
|
sender_user,
|
||||||
|
|||||||
+294
-110
@@ -1,23 +1,29 @@
|
|||||||
|
use std::collections::{HashMap, hash_map::Entry};
|
||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum_client_ip::ClientIp;
|
use axum_client_ip::ClientIp;
|
||||||
use base64::{Engine as _, engine::general_purpose};
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Error, PduEvent, Result, err, error,
|
Err, Error, EventTypeExt, PduEvent, Result, debug, err, error,
|
||||||
matrix::{Event, event::gen_event_id},
|
matrix::{Event, StateKey},
|
||||||
utils::{self, hash::sha256},
|
result::FlatOk,
|
||||||
|
state_res, trace,
|
||||||
|
utils::hash::sha256,
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonValue, UserId,
|
CanonicalJsonObject, CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, ServerName,
|
||||||
|
UserId,
|
||||||
api::{
|
api::{
|
||||||
error::{ErrorKind, IncompatibleRoomVersionErrorData},
|
error::{ErrorKind, IncompatibleRoomVersionErrorData},
|
||||||
federation::membership::{RawStrippedState, create_invite},
|
federation::membership::{RawStrippedState, create_invite},
|
||||||
},
|
},
|
||||||
events::room::member::{MembershipState, RoomMemberEventContent},
|
events::{StateEventType, room::member::MembershipState},
|
||||||
serde::JsonObject,
|
room_version_rules::RoomVersionRules,
|
||||||
};
|
};
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::{Ruma, server::utils::validate_any_membership_event};
|
||||||
|
|
||||||
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
|
/// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}`
|
||||||
///
|
///
|
||||||
@@ -28,30 +34,16 @@ pub(crate) async fn create_invite_route(
|
|||||||
ClientIp(client): ClientIp,
|
ClientIp(client): ClientIp,
|
||||||
body: Ruma<create_invite::v2::Request>,
|
body: Ruma<create_invite::v2::Request>,
|
||||||
) -> Result<create_invite::v2::Response> {
|
) -> Result<create_invite::v2::Response> {
|
||||||
// ACL check origin
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.event_handler
|
|
||||||
.acl_check(&body.identity, &body.room_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if !services.server.supported_room_version(&body.room_version) {
|
if !services.server.supported_room_version(&body.room_version) {
|
||||||
return Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
|
ErrorKind::IncompatibleRoomVersion(IncompatibleRoomVersionErrorData::new(
|
||||||
body.room_version.clone(),
|
body.room_version.clone(),
|
||||||
)),
|
)),
|
||||||
"Server does not support this room version.",
|
"This server does not support that room version",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let room_version_rules = body.room_version.rules().unwrap();
|
let room_version_rules = body.room_version.rules().unwrap();
|
||||||
|
|
||||||
if let Some(server) = body.room_id.server_name() {
|
|
||||||
if services.moderation.is_remote_server_forbidden(server) {
|
|
||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if services
|
if services
|
||||||
.moderation
|
.moderation
|
||||||
.is_remote_server_forbidden(&body.identity)
|
.is_remote_server_forbidden(&body.identity)
|
||||||
@@ -61,90 +53,61 @@ pub(crate) async fn create_invite_route(
|
|||||||
body.identity, body.room_id
|
body.identity, body.room_id
|
||||||
);
|
);
|
||||||
|
|
||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("Federation denied with {}", body.identity)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut signed_event = utils::to_canonical_object(&body.event)
|
// First, validate the invite room state, so we can compare with the create
|
||||||
.map_err(|_| err!(Request(InvalidParam("Invite event is invalid."))))?;
|
// event.
|
||||||
|
debug!(
|
||||||
// Ensure this is a membership event
|
event_id=%body.event_id,
|
||||||
if signed_event
|
room_id=%body.room_id,
|
||||||
.get("type")
|
room_version=?body.room_version,
|
||||||
.expect("event must have a type")
|
via=?body.via,
|
||||||
.as_str()
|
"Validating invite room state for invite request"
|
||||||
.expect("type must be a string")
|
);
|
||||||
!= "m.room.member"
|
let (create_event_id, state) = validate_invite_state(
|
||||||
{
|
&services,
|
||||||
return Err!(Request(BadJson(
|
&body.invite_room_state,
|
||||||
"Not allowed to send non-membership event to invite endpoint."
|
&room_version_rules,
|
||||||
)));
|
body.room_id.clone(),
|
||||||
}
|
|
||||||
|
|
||||||
let content: RoomMemberEventContent = serde_json::from_value(
|
|
||||||
signed_event
|
|
||||||
.get("content")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
)
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
|
.await?;
|
||||||
|
let create_event_json = state
|
||||||
|
.get(&StateEventType::RoomCreate.with_state_key(""))
|
||||||
|
.expect("must have create event in invite state by this point");
|
||||||
|
|
||||||
// Ensure this is an invite membership event
|
// We can now perform the banned remote server check with the create event.
|
||||||
if content.membership != MembershipState::Invite {
|
// N.B. this checks the sender field, which is technically incorrect for rooms
|
||||||
return Err!(Request(BadJson(
|
// v10 and below. This usually isn't the case though so sue me
|
||||||
"Not allowed to send a non-invite membership event to invite endpoint."
|
let creator = create_event_json
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the sending user isn't a lying bozo
|
|
||||||
let sender_user = signed_event
|
|
||||||
.get("sender")
|
.get("sender")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(UserId::parse)
|
.map(UserId::parse)
|
||||||
.and_then(Result::ok)
|
.flat_ok()
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?;
|
.expect("must have valid sender in create event");
|
||||||
|
if services
|
||||||
if sender_user.server_name() != body.identity {
|
.moderation
|
||||||
return Err!(Request(Forbidden("Sender's server does not match the origin server.",)));
|
.is_remote_server_forbidden(creator.server_name())
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure the target user belongs to this server
|
|
||||||
let recipient_user = signed_event
|
|
||||||
.get("state_key")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.map(UserId::parse)
|
|
||||||
.and_then(Result::ok)
|
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?;
|
|
||||||
|
|
||||||
if !services
|
|
||||||
.globals
|
|
||||||
.server_is_ours(recipient_user.server_name())
|
|
||||||
{
|
{
|
||||||
return Err!(Request(InvalidParam("User does not belong to this homeserver.")));
|
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure we're not ACL'ed from their room.
|
// And then we can validate the member event itself
|
||||||
services
|
let (mut signed_event, sender_user, recipient_user) = validate_invite_membership_event(
|
||||||
.rooms
|
&services,
|
||||||
.event_handler
|
&body.event,
|
||||||
.acl_check(recipient_user.server_name(), &body.room_id)
|
&room_version_rules,
|
||||||
.await?;
|
&body.identity,
|
||||||
|
create_event_id.clone(),
|
||||||
services
|
body.room_id.clone(),
|
||||||
.server_keys
|
body.event_id.clone(),
|
||||||
.hash_and_sign_event(&mut signed_event, &room_version_rules)
|
)
|
||||||
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
|
.await?;
|
||||||
|
|
||||||
// Generate event id
|
|
||||||
let event_id = gen_event_id(&signed_event, &room_version_rules)?;
|
|
||||||
|
|
||||||
// Add event_id back
|
|
||||||
signed_event.insert("event_id".to_owned(), CanonicalJsonValue::String(event_id.to_string()));
|
|
||||||
|
|
||||||
if services.rooms.metadata.is_banned(&body.room_id).await
|
if services.rooms.metadata.is_banned(&body.room_id).await
|
||||||
&& !services.users.is_admin(&recipient_user).await
|
&& !services.users.is_admin(&recipient_user).await
|
||||||
{
|
{
|
||||||
return Err!(Request(Forbidden("This room is banned on this homeserver.")));
|
return Err!(Request(Forbidden("That room is banned on this homeserver.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
|
if services.config.block_non_admin_invites && !services.users.is_admin(&recipient_user).await
|
||||||
@@ -161,30 +124,57 @@ pub(crate) async fn create_invite_route(
|
|||||||
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
|
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If we're already in the room, ensure that neither the origin nor ourselves
|
||||||
|
// are ACL'd.
|
||||||
|
let resident = services
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||||
|
.await;
|
||||||
|
if resident {
|
||||||
|
services
|
||||||
|
.rooms
|
||||||
|
.event_handler
|
||||||
|
.acl_check(&body.identity, &body.room_id)
|
||||||
|
.await?;
|
||||||
|
services
|
||||||
|
.rooms
|
||||||
|
.event_handler
|
||||||
|
.acl_check(recipient_user.server_name(), &body.room_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| err!(Request(Forbidden("This server is ACL'd from that room"))))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.server_keys
|
||||||
|
.hash_and_sign_event(&mut signed_event, &room_version_rules)
|
||||||
|
.map_err(|e| err!(Request(InvalidParam("Failed to sign event: {e}"))))?;
|
||||||
|
|
||||||
|
// Add event_id back
|
||||||
|
signed_event
|
||||||
|
.insert("event_id".to_owned(), CanonicalJsonValue::String(body.event_id.to_string()));
|
||||||
|
|
||||||
let mut invite_state = body.invite_room_state.clone();
|
let mut invite_state = body.invite_room_state.clone();
|
||||||
|
let pdu = PduEvent::from_id_val(&body.event_id, signed_event.clone())
|
||||||
let mut event: JsonObject = serde_json::from_str(body.event.get())
|
.expect("must be able to create PDU object");
|
||||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
invite_state.push(RawStrippedState::Pdu(serde_json::value::to_raw_value(&signed_event)?));
|
||||||
|
|
||||||
event.insert("event_id".to_owned(), "$placeholder".into());
|
|
||||||
|
|
||||||
let pdu: PduEvent = serde_json::from_value(event.into())
|
|
||||||
.map_err(|e| err!(Request(BadJson("Invalid invite event PDU: {e}"))))?;
|
|
||||||
|
|
||||||
invite_state.push(RawStrippedState::Pdu(
|
|
||||||
serde_json::value::to_raw_value(&pdu).expect("PDU was just created, it must be valid"),
|
|
||||||
));
|
|
||||||
|
|
||||||
// If we are active in the room, the remote server will notify us about the
|
// If we are active in the room, the remote server will notify us about the
|
||||||
// join/invite through /send. If we are not in the room, we need to manually
|
// join/invite through /send. If we are not in the room, we need to manually
|
||||||
// record the invited state for client /sync through update_membership(), and
|
// record the invited state for client /sync through update_membership(), and
|
||||||
// send the invite PDU to the relevant appservices.
|
// send the invite PDU to the relevant appservices.
|
||||||
if !services
|
if !resident {
|
||||||
.rooms
|
// We will start by recording the room's create event as an outlier.
|
||||||
.state_cache
|
// This will allow us to recognise it later in case the sender revokes the
|
||||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
// invite over federation later. We could store more state from the invite
|
||||||
.await
|
// request, but we will get that during send_join anyway.
|
||||||
{
|
// This is safe to just add directly as an outlier as we already auth checked it
|
||||||
|
// during validation.
|
||||||
|
services
|
||||||
|
.rooms
|
||||||
|
.outlier
|
||||||
|
.add_pdu_outlier(&create_event_id, create_event_json);
|
||||||
|
|
||||||
services
|
services
|
||||||
.rooms
|
.rooms
|
||||||
.state_cache
|
.state_cache
|
||||||
@@ -240,3 +230,197 @@ pub(crate) async fn create_invite_route(
|
|||||||
.await,
|
.await,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates the *membership event* in the invite request, per the steps listed
|
||||||
|
/// under the invite endpoint's [spec].
|
||||||
|
///
|
||||||
|
/// Returns the validated JSON body, sender user ID, and recipient user ID.
|
||||||
|
///
|
||||||
|
/// Since this function performs a PDU format check, the create event must be
|
||||||
|
/// known ahead of time. This implies validating the invite state before the
|
||||||
|
/// invite event itself.
|
||||||
|
///
|
||||||
|
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid
|
||||||
|
async fn validate_invite_membership_event(
|
||||||
|
services: &crate::State,
|
||||||
|
body: &serde_json::value::RawValue,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
origin: &ServerName,
|
||||||
|
create_event_id: OwnedEventId,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
event_id: OwnedEventId,
|
||||||
|
) -> Result<(CanonicalJsonObject, OwnedUserId, OwnedUserId)> {
|
||||||
|
trace!(?body, "Invite membership event");
|
||||||
|
let (pdu, target_membership, sender_user, recipient_user) = validate_any_membership_event(
|
||||||
|
services,
|
||||||
|
body,
|
||||||
|
room_version_rules,
|
||||||
|
create_event_id,
|
||||||
|
room_id,
|
||||||
|
event_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
// Ensure the sender belongs to the remote that is sending the invite
|
||||||
|
if sender_user.server_name() != origin {
|
||||||
|
return Err!(Request(Forbidden("Sender belongs to a different server")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the target user belongs to this server
|
||||||
|
if !services
|
||||||
|
.globals
|
||||||
|
.server_is_ours(recipient_user.server_name())
|
||||||
|
{
|
||||||
|
return Err!(Request(InvalidParam("Recipient does not belong to this homeserver")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if target_membership != MembershipState::Invite {
|
||||||
|
return Err!(Request(BadJson("Invalid membership (expected `invite`)")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((pdu, sender_user, recipient_user))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates the *invite state* of an invite request, per the steps listed
|
||||||
|
/// under the endpoint's [spec].
|
||||||
|
///
|
||||||
|
/// Returns the create event's event ID, and the partial state map.
|
||||||
|
///
|
||||||
|
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#put_matrixfederationv2inviteroomideventid
|
||||||
|
async fn validate_invite_state(
|
||||||
|
services: &crate::State,
|
||||||
|
invite_state: &[RawStrippedState],
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
room_id: OwnedRoomId,
|
||||||
|
) -> Result<(OwnedEventId, HashMap<(StateEventType, StateKey), CanonicalJsonObject>)> {
|
||||||
|
trace!(?invite_state, "Raw invite state");
|
||||||
|
let mut invite_state_map: HashMap<(StateEventType, StateKey), _> =
|
||||||
|
HashMap::with_capacity(invite_state.len());
|
||||||
|
let mut create_event_id: Option<OwnedEventId> = None;
|
||||||
|
|
||||||
|
for (idx, invite_state_event) in invite_state.iter().cloned().enumerate() {
|
||||||
|
trace!(%idx, ?invite_state_event, "Invite state event");
|
||||||
|
// Stripped state hasn't been sent over federation since v1.16.
|
||||||
|
let RawStrippedState::Pdu(raw_pdu) = invite_state_event else {
|
||||||
|
debug!(%idx, "Invite state event is not a PDU");
|
||||||
|
return Err!(Request(InvalidParam(
|
||||||
|
"PDU in invite state (index {idx}) violates the room event format"
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
let (state_event_room_id, state_event_id, state_event_json) = services
|
||||||
|
.rooms
|
||||||
|
.event_handler
|
||||||
|
.parse_incoming_pdu(&raw_pdu, Some(room_version_rules))
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(Request(InvalidParam(debug_warn!("Invalid PDU in invite state: {e}"))))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if state_event_room_id != room_id {
|
||||||
|
return Err!(Request(InvalidParam(debug_warn!(
|
||||||
|
%state_event_room_id,
|
||||||
|
%room_id,
|
||||||
|
"PDU in invite state ({state_event_id}) belongs to the wrong room"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.server_keys
|
||||||
|
.verify_event(&state_event_json, room_version_rules)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(Request(InvalidParam("Signature verification failed on invite event: {e}")))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(state_key) = state_event_json.get("state_key").and_then(|k| k.as_str()) else {
|
||||||
|
return Err!(Request(InvalidParam(debug_info!(
|
||||||
|
"PDU in invite state ({state_event_id}) is not a state event"
|
||||||
|
))));
|
||||||
|
};
|
||||||
|
let Some(event_type) = state_event_json.get("type").and_then(|k| k.as_str()) else {
|
||||||
|
return Err!(Request(InvalidParam(debug_warn!(
|
||||||
|
"PDU in invite state ({state_event_id}) is not an event?"
|
||||||
|
))));
|
||||||
|
};
|
||||||
|
|
||||||
|
let key = StateEventType::from(event_type).with_state_key(state_key);
|
||||||
|
match invite_state_map.entry(key) {
|
||||||
|
| Entry::Occupied(entry) =>
|
||||||
|
return Err!(Request(InvalidParam(
|
||||||
|
"Duplicate state events in invite state for state key: {:?}",
|
||||||
|
entry.key(),
|
||||||
|
))),
|
||||||
|
| Entry::Vacant(entry) => {
|
||||||
|
if entry.key().0 == StateEventType::RoomCreate {
|
||||||
|
// Ensure this is a legal create event.
|
||||||
|
let pdu_event =
|
||||||
|
PduEvent::from_id_val(&state_event_id, state_event_json.clone())
|
||||||
|
.expect("must be able to create pdu event from event json");
|
||||||
|
debug!("Validating discovered create event in invite room state");
|
||||||
|
validate_invite_create_event(&pdu_event, room_version_rules).await?;
|
||||||
|
create_event_id = Some(state_event_id);
|
||||||
|
}
|
||||||
|
entry.insert(state_event_json);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let Some(create_event_id) = create_event_id else {
|
||||||
|
return Err!(Request(InvalidParam(debug_warn!(
|
||||||
|
parsed_state=?invite_state_map,
|
||||||
|
"Invite state does not contain the m.room.create event"
|
||||||
|
))));
|
||||||
|
};
|
||||||
|
invite_state_map.iter().try_for_each(|(key, event_json)| {
|
||||||
|
service::rooms::event_handler::Service::pdu_format_check_1(
|
||||||
|
event_json,
|
||||||
|
room_version_rules,
|
||||||
|
&create_event_id,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(Request(InvalidParam(
|
||||||
|
"PDU in invite state for {key:?} violates the room event format: {e}"
|
||||||
|
)))
|
||||||
|
})
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((create_event_id, invite_state_map))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct MFederate {
|
||||||
|
#[serde(rename = "m.federate")]
|
||||||
|
mfederate: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates that a create event is suitable for the invite, namely:
|
||||||
|
///
|
||||||
|
/// 1. It passes auth checks (aka is valid)
|
||||||
|
/// 2. The room is federated (there's no point persisting unfederated rooms)
|
||||||
|
async fn validate_invite_create_event(
|
||||||
|
pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
) -> Result {
|
||||||
|
if !state_res::auth_check(
|
||||||
|
room_version_rules,
|
||||||
|
pdu,
|
||||||
|
None,
|
||||||
|
|_, _| async {
|
||||||
|
unreachable!("No state should be fetched when processing a lone create event");
|
||||||
|
},
|
||||||
|
pdu,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
return Err!(Request(InvalidParam("m.room.create event fails auth check")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let can_federate = pdu.get_content::<MFederate>()?.mfederate;
|
||||||
|
if !can_federate.unwrap_or(true) {
|
||||||
|
return Err!(Request(InvalidParam(
|
||||||
|
"Cannot receive invites to a room with m.federate=false"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,14 +75,6 @@ pub(crate) async fn create_join_event_template_route(
|
|||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(server) = body.room_id.server_name() {
|
|
||||||
if services.moderation.is_remote_server_forbidden(server) {
|
|
||||||
return Err!(Request(Forbidden(warn!(
|
|
||||||
"Room ID server name {server} is banned on this homeserver."
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
||||||
let room_version_rules = room_version.rules().unwrap();
|
let room_version_rules = room_version.rules().unwrap();
|
||||||
if !body.ver.contains(&room_version) {
|
if !body.ver.contains(&room_version) {
|
||||||
|
|||||||
@@ -58,12 +58,6 @@ pub(crate) async fn create_knock_event_template_route(
|
|||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(server) = body.room_id.server_name() {
|
|
||||||
if services.moderation.is_remote_server_forbidden(server) {
|
|
||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
||||||
let room_version_rules = room_version.rules().unwrap();
|
let room_version_rules = room_version.rules().unwrap();
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ pub(crate) async fn create_leave_event_template_route(
|
|||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
origin = body.identity.as_str(),
|
origin = body.identity.as_str(),
|
||||||
|
room_id = %body.room_id,
|
||||||
"Refusing to serve make_leave for room we aren't participating in"
|
"Refusing to serve make_leave for room we aren't participating in"
|
||||||
);
|
);
|
||||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||||
|
|||||||
+11
-19
@@ -150,7 +150,7 @@ async fn process_inbound_transaction(
|
|||||||
.pdus
|
.pdus
|
||||||
.iter()
|
.iter()
|
||||||
.stream()
|
.stream()
|
||||||
.broad_then(|pdu| services.rooms.event_handler.parse_incoming_pdu(pdu))
|
.broad_then(|pdu| services.rooms.event_handler.parse_incoming_pdu(pdu, None))
|
||||||
.inspect_err(|e| warn!("Could not parse incoming PDU: {e}"))
|
.inspect_err(|e| warn!("Could not parse incoming PDU: {e}"))
|
||||||
.ready_filter_map(Result::ok);
|
.ready_filter_map(Result::ok);
|
||||||
|
|
||||||
@@ -241,7 +241,7 @@ async fn handle(
|
|||||||
pdus: impl Stream<Item = Pdu> + Send,
|
pdus: impl Stream<Item = Pdu> + Send,
|
||||||
edus: impl Stream<Item = Edu> + Send,
|
edus: impl Stream<Item = Edu> + Send,
|
||||||
) -> std::result::Result<ResolvedMap, TransactionError> {
|
) -> std::result::Result<ResolvedMap, TransactionError> {
|
||||||
// group pdus by room
|
// Group PDUs by room for parallel processing
|
||||||
let pdus = pdus
|
let pdus = pdus
|
||||||
.collect()
|
.collect()
|
||||||
.map(|mut pdus: Vec<_>| {
|
.map(|mut pdus: Vec<_>| {
|
||||||
@@ -252,7 +252,7 @@ async fn handle(
|
|||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
// we can evaluate rooms concurrently
|
// Evaluate rooms in parallel
|
||||||
let results: ResolvedMap = pdus
|
let results: ResolvedMap = pdus
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.try_stream()
|
.try_stream()
|
||||||
@@ -266,7 +266,7 @@ async fn handle(
|
|||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// evaluate edus after pdus, at least for now.
|
// Evaluate EDUs after PDUs in case some of the PDUs then forbid some EDUs.
|
||||||
edus.for_each_concurrent(automatic_width(), |edu| handle_edu(services, client, origin, edu))
|
edus.for_each_concurrent(automatic_width(), |edu| handle_edu(services, client, origin, edu))
|
||||||
.boxed()
|
.boxed()
|
||||||
.await;
|
.await;
|
||||||
@@ -296,20 +296,12 @@ async fn handle_room(
|
|||||||
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on
|
// Try to sort PDUs by their dependencies, but fall back to arbitrary order on
|
||||||
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
|
// failure (e.g., cycles). This is best-effort; proper ordering is the sender's
|
||||||
// responsibility.
|
// responsibility.
|
||||||
let sorted_event_ids = if pdu_map.len() >= 2 {
|
let sorted_event_ids = build_local_dag(&pdu_map, DagBuilderTree::PrevEvents)
|
||||||
let refmap = pdu_map
|
.await
|
||||||
.iter()
|
.unwrap_or_else(|e| {
|
||||||
.map(|(event_id, obj)| (event_id.clone(), obj))
|
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
|
||||||
.collect();
|
pdu_map.keys().cloned().collect()
|
||||||
build_local_dag(&refmap, DagBuilderTree::PrevEvents)
|
});
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
debug_warn!("Failed to build local DAG for room {room_id}: {e}");
|
|
||||||
pdu_map.keys().cloned().collect()
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
pdu_map.keys().cloned().collect()
|
|
||||||
};
|
|
||||||
let mut results = Vec::with_capacity(sorted_event_ids.len());
|
let mut results = Vec::with_capacity(sorted_event_ids.len());
|
||||||
for event_id in sorted_event_ids {
|
for event_id in sorted_event_ids {
|
||||||
let value = pdu_map
|
let value = pdu_map
|
||||||
@@ -322,7 +314,7 @@ async fn handle_room(
|
|||||||
let result = services
|
let result = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(origin, room_id, &event_id, value.clone(), true)
|
.handle_incoming_pdu(origin, room_id, &event_id, value, false)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await
|
.await
|
||||||
.map(|_| ());
|
.map(|_| ());
|
||||||
|
|||||||
+67
-115
@@ -2,34 +2,36 @@
|
|||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Event, Result, at, debug, err, info,
|
Err, Event, Result, at, debug, err, info, trace,
|
||||||
matrix::event::gen_event_id_canonical_json,
|
|
||||||
trace,
|
|
||||||
utils::stream::{BroadbandExt, IterStream, TryBroadbandExt},
|
utils::stream::{BroadbandExt, IterStream, TryBroadbandExt},
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonValue, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName, UserId,
|
||||||
api::federation::membership::create_join_event,
|
api::federation::membership::create_join_event,
|
||||||
assign,
|
assign,
|
||||||
events::{
|
events::{
|
||||||
StateEventType, TimelineEventType,
|
TimelineEventType,
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
room::member::{MembershipState, RoomMemberEventContent},
|
||||||
},
|
},
|
||||||
|
room_version_rules::RoomVersionRules,
|
||||||
};
|
};
|
||||||
use serde_json::value::{RawValue as RawJsonValue, to_raw_value};
|
use serde_json::value::to_raw_value;
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::{Ruma, server::utils::validate_any_membership_event};
|
||||||
|
|
||||||
/// helper method for /send_join v1 and v2
|
/// Creates a join membership event for the target, returning a computed
|
||||||
|
/// response with room state, auth chains, etc.
|
||||||
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
|
#[tracing::instrument(skip(services, pdu, omit_members), fields(room_id = room_id.as_str(), origin = origin.as_str()), level = "info")]
|
||||||
async fn create_join_event(
|
async fn create_join_event(
|
||||||
services: &Services,
|
services: &Services,
|
||||||
origin: &ServerName,
|
origin: &ServerName,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
pdu: &RawJsonValue,
|
event_id: &EventId,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
mut pdu: CanonicalJsonObject,
|
||||||
omit_members: bool,
|
omit_members: bool,
|
||||||
) -> Result<create_join_event::v2::RoomState> {
|
) -> Result<create_join_event::v2::RoomState> {
|
||||||
if !services.rooms.metadata.exists(room_id).await {
|
if !services.rooms.metadata.exists(room_id).await {
|
||||||
@@ -43,6 +45,7 @@ async fn create_join_event(
|
|||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
origin = origin.as_str(),
|
origin = origin.as_str(),
|
||||||
|
room_id = %room_id,
|
||||||
"Refusing to serve send_join for room we aren't participating in"
|
"Refusing to serve send_join for room we aren't participating in"
|
||||||
);
|
);
|
||||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||||
@@ -64,94 +67,25 @@ async fn create_join_event(
|
|||||||
.await
|
.await
|
||||||
.map_err(|e| err!(Request(NotFound(error!("Room has no state: {e}")))))?;
|
.map_err(|e| err!(Request(NotFound(error!("Room has no state: {e}")))))?;
|
||||||
|
|
||||||
// We do not add the event_id field to the pdu here because of signature and
|
let sender = pdu
|
||||||
// hashes checks
|
.get("sender")
|
||||||
trace!("Getting room version");
|
.and_then(|v| v.as_str())
|
||||||
let room_version = services.rooms.state.get_room_version(room_id).await?;
|
.map(UserId::parse)
|
||||||
let room_version_rules = room_version.rules().unwrap();
|
.and_then(Result::ok)
|
||||||
|
.expect("sender was already validated");
|
||||||
trace!("Generating event ID and converting to canonical json");
|
|
||||||
let Ok((event_id, mut value)) = gen_event_id_canonical_json(pdu, &room_version_rules) else {
|
|
||||||
// Event could not be converted to canonical json
|
|
||||||
return Err!(Request(BadJson("Could not convert event to canonical json.")));
|
|
||||||
};
|
|
||||||
|
|
||||||
let event_room_id: OwnedRoomId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("room_id")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing room_id property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("room_id field is not a valid room ID: {e}")))))?;
|
|
||||||
|
|
||||||
if event_room_id != room_id {
|
|
||||||
return Err!(Request(BadJson("Event room_id does not match request path room ID.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let event_type: StateEventType = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("type")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing type property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Event has invalid state event type: {e}")))))?;
|
|
||||||
|
|
||||||
if event_type != StateEventType::RoomMember {
|
|
||||||
return Err!(Request(BadJson(
|
|
||||||
"Not allowed to send non-membership state event to join endpoint."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let content: RoomMemberEventContent = serde_json::from_value(
|
let content: RoomMemberEventContent = serde_json::from_value(
|
||||||
value
|
pdu.get("content")
|
||||||
.get("content")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
||||||
.clone()
|
.clone()
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
|
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
|
||||||
|
|
||||||
if content.membership != MembershipState::Join {
|
|
||||||
return Err!(Request(BadJson(
|
|
||||||
"Not allowed to send a non-join membership event to join endpoint."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let sender: OwnedUserId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("sender")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing sender property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("sender property is not a valid user ID: {e}")))))?;
|
|
||||||
|
|
||||||
// check if origin server is trying to send for another server
|
|
||||||
if sender.server_name() != origin {
|
|
||||||
return Err!(Request(Forbidden("Not allowed to join on behalf of another server.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let state_key: OwnedUserId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("state_key")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?;
|
|
||||||
|
|
||||||
if state_key != sender {
|
|
||||||
return Err!(Request(BadJson("State key does not match sender user.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(authorising_user) = content.join_authorized_via_users_server {
|
if let Some(authorising_user) = content.join_authorized_via_users_server {
|
||||||
use ruma::RoomVersionId::*;
|
if !room_version_rules.authorization.restricted_join_rule {
|
||||||
|
|
||||||
if matches!(room_version, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
|
||||||
return Err!(Request(InvalidParam(
|
return Err!(Request(InvalidParam(
|
||||||
"Room version {room_version} does not support restricted rooms but \
|
"Room version does not support restricted rooms but \
|
||||||
join_authorised_via_users_server ({authorising_user}) was found in the event."
|
join_authorised_via_users_server ({authorising_user}) was found in the event."
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
@@ -174,8 +108,7 @@ async fn create_join_event(
|
|||||||
they cannot authorise your join."
|
they cannot authorise your join."
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
if !super::user_can_perform_restricted_join(services, &sender, room_id).await? {
|
||||||
if !super::user_can_perform_restricted_join(services, &state_key, room_id).await? {
|
|
||||||
return Err!(Request(UnableToAuthorizeJoin(
|
return Err!(Request(UnableToAuthorizeJoin(
|
||||||
"Joining user did not pass restricted room's rules."
|
"Joining user did not pass restricted room's rules."
|
||||||
)));
|
)));
|
||||||
@@ -183,7 +116,7 @@ async fn create_join_event(
|
|||||||
|
|
||||||
services
|
services
|
||||||
.server_keys
|
.server_keys
|
||||||
.hash_and_sign_event(&mut value, &room_version_rules)
|
.hash_and_sign_event(&mut pdu, room_version_rules)
|
||||||
.map_err(|e| {
|
.map_err(|e| {
|
||||||
err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}"))))
|
err!(Request(InvalidParam(warn!("Failed to sign send_join event: {e}"))))
|
||||||
})?;
|
})?;
|
||||||
@@ -200,7 +133,7 @@ async fn create_join_event(
|
|||||||
let pdu_id = services
|
let pdu_id = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(sender.server_name(), room_id, &event_id, value.clone(), true)
|
.handle_incoming_pdu(sender.server_name(), room_id, event_id, pdu.clone(), false)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
||||||
@@ -220,14 +153,12 @@ async fn create_join_event(
|
|||||||
.iter()
|
.iter()
|
||||||
.try_stream()
|
.try_stream()
|
||||||
.broad_filter_map(|event_id| async move {
|
.broad_filter_map(|event_id| async move {
|
||||||
if omit_members {
|
if omit_members && let Ok(e) = event_id.as_ref() {
|
||||||
if let Ok(e) = event_id.as_ref() {
|
let pdu = services.rooms.timeline.get_pdu(e).await;
|
||||||
let pdu = services.rooms.timeline.get_pdu(e).await;
|
if pdu.is_ok_and(|p| *p.kind() == TimelineEventType::RoomMember) {
|
||||||
if pdu.is_ok_and(|p| *p.kind() == TimelineEventType::RoomMember) {
|
trace!("omitting member event {e:?} from returned state");
|
||||||
trace!("omitting member event {e:?} from returned state");
|
// skip members
|
||||||
// skip members
|
return None;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(event_id)
|
Some(event_id)
|
||||||
@@ -289,7 +220,7 @@ async fn create_join_event(
|
|||||||
Ok(assign!(create_join_event::v2::RoomState::new(), {
|
Ok(assign!(create_join_event::v2::RoomState::new(), {
|
||||||
auth_chain,
|
auth_chain,
|
||||||
state,
|
state,
|
||||||
event: to_raw_value(&CanonicalJsonValue::Object(value)).ok(),
|
event: to_raw_value(&CanonicalJsonValue::Object(pdu)).ok(),
|
||||||
members_omitted: omit_members,
|
members_omitted: omit_members,
|
||||||
servers_in_room,
|
servers_in_room,
|
||||||
}))
|
}))
|
||||||
@@ -309,24 +240,45 @@ pub(crate) async fn create_join_event_v2_route(
|
|||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(server) = body.room_id.server_name() {
|
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
||||||
if services.moderation.is_remote_server_forbidden(server) {
|
let create_event = services
|
||||||
warn!(
|
.rooms
|
||||||
"Server {} tried joining room ID {} through us which has a server name that is \
|
.state_accessor
|
||||||
globally forbidden. Rejecting.",
|
.get_room_create_event(&body.room_id)
|
||||||
body.identity, &body.room_id,
|
.await;
|
||||||
);
|
let room_version_rules = room_version.rules().unwrap();
|
||||||
return Err!(Request(Forbidden(warn!(
|
|
||||||
"Room ID server name {server} is banned on this homeserver."
|
let (value, membership, sender, target) = validate_any_membership_event(
|
||||||
))));
|
&services,
|
||||||
}
|
&body.pdu,
|
||||||
|
&room_version_rules,
|
||||||
|
create_event.event_id.clone(),
|
||||||
|
body.room_id.clone(),
|
||||||
|
body.event_id.clone(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if membership != MembershipState::Join {
|
||||||
|
return Err!(Request(InvalidParam("Invalid membership (expected `join`)")));
|
||||||
|
}
|
||||||
|
if sender.server_name() != body.identity {
|
||||||
|
return Err!(Request(InvalidParam("Sender belongs to a different server")));
|
||||||
|
}
|
||||||
|
if sender != target {
|
||||||
|
return Err!(Request(InvalidParam("Sender does not match state key")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let room_state =
|
let room_state = create_join_event(
|
||||||
create_join_event(&services, &body.identity, &body.room_id, &body.pdu, body.omit_members)
|
&services,
|
||||||
.boxed()
|
&body.identity,
|
||||||
.await?;
|
&body.room_id,
|
||||||
|
&body.event_id,
|
||||||
|
&room_version_rules,
|
||||||
|
value,
|
||||||
|
body.omit_members,
|
||||||
|
)
|
||||||
|
.boxed()
|
||||||
|
.await?;
|
||||||
info!(
|
info!(
|
||||||
"Finished sending a join for {} in {} in {:?}",
|
"Finished sending a join for {} in {} in {:?}",
|
||||||
body.identity,
|
body.identity,
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{
|
use conduwuit::{Err, Event, Result, debug_info, err, matrix::pdu::PduEvent, warn};
|
||||||
Err, Result, err, info,
|
|
||||||
matrix::{event::gen_event_id_canonical_json, pdu::PduEvent},
|
|
||||||
warn,
|
|
||||||
};
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedUserId,
|
api::federation::membership::create_knock_event, events::room::member::MembershipState,
|
||||||
api::federation::membership::create_knock_event,
|
|
||||||
events::{
|
|
||||||
StateEventType,
|
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
|
||||||
},
|
|
||||||
serde::JsonObject,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::{Ruma, server::utils::validate_any_membership_event};
|
||||||
|
|
||||||
/// # `PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}`
|
/// # `PUT /_matrix/federation/v1/send_knock/{roomId}/{eventId}`
|
||||||
///
|
///
|
||||||
@@ -33,18 +23,7 @@ pub(crate) async fn create_knock_event_v1_route(
|
|||||||
forbidden. Rejecting.",
|
forbidden. Rejecting.",
|
||||||
body.identity, &body.room_id,
|
body.identity, &body.room_id,
|
||||||
);
|
);
|
||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
return Err!(Request(Forbidden("Federation denied with {}", body.identity)));
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(server) = body.room_id.server_name() {
|
|
||||||
if services.moderation.is_remote_server_forbidden(server) {
|
|
||||||
warn!(
|
|
||||||
"Server {} tried knocking room ID {} which has a server name that is globally \
|
|
||||||
forbidden. Rejecting.",
|
|
||||||
body.identity, &body.room_id,
|
|
||||||
);
|
|
||||||
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !services.rooms.metadata.exists(&body.room_id).await {
|
if !services.rooms.metadata.exists(&body.room_id).await {
|
||||||
@@ -57,8 +36,9 @@ pub(crate) async fn create_knock_event_v1_route(
|
|||||||
.server_in_room(services.globals.server_name(), &body.room_id)
|
.server_in_room(services.globals.server_name(), &body.room_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
info!(
|
debug_info!(
|
||||||
origin = body.identity.as_str(),
|
origin = body.identity.as_str(),
|
||||||
|
room_id = %body.room_id,
|
||||||
"Refusing to serve send_knock for room we aren't participating in"
|
"Refusing to serve send_knock for room we aren't participating in"
|
||||||
);
|
);
|
||||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||||
@@ -72,88 +52,40 @@ pub(crate) async fn create_knock_event_v1_route(
|
|||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
|
||||||
|
let create_event = services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.get_room_create_event(&body.room_id)
|
||||||
|
.await;
|
||||||
let room_version_rules = room_version.rules().unwrap();
|
let room_version_rules = room_version.rules().unwrap();
|
||||||
|
|
||||||
if !room_version_rules.authorization.knocking {
|
if !room_version_rules.authorization.knocking {
|
||||||
return Err!(Request(Forbidden("Room version does not support knocking.")));
|
return Err!(Request(Forbidden("Room version does not support knocking.")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let Ok((event_id, value)) = gen_event_id_canonical_json(&body.pdu, &room_version_rules)
|
let (mut event, target_membership, sender, target) = validate_any_membership_event(
|
||||||
else {
|
&services,
|
||||||
// Event could not be converted to canonical json
|
&body.pdu,
|
||||||
return Err!(Request(InvalidParam("Could not convert event to canonical json.")));
|
&room_version_rules,
|
||||||
};
|
create_event.event_id().to_owned(),
|
||||||
|
body.room_id.clone(),
|
||||||
let event_type: StateEventType = serde_json::from_value(
|
body.event_id.clone(),
|
||||||
value
|
|
||||||
.get("type")
|
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Event has no event type."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
)
|
||||||
.map_err(|e| err!(Request(InvalidParam("Event has invalid event type: {e}"))))?;
|
.await?;
|
||||||
|
|
||||||
if event_type != StateEventType::RoomMember {
|
if target_membership != MembershipState::Knock {
|
||||||
return Err!(Request(InvalidParam(
|
return Err!(Request(InvalidParam("Invalid membership (expected `knock`)")));
|
||||||
"Not allowed to send non-membership state event to knock endpoint.",
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let content: RoomMemberEventContent = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("content")
|
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Membership event has no content"))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Event has invalid membership content: {e}"))))?;
|
|
||||||
|
|
||||||
if content.membership != MembershipState::Knock {
|
|
||||||
return Err!(Request(InvalidParam(
|
|
||||||
"Not allowed to send a non-knock membership event to knock endpoint."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACL check sender server name
|
|
||||||
let sender: OwnedUserId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("sender")
|
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Event has no sender user ID."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson("Event sender is not a valid user ID: {e}"))))?;
|
|
||||||
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.event_handler
|
|
||||||
.acl_check(sender.server_name(), &body.room_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// check if origin server is trying to send for another server
|
|
||||||
if sender.server_name() != body.identity {
|
if sender.server_name() != body.identity {
|
||||||
return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user.")));
|
return Err!(Request(InvalidParam("Sender belongs to a different server")));
|
||||||
|
}
|
||||||
|
if sender != target {
|
||||||
|
return Err!(Request(InvalidParam("Sender does not match state key")));
|
||||||
}
|
}
|
||||||
|
|
||||||
let state_key: OwnedUserId = serde_json::from_value(
|
event.insert("event_id".to_owned(), body.event_id.as_str().into());
|
||||||
value
|
|
||||||
.get("state_key")
|
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Event does not have a state_key"))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson("Event does not have a valid state_key: {e}"))))?;
|
|
||||||
|
|
||||||
if state_key != sender {
|
let pdu = PduEvent::from_id_val(&body.event_id, event.clone())
|
||||||
return Err!(Request(InvalidParam("state_key does not match sender user of event.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut event: JsonObject = serde_json::from_str(body.pdu.get())
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
|
|
||||||
|
|
||||||
event.insert("event_id".to_owned(), "$placeholder".into());
|
|
||||||
|
|
||||||
let pdu: PduEvent = serde_json::from_value(event.into())
|
|
||||||
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
|
.map_err(|e| err!(Request(InvalidParam("Invalid knock event PDU: {e}"))))?;
|
||||||
|
|
||||||
let mutex_lock = services
|
let mutex_lock = services
|
||||||
@@ -166,7 +98,7 @@ pub(crate) async fn create_knock_event_v1_route(
|
|||||||
let pdu_id = services
|
let pdu_id = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(sender.server_name(), &body.room_id, &event_id, value.clone(), true)
|
.handle_incoming_pdu(sender.server_name(), &body.room_id, &body.event_id, event, false)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
||||||
@@ -181,7 +113,7 @@ pub(crate) async fn create_knock_event_v1_route(
|
|||||||
let knock_room_state = services
|
let knock_room_state = services
|
||||||
.rooms
|
.rooms
|
||||||
.state
|
.state
|
||||||
.summary_stripped(&pdu, &body.room_id, &sender)
|
.summary_stripped(&pdu, &body.room_id, &sender, true)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
Ok(create_knock_event::v1::Response::new(knock_room_state))
|
Ok(create_knock_event::v1::Response::new(knock_room_state))
|
||||||
|
|||||||
+31
-104
@@ -1,18 +1,11 @@
|
|||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use conduwuit::{Err, Result, err, info, matrix::event::gen_event_id_canonical_json};
|
use conduwuit::{Err, Result, debug_info, err};
|
||||||
use conduwuit_service::Services;
|
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedRoomId, OwnedUserId, RoomId, ServerName,
|
api::federation::membership::create_leave_event, events::room::member::MembershipState,
|
||||||
api::federation::membership::create_leave_event,
|
|
||||||
events::{
|
|
||||||
StateEventType,
|
|
||||||
room::member::{MembershipState, RoomMemberEventContent},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
use serde_json::value::RawValue as RawJsonValue;
|
|
||||||
|
|
||||||
use crate::Ruma;
|
use crate::{Ruma, server::utils::validate_any_membership_event};
|
||||||
|
|
||||||
/// # `PUT /_matrix/federation/v2/send_leave/{roomId}/{eventId}`
|
/// # `PUT /_matrix/federation/v2/send_leave/{roomId}/{eventId}`
|
||||||
///
|
///
|
||||||
@@ -21,17 +14,8 @@ pub(crate) async fn create_leave_event_v2_route(
|
|||||||
State(services): State<crate::State>,
|
State(services): State<crate::State>,
|
||||||
body: Ruma<create_leave_event::v2::Request>,
|
body: Ruma<create_leave_event::v2::Request>,
|
||||||
) -> Result<create_leave_event::v2::Response> {
|
) -> Result<create_leave_event::v2::Response> {
|
||||||
create_leave_event(&services, &body.identity, &body.room_id, &body.pdu).await?;
|
let room_id = body.room_id.as_ref();
|
||||||
|
let origin = &body.identity;
|
||||||
Ok(create_leave_event::v2::Response::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_leave_event(
|
|
||||||
services: &Services,
|
|
||||||
origin: &ServerName,
|
|
||||||
room_id: &RoomId,
|
|
||||||
pdu: &RawJsonValue,
|
|
||||||
) -> Result {
|
|
||||||
if !services.rooms.metadata.exists(room_id).await {
|
if !services.rooms.metadata.exists(room_id).await {
|
||||||
return Err!(Request(NotFound("Room is unknown to this server.")));
|
return Err!(Request(NotFound("Room is unknown to this server.")));
|
||||||
}
|
}
|
||||||
@@ -42,9 +26,10 @@ async fn create_leave_event(
|
|||||||
.server_in_room(services.globals.server_name(), room_id)
|
.server_in_room(services.globals.server_name(), room_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
info!(
|
debug_info!(
|
||||||
origin = origin.as_str(),
|
origin = origin.as_str(),
|
||||||
"Refusing to serve backfill for room we aren't participating in"
|
room_id = %room_id,
|
||||||
|
"Refusing to send_leave for room we aren't participating in"
|
||||||
);
|
);
|
||||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
return Err!(Request(NotFound("This server is not participating in that room.")));
|
||||||
}
|
}
|
||||||
@@ -56,91 +41,31 @@ async fn create_leave_event(
|
|||||||
.acl_check(origin, room_id)
|
.acl_check(origin, room_id)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// We do not add the event_id field to the pdu here because of signature and
|
|
||||||
// hashes checks
|
|
||||||
let room_version = services.rooms.state.get_room_version(room_id).await?;
|
let room_version = services.rooms.state.get_room_version(room_id).await?;
|
||||||
|
let create_event = services
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.get_room_create_event(room_id)
|
||||||
|
.await;
|
||||||
let room_version_rules = room_version.rules().unwrap();
|
let room_version_rules = room_version.rules().unwrap();
|
||||||
|
|
||||||
let Ok((event_id, value)) = gen_event_id_canonical_json(pdu, &room_version_rules) else {
|
let (value, membership, sender, target) = validate_any_membership_event(
|
||||||
// Event could not be converted to canonical json
|
&services,
|
||||||
return Err!(Request(BadJson("Could not convert event to canonical json.")));
|
&body.pdu,
|
||||||
};
|
&room_version_rules,
|
||||||
|
create_event.event_id.clone(),
|
||||||
let event_room_id: OwnedRoomId = serde_json::from_value(
|
body.room_id.clone(),
|
||||||
serde_json::to_value(
|
body.event_id.clone(),
|
||||||
value
|
|
||||||
.get("room_id")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing room_id property."))))?,
|
|
||||||
)
|
|
||||||
.expect("CanonicalJson is valid json value"),
|
|
||||||
)
|
)
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("room_id field is not a valid room ID: {e}")))))?;
|
.await?;
|
||||||
|
if membership != MembershipState::Leave {
|
||||||
if event_room_id != room_id {
|
return Err!(Request(InvalidParam("Invalid membership (expected `leave`)")));
|
||||||
return Err!(Request(BadJson("Event room_id does not match request path room ID.")));
|
|
||||||
}
|
}
|
||||||
|
if sender.server_name() != body.identity {
|
||||||
let content: RoomMemberEventContent = serde_json::from_value(
|
return Err!(Request(InvalidParam("Sender belongs to a different server")));
|
||||||
value
|
|
||||||
.get("content")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing content property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Event content is empty or invalid: {e}")))))?;
|
|
||||||
|
|
||||||
if content.membership != MembershipState::Leave {
|
|
||||||
return Err!(Request(BadJson(
|
|
||||||
"Not allowed to send a non-leave membership event to leave endpoint."
|
|
||||||
)));
|
|
||||||
}
|
}
|
||||||
|
if sender != target {
|
||||||
let event_type: StateEventType = serde_json::from_value(
|
return Err!(Request(InvalidParam("Sender does not match state key")));
|
||||||
value
|
|
||||||
.get("type")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing type property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("Event has invalid state event type: {e}")))))?;
|
|
||||||
|
|
||||||
if event_type != StateEventType::RoomMember {
|
|
||||||
return Err!(Request(BadJson(
|
|
||||||
"Not allowed to send non-membership state event to leave endpoint."
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ACL check sender server name
|
|
||||||
let sender: OwnedUserId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("sender")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing sender property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("sender property is not a valid user ID: {e}")))))?;
|
|
||||||
|
|
||||||
services
|
|
||||||
.rooms
|
|
||||||
.event_handler
|
|
||||||
.acl_check(sender.server_name(), room_id)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
if sender.server_name() != origin {
|
|
||||||
return Err!(Request(BadJson("Not allowed to leave on behalf of another server/user.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let state_key: OwnedUserId = serde_json::from_value(
|
|
||||||
value
|
|
||||||
.get("state_key")
|
|
||||||
.ok_or_else(|| err!(Request(BadJson("Event missing state_key property."))))?
|
|
||||||
.clone()
|
|
||||||
.into(),
|
|
||||||
)
|
|
||||||
.map_err(|e| err!(Request(BadJson(warn!("State key is not a valid user ID: {e}")))))?;
|
|
||||||
|
|
||||||
if state_key != sender {
|
|
||||||
return Err!(Request(BadJson("State key does not match sender user.")));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let mutex_lock = services
|
let mutex_lock = services
|
||||||
@@ -153,7 +78,7 @@ async fn create_leave_event(
|
|||||||
let pdu_id = services
|
let pdu_id = services
|
||||||
.rooms
|
.rooms
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(origin, room_id, &event_id, value, true)
|
.handle_incoming_pdu(origin, room_id, &body.event_id, value, false)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
.ok_or_else(|| err!(Request(InvalidParam("Could not accept as timeline event."))))?;
|
||||||
@@ -164,5 +89,7 @@ async fn create_leave_event(
|
|||||||
.sending
|
.sending
|
||||||
.send_pdu_room(room_id, &pdu_id)
|
.send_pdu_room(room_id, &pdu_id)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await
|
.await?;
|
||||||
|
|
||||||
|
Ok(create_leave_event::v2::Response::new())
|
||||||
}
|
}
|
||||||
|
|||||||
+105
-2
@@ -1,7 +1,10 @@
|
|||||||
use conduwuit::{Err, Result, is_false};
|
use conduwuit::{Err, Result, err, is_false};
|
||||||
use conduwuit_service::Services;
|
use conduwuit_service::Services;
|
||||||
use futures::{FutureExt, future::OptionFuture, join};
|
use futures::{FutureExt, future::OptionFuture, join};
|
||||||
use ruma::{EventId, RoomId, ServerName};
|
use ruma::{
|
||||||
|
CanonicalJsonObject, EventId, OwnedEventId, OwnedRoomId, OwnedUserId, RoomId, ServerName,
|
||||||
|
UserId, events::room::member::MembershipState, room_version_rules::RoomVersionRules,
|
||||||
|
};
|
||||||
|
|
||||||
pub(super) struct AccessCheck<'a> {
|
pub(super) struct AccessCheck<'a> {
|
||||||
pub(super) services: &'a Services,
|
pub(super) services: &'a Services,
|
||||||
@@ -63,3 +66,103 @@ pub(super) async fn assert(&self) -> Result {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Performs validation on a membership event that should be run on any event a
|
||||||
|
/// remote is trying to send via us.
|
||||||
|
///
|
||||||
|
/// ## Checks performed
|
||||||
|
///
|
||||||
|
/// 1. PDU room ID matches request path room ID
|
||||||
|
/// 2. PDU event ID matches request path event ID
|
||||||
|
/// 3. Signature check
|
||||||
|
/// 4. Event type check
|
||||||
|
/// 5. `sender` field presence (and parsing)
|
||||||
|
/// 6. `state_key` field presence (and parsing)
|
||||||
|
/// 7. PDU room format check (PDU check 1)
|
||||||
|
///
|
||||||
|
/// ## Returns
|
||||||
|
///
|
||||||
|
/// A resulting tuple of (PDU JSON, target membership state, sender, recipient).
|
||||||
|
pub(crate) async fn validate_any_membership_event(
|
||||||
|
services: &crate::State,
|
||||||
|
body: &serde_json::value::RawValue,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
create_event_id: OwnedEventId,
|
||||||
|
expected_room_id: OwnedRoomId,
|
||||||
|
expected_event_id: OwnedEventId,
|
||||||
|
) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> {
|
||||||
|
let (template_room_id, template_event_id, pdu) = services
|
||||||
|
.rooms
|
||||||
|
.event_handler
|
||||||
|
.parse_incoming_pdu(body, Some(room_version_rules))
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!(Request(BadJson("Invalid membership PDU: {e}"))))?;
|
||||||
|
|
||||||
|
if template_room_id != expected_room_id {
|
||||||
|
return Err!(Request(InvalidParam("Membership event does not belong to requested room")));
|
||||||
|
}
|
||||||
|
if template_event_id != expected_event_id {
|
||||||
|
return Err!(Request(InvalidParam(debug_warn!(
|
||||||
|
%template_event_id,
|
||||||
|
%expected_event_id,
|
||||||
|
"Membership event ID does not match provided event ID"
|
||||||
|
))));
|
||||||
|
}
|
||||||
|
|
||||||
|
services
|
||||||
|
.server_keys
|
||||||
|
.verify_event(&pdu, room_version_rules)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(Request(InvalidParam("Signature verification failed on membership event: {e}")))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Ensure this is a membership event
|
||||||
|
if pdu
|
||||||
|
.get("type")
|
||||||
|
.expect("event must have a type")
|
||||||
|
.as_str()
|
||||||
|
.expect("type must be a string")
|
||||||
|
!= "m.room.member"
|
||||||
|
{
|
||||||
|
return Err!(Request(BadJson(
|
||||||
|
"Not allowed to send non-membership event to this endpoint"
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
let membership = pdu
|
||||||
|
.get("content")
|
||||||
|
.ok_or_else(|| err!(Request(BadJson("Event missing content property"))))?
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| err!(Request(BadJson("Event content is not an object"))))?
|
||||||
|
.get("membership")
|
||||||
|
.ok_or_else(|| err!(Request(BadJson("Event missing membership property"))))?
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| err!(Request(BadJson("Event is not a string"))))?
|
||||||
|
.to_owned();
|
||||||
|
|
||||||
|
let sender_user = pdu
|
||||||
|
.get("sender")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(UserId::parse)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?;
|
||||||
|
let recipient_user = pdu
|
||||||
|
.get("state_key")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.map(UserId::parse)
|
||||||
|
.and_then(Result::ok)
|
||||||
|
.ok_or_else(|| err!(Request(InvalidParam("Invalid state_key property"))))?;
|
||||||
|
|
||||||
|
// Do a quick format check. The spec doesn't suggest this, but it's probably
|
||||||
|
// a good idea nonetheless.
|
||||||
|
service::rooms::event_handler::Service::pdu_format_check_1(
|
||||||
|
&pdu,
|
||||||
|
room_version_rules,
|
||||||
|
&create_event_id,
|
||||||
|
)
|
||||||
|
.map_err(|e| {
|
||||||
|
err!(Request(InvalidParam("Membership event violates the room event format: {e}")))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok((pdu, membership.into(), sender_user, recipient_user))
|
||||||
|
}
|
||||||
|
|||||||
@@ -2442,6 +2442,7 @@ pub struct OauthConfig {
|
|||||||
/// legacy authentication will be unable to log in.
|
/// legacy authentication will be unable to log in.
|
||||||
///
|
///
|
||||||
/// default: "hybrid"
|
/// default: "hybrid"
|
||||||
|
#[serde(default)]
|
||||||
compatibility_mode: OAuthMode,
|
compatibility_mode: OAuthMode,
|
||||||
|
|
||||||
/// display: hidden
|
/// display: hidden
|
||||||
|
|||||||
@@ -149,6 +149,14 @@ macro_rules! err_log {
|
|||||||
#[macro_export]
|
#[macro_export]
|
||||||
#[collapse_debuginfo(yes)]
|
#[collapse_debuginfo(yes)]
|
||||||
macro_rules! err_lev {
|
macro_rules! err_lev {
|
||||||
|
(debug_info) => {
|
||||||
|
if $crate::debug::logging() {
|
||||||
|
$crate::tracing::Level::INFO
|
||||||
|
} else {
|
||||||
|
$crate::tracing::Level::DEBUG
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
(debug_warn) => {
|
(debug_warn) => {
|
||||||
if $crate::debug::logging() {
|
if $crate::debug::logging() {
|
||||||
$crate::tracing::Level::WARN
|
$crate::tracing::Level::WARN
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ fn configure_output_err(mut output: MadSkin) -> MadSkin {
|
|||||||
use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
|
use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
|
||||||
|
|
||||||
let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(196), Color::AnsiValue(234));
|
let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(196), Color::AnsiValue(234));
|
||||||
output.inline_code = code_style.clone();
|
output.inline_code = code_style;
|
||||||
output.code_block = LineStyle {
|
output.code_block = LineStyle {
|
||||||
left_margin: 0,
|
left_margin: 0,
|
||||||
right_margin: 0,
|
right_margin: 0,
|
||||||
@@ -234,7 +234,7 @@ fn configure_output(mut output: MadSkin) -> MadSkin {
|
|||||||
use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
|
use termimad::{Alignment, CompoundStyle, LineStyle, crossterm::style::Color};
|
||||||
|
|
||||||
let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(40), Color::AnsiValue(234));
|
let code_style = CompoundStyle::with_fgbg(Color::AnsiValue(40), Color::AnsiValue(234));
|
||||||
output.inline_code = code_style.clone();
|
output.inline_code = code_style;
|
||||||
output.code_block = LineStyle {
|
output.code_block = LineStyle {
|
||||||
left_margin: 0,
|
left_margin: 0,
|
||||||
right_margin: 0,
|
right_margin: 0,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use conduwuit::{Err, Result, debug, debug_info, error, info};
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
|
use conduwuit::{Err, Result, debug, debug_info, error, info, warn};
|
||||||
use ruma::events::room::message::RoomMessageEventContent;
|
use ruma::events::room::message::RoomMessageEventContent;
|
||||||
use tokio::time::{Duration, sleep};
|
use tokio::time::{Duration, sleep};
|
||||||
|
|
||||||
@@ -7,10 +9,16 @@
|
|||||||
pub(super) const SIGNAL: &str = "SIGUSR2";
|
pub(super) const SIGNAL: &str = "SIGUSR2";
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Possibly spawn the terminal console at startup if configured.
|
/// Possibly spawn the terminal console at startup if configured and a TTY
|
||||||
|
/// is available.
|
||||||
pub(super) async fn console_auto_start(&self) {
|
pub(super) async fn console_auto_start(&self) {
|
||||||
#[cfg(feature = "console")]
|
#[cfg(feature = "console")]
|
||||||
if self.services.server.config.admin_console_automatic {
|
if self.services.server.config.admin_console_automatic {
|
||||||
|
if !std::io::stdin().is_terminal() {
|
||||||
|
warn!("Console enabled without a tty available; Not enabling console");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow more of the startup sequence to execute before spawning
|
// Allow more of the startup sequence to execute before spawning
|
||||||
tokio::task::yield_now().await;
|
tokio::task::yield_now().await;
|
||||||
self.console.start().await;
|
self.console.start().await;
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ async fn check(&self) -> Result<()> {
|
|||||||
let response = self
|
let response = self
|
||||||
.services
|
.services
|
||||||
.client
|
.client
|
||||||
.default
|
.external_resource
|
||||||
.get(CHECK_FOR_ANNOUNCEMENTS_URL)
|
.get(CHECK_FOR_ANNOUNCEMENTS_URL)
|
||||||
.send()
|
.send()
|
||||||
.await?
|
.await?
|
||||||
|
|||||||
+14
-19
@@ -16,9 +16,9 @@ pub struct Service {
|
|||||||
pub matrix_resolver: Arc<MatrixResolver>,
|
pub matrix_resolver: Arc<MatrixResolver>,
|
||||||
pub dns_resolver: Arc<TokioResolver>,
|
pub dns_resolver: Arc<TokioResolver>,
|
||||||
|
|
||||||
pub default: reqwest::Client,
|
pub dns: reqwest::Client,
|
||||||
pub url_preview: reqwest::Client,
|
pub url_preview: reqwest::Client,
|
||||||
pub extern_media: reqwest::Client,
|
pub external_resource: reqwest::Client,
|
||||||
pub federation: reqwest::Client,
|
pub federation: reqwest::Client,
|
||||||
pub federation_slow: reqwest::Client,
|
pub federation_slow: reqwest::Client,
|
||||||
pub sender: reqwest::Client,
|
pub sender: reqwest::Client,
|
||||||
@@ -35,17 +35,17 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
|
|
||||||
let dns_resolver = get_dns_resolver(args.server)?;
|
let dns_resolver = get_dns_resolver(args.server)?;
|
||||||
|
|
||||||
|
let dns_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()?;
|
||||||
|
|
||||||
let matrix_resolver = Arc::new(MatrixResolverBuilder::new()
|
let matrix_resolver = Arc::new(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)
|
.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(
|
.http_client(dns_client.clone())
|
||||||
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(dns_resolver.clone())
|
.dns_resolver(dns_resolver.clone())
|
||||||
.build()?);
|
.build()?);
|
||||||
let matrix_dns_resolver = matrix_resolver.create_dns_resolver();
|
let matrix_dns_resolver = matrix_resolver.create_dns_resolver();
|
||||||
@@ -69,23 +69,19 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
matrix_resolver,
|
matrix_resolver,
|
||||||
dns_resolver,
|
dns_resolver,
|
||||||
|
|
||||||
default: base(config)?
|
dns: dns_client,
|
||||||
.dns_resolver(matrix_dns_resolver.clone())
|
|
||||||
.build()?,
|
|
||||||
|
|
||||||
url_preview: base(config)
|
url_preview: base(config)
|
||||||
.and_then(|builder| {
|
.and_then(|builder| {
|
||||||
builder_interface(builder, url_preview_bind_iface.as_deref())
|
builder_interface(builder, url_preview_bind_iface.as_deref())
|
||||||
})?
|
})?
|
||||||
.local_address(url_preview_bind_addr)
|
.local_address(url_preview_bind_addr)
|
||||||
.dns_resolver(matrix_dns_resolver.clone())
|
|
||||||
.timeout(Duration::from_secs(config.url_preview_timeout))
|
.timeout(Duration::from_secs(config.url_preview_timeout))
|
||||||
.redirect(redirect::Policy::limited(3))
|
.redirect(redirect::Policy::limited(3))
|
||||||
.user_agent(url_preview_user_agent)
|
.user_agent(url_preview_user_agent)
|
||||||
.build()?,
|
.build()?,
|
||||||
|
|
||||||
extern_media: base(config)?
|
external_resource: base(config)?
|
||||||
.dns_resolver(matrix_dns_resolver.clone())
|
|
||||||
.redirect(redirect::Policy::limited(3))
|
.redirect(redirect::Policy::limited(3))
|
||||||
.build()?,
|
.build()?,
|
||||||
|
|
||||||
@@ -128,7 +124,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
.build()?,
|
.build()?,
|
||||||
|
|
||||||
appservice: base(config)?
|
appservice: base(config)?
|
||||||
.dns_resolver(matrix_dns_resolver.clone())
|
.dns_resolver(matrix_dns_resolver)
|
||||||
.connect_timeout(Duration::from_secs(5))
|
.connect_timeout(Duration::from_secs(5))
|
||||||
.read_timeout(Duration::from_secs(config.appservice_timeout))
|
.read_timeout(Duration::from_secs(config.appservice_timeout))
|
||||||
.timeout(Duration::from_secs(config.appservice_timeout))
|
.timeout(Duration::from_secs(config.appservice_timeout))
|
||||||
@@ -138,7 +134,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
.build()?,
|
.build()?,
|
||||||
|
|
||||||
pusher: base(config)?
|
pusher: base(config)?
|
||||||
.dns_resolver(matrix_dns_resolver)
|
|
||||||
.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
|
.connect_timeout(Duration::from_secs(config.pusher_conn_timeout))
|
||||||
.timeout(Duration::from_secs(config.pusher_timeout))
|
.timeout(Duration::from_secs(config.pusher_timeout))
|
||||||
.pool_max_idle_per_host(1)
|
.pool_max_idle_per_host(1)
|
||||||
|
|||||||
@@ -260,7 +260,7 @@ async fn location_request(&self, location: &str) -> Result<FileMeta> {
|
|||||||
let response = self
|
let response = self
|
||||||
.services
|
.services
|
||||||
.client
|
.client
|
||||||
.extern_media
|
.external_resource
|
||||||
.get(location)
|
.get(location)
|
||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
|
|||||||
@@ -829,7 +829,7 @@ async fn fix_local_invite_state(services: &Services) -> Result {
|
|||||||
&& services.globals.user_is_local(&membership_event.sender) {
|
&& services.globals.user_is_local(&membership_event.sender) {
|
||||||
|
|
||||||
// build and save stripped state for their invite in the database
|
// build and save stripped state for their invite in the database
|
||||||
let stripped_state = services.rooms.state.summary_stripped(&membership_event, &room_id, &user_id).await;
|
let stripped_state = services.rooms.state.summary_stripped(&membership_event, &room_id, &user_id, false).await;
|
||||||
userroomid_invitestate.put((&user_id, &room_id), Json(stripped_state));
|
userroomid_invitestate.put((&user_id, &room_id), Json(stripped_state));
|
||||||
fixed = fixed.saturating_add(1);
|
fixed = fixed.saturating_add(1);
|
||||||
}
|
}
|
||||||
@@ -882,7 +882,7 @@ async fn split_userid_password(services: &Services) -> Result {
|
|||||||
drop(cork);
|
drop(cork);
|
||||||
info!(?remote_users, "Split userid_password.");
|
info!(?remote_users, "Split userid_password.");
|
||||||
|
|
||||||
db["global"].insert(FIXED_LOCAL_INVITE_STATE_MARKER, []);
|
db["global"].insert(SPLIT_USERID_PASSWORD, []);
|
||||||
db.db.sort()?;
|
db.db.sort()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -895,5 +895,7 @@ async fn obliterate_roomsynctoken_shortstatehash_with_extreme_prejudice(
|
|||||||
|
|
||||||
info!("Cleared roomsynctoken_shortstatehash.");
|
info!("Cleared roomsynctoken_shortstatehash.");
|
||||||
|
|
||||||
|
services.db["global"].insert(DROP_ROOMSYNCTOKEN_SHORTSTATEHASH, []);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
|
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
users,
|
users::{self, DeviceToken},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod client_metadata;
|
pub mod client_metadata;
|
||||||
@@ -343,7 +343,7 @@ async fn create_session(
|
|||||||
client_name: Option<String>,
|
client_name: Option<String>,
|
||||||
client_id: String,
|
client_id: String,
|
||||||
) -> Result<TokenResponse, OAuthError> {
|
) -> Result<TokenResponse, OAuthError> {
|
||||||
let access_token = Self::generate_token();
|
let access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
|
||||||
let refresh_token = Self::generate_token();
|
let refresh_token = Self::generate_token();
|
||||||
|
|
||||||
let device_id = requested_scopes
|
let device_id = requested_scopes
|
||||||
@@ -376,8 +376,7 @@ async fn create_session(
|
|||||||
.create_device(
|
.create_device(
|
||||||
&authorizing_user,
|
&authorizing_user,
|
||||||
device_id,
|
device_id,
|
||||||
&access_token,
|
Some(access_token.clone()),
|
||||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
|
||||||
client_name,
|
client_name,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
@@ -413,7 +412,7 @@ async fn create_session(
|
|||||||
);
|
);
|
||||||
|
|
||||||
Ok(TokenResponse {
|
Ok(TokenResponse {
|
||||||
access_token,
|
access_token: access_token.into_token(),
|
||||||
token_type: TokenType::Bearer,
|
token_type: TokenType::Bearer,
|
||||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
||||||
scope: requested_scopes.iter().join(" "),
|
scope: requested_scopes.iter().join(" "),
|
||||||
@@ -449,7 +448,7 @@ async fn refresh_session(
|
|||||||
|
|
||||||
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
|
assert_eq!(&client_id, &session_info.client_id, "session info client id mismatch");
|
||||||
|
|
||||||
let new_access_token = Self::generate_token();
|
let new_access_token = DeviceToken::new_random().with_max_age(Self::ACCESS_TOKEN_MAX_AGE);
|
||||||
let new_refresh_token = Self::generate_token();
|
let new_refresh_token = Self::generate_token();
|
||||||
let scope = session_info.scopes.iter().join(" ");
|
let scope = session_info.scopes.iter().join(" ");
|
||||||
session_info
|
session_info
|
||||||
@@ -461,8 +460,7 @@ async fn refresh_session(
|
|||||||
.set_token(
|
.set_token(
|
||||||
&refresh_token_info.user_id,
|
&refresh_token_info.user_id,
|
||||||
&refresh_token_info.device_id,
|
&refresh_token_info.device_id,
|
||||||
&new_access_token,
|
new_access_token.clone(),
|
||||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.expect("should be able to set token");
|
.expect("should be able to set token");
|
||||||
@@ -479,7 +477,7 @@ async fn refresh_session(
|
|||||||
.raw_put(&new_refresh_token, Json(refresh_token_info));
|
.raw_put(&new_refresh_token, Json(refresh_token_info));
|
||||||
|
|
||||||
Ok(TokenResponse {
|
Ok(TokenResponse {
|
||||||
access_token: new_access_token,
|
access_token: new_access_token.into_token(),
|
||||||
token_type: TokenType::Bearer,
|
token_type: TokenType::Bearer,
|
||||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
||||||
scope,
|
scope,
|
||||||
|
|||||||
+39
-6
@@ -115,6 +115,21 @@ pub enum SessionCompletionStatus {
|
|||||||
Complete(OwnedUserId),
|
Complete(OwnedUserId),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum ClaimedLocalUser {
|
||||||
|
/// The claim refers to an existing user.
|
||||||
|
Existing(OwnedUserId),
|
||||||
|
/// The claim refers to a new user ID which should be registered.
|
||||||
|
New(OwnedUserId),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClaimedLocalUser {
|
||||||
|
fn into_user_id(self) -> OwnedUserId {
|
||||||
|
match self {
|
||||||
|
| Self::Existing(user_id) | Self::New(user_id) => user_id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl crate::Service for Service {
|
impl crate::Service for Service {
|
||||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
@@ -136,6 +151,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
client_secret: if let Some(client_secret_file) = &config.client_secret_file {
|
client_secret: if let Some(client_secret_file) = &config.client_secret_file {
|
||||||
std::fs::read_to_string(client_secret_file)
|
std::fs::read_to_string(client_secret_file)
|
||||||
|
.map(|client_secret| client_secret.trim().to_owned())
|
||||||
.map(ClientSecret::new)
|
.map(ClientSecret::new)
|
||||||
.map_err(|err| err!("Failed to read OIDC client secret file: {err}"))?
|
.map_err(|err| err!("Failed to read OIDC client secret file: {err}"))?
|
||||||
} else if let Some(client_secret) = &config.client_secret {
|
} else if let Some(client_secret) = &config.client_secret {
|
||||||
@@ -303,7 +319,7 @@ pub async fn complete_session(
|
|||||||
.expect("claims should be an object")
|
.expect("claims should be an object")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
debug!(?all_claims);
|
debug!(?all_claims, "Got claims from the identity provider");
|
||||||
|
|
||||||
let subject = claims.subject().as_str();
|
let subject = claims.subject().as_str();
|
||||||
|
|
||||||
@@ -326,14 +342,13 @@ pub async fn complete_session(
|
|||||||
.get(&config.preferred_username_claim)
|
.get(&config.preferred_username_claim)
|
||||||
.and_then(|claim| claim.as_str())
|
.and_then(|claim| claim.as_str())
|
||||||
{
|
{
|
||||||
self.services
|
self.identify_claimed_local_user(preferred_username)
|
||||||
.users
|
|
||||||
.determine_registration_user_id(Some(preferred_username.to_owned()), None, None)
|
|
||||||
.await
|
.await
|
||||||
|
.map(ClaimedLocalUser::into_user_id)
|
||||||
.map_err(|err| {
|
.map_err(|err| {
|
||||||
error!("Preferred username claim is not a valid localpart: {err}");
|
error!("Preferred username claim is not a valid localpart: {err}");
|
||||||
"Your preferred username is not a valid Matrix user ID localpart. Contact \
|
"Your preferred username could not be converted to a valid Matrix user ID. \
|
||||||
your homeserver's administrator."
|
Contact your homeserver's administrator."
|
||||||
})?
|
})?
|
||||||
} else {
|
} else {
|
||||||
error!("Preferred username claim was not present or was not a string");
|
error!("Preferred username claim was not present or was not a string");
|
||||||
@@ -482,4 +497,22 @@ pub fn link_user(&self, user_id: &UserId, subject: &str) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn unlink_user(&self, subject: &str) { self.db.openidsubject_localpart.remove(subject); }
|
pub fn unlink_user(&self, subject: &str) { self.db.openidsubject_localpart.remove(subject); }
|
||||||
|
|
||||||
|
/// Determine what user ID a localpart claim refers to.
|
||||||
|
pub async fn identify_claimed_local_user(&self, claim: &str) -> Result<ClaimedLocalUser> {
|
||||||
|
if let Ok(user_id) =
|
||||||
|
UserId::parse(format!("@{}:{}", claim, self.services.globals.server_name()))
|
||||||
|
&& self.services.users.status(&user_id).await.is_active()
|
||||||
|
{
|
||||||
|
Ok(ClaimedLocalUser::Existing(user_id))
|
||||||
|
} else {
|
||||||
|
let user_id = self
|
||||||
|
.services
|
||||||
|
.users
|
||||||
|
.determine_registration_user_id(Some(claim.to_owned()), None, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(ClaimedLocalUser::New(user_id))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ pub async fn acl_check(&self, server_name: &ServerName, room_id: &RoomId) -> Res
|
|||||||
.await
|
.await
|
||||||
.map(|c: RoomServerAclEventContent| c)
|
.map(|c: RoomServerAclEventContent| c)
|
||||||
else {
|
else {
|
||||||
|
trace!("Room has no ACL, allowing");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
|
fmt::{Display, Formatter},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -33,6 +34,20 @@ pub enum DagBuilderTree {
|
|||||||
AuthEvents,
|
AuthEvents,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DagBuilderTree {
|
||||||
|
#[must_use]
|
||||||
|
pub fn as_str(&self) -> &str {
|
||||||
|
match self {
|
||||||
|
| Self::AuthEvents => "auth_events",
|
||||||
|
| Self::PrevEvents => "prev_events",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DagBuilderTree {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.write_str(self.as_str()) }
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
|
/// Attempts to build a localised directed acyclic graph out of the given PDUs,
|
||||||
/// returning them in a topologically sorted order.
|
/// returning them in a topologically sorted order.
|
||||||
///
|
///
|
||||||
@@ -42,23 +57,22 @@ pub enum DagBuilderTree {
|
|||||||
/// not account for power levels or other tie breaks.
|
/// not account for power levels or other tie breaks.
|
||||||
#[allow(clippy::implicit_hasher)]
|
#[allow(clippy::implicit_hasher)]
|
||||||
pub async fn build_local_dag(
|
pub async fn build_local_dag(
|
||||||
pdu_map: &HashMap<OwnedEventId, &CanonicalJsonObject>,
|
pdu_map: &HashMap<OwnedEventId, CanonicalJsonObject>,
|
||||||
tree: DagBuilderTree,
|
tree: DagBuilderTree,
|
||||||
) -> Result<Vec<OwnedEventId>> {
|
) -> Result<Vec<OwnedEventId>> {
|
||||||
debug_assert!(pdu_map.len() >= 2, "needless call to build_local_dag with less than 2 PDUs");
|
if pdu_map.len() <= 1 {
|
||||||
|
return Ok(pdu_map.keys().cloned().collect());
|
||||||
|
}
|
||||||
|
|
||||||
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> =
|
let mut dag: HashMap<OwnedEventId, HashSet<OwnedEventId>> =
|
||||||
HashMap::with_capacity(pdu_map.len());
|
HashMap::with_capacity(pdu_map.len());
|
||||||
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
|
let mut id_origin_ts: HashMap<OwnedEventId, _> = HashMap::with_capacity(pdu_map.len());
|
||||||
let tree = match tree {
|
|
||||||
| DagBuilderTree::AuthEvents => "auth_events",
|
|
||||||
| DagBuilderTree::PrevEvents => "prev_events",
|
|
||||||
};
|
|
||||||
|
|
||||||
for (event_id, value) in pdu_map {
|
for (event_id, value) in pdu_map {
|
||||||
// Parse all prev events as event IDs - if they are missing, return an error (we
|
// Parse all prev events as event IDs - if they are missing, return an error (we
|
||||||
// can't sanely continue in this case), otherwise skip invalid prev events.
|
// can't sanely continue in this case), otherwise skip invalid prev events.
|
||||||
let prev_events = value
|
let prev_events = value
|
||||||
.get(tree)
|
.get(tree.as_str())
|
||||||
.and_then(CanonicalJsonValue::as_array)
|
.and_then(CanonicalJsonValue::as_array)
|
||||||
.ok_or_else(|| err!(Request(BadJson("event JSON for {event_id} is missing {tree}"))))?
|
.ok_or_else(|| err!(Request(BadJson("event JSON for {event_id} is missing {tree}"))))?
|
||||||
.iter()
|
.iter()
|
||||||
@@ -227,7 +241,7 @@ pub async fn get_missing_events(
|
|||||||
|
|
||||||
latest_events.clear();
|
latest_events.clear();
|
||||||
for raw_event in response.events {
|
for raw_event in response.events {
|
||||||
let (_, event_id, pdu_json) = self.parse_incoming_pdu(&raw_event).await?;
|
let (_, event_id, pdu_json) = self.parse_incoming_pdu(&raw_event, None).await?;
|
||||||
let pdu = PduEvent::from_id_val(&event_id, pdu_json).map_err(|e| {
|
let pdu = PduEvent::from_id_val(&event_id, pdu_json).map_err(|e| {
|
||||||
err!(Request(BadJson("Failed to parse gapfilled event {event_id}: {e}")))
|
err!(Request(BadJson("Failed to parse gapfilled event {event_id}: {e}")))
|
||||||
})?;
|
})?;
|
||||||
@@ -370,8 +384,8 @@ async fn fetch_event_via(
|
|||||||
.send_federation_request(&remote, get_event::v1::Request::new(event_id.clone()))
|
.send_federation_request(&remote, get_event::v1::Request::new(event_id.clone()))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let (calculated_event_id, value) = self
|
let (_, calculated_event_id, value) = self
|
||||||
.parse_incoming_pdu_with_known_room(&res.pdu, room_version_rules)
|
.parse_incoming_pdu(&res.pdu, Some(room_version_rules))
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if calculated_event_id != event_id {
|
if calculated_event_id != event_id {
|
||||||
@@ -538,11 +552,7 @@ pub(super) async fn fetch_and_handle_auth_events<Pdu>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let refmap: HashMap<OwnedEventId, &CanonicalJsonObject> = discovered_events
|
let seeded_ordered = build_local_dag(&discovered_events, DagBuilderTree::AuthEvents)
|
||||||
.iter()
|
|
||||||
.map(|(id, data)| (id.clone(), data))
|
|
||||||
.collect();
|
|
||||||
let seeded_ordered = build_local_dag(&refmap, DagBuilderTree::AuthEvents)
|
|
||||||
.await
|
.await
|
||||||
.expect("failed to build local DAG");
|
.expect("failed to build local DAG");
|
||||||
let mut pdus = HashMap::with_capacity(seeded_ordered.len());
|
let mut pdus = HashMap::with_capacity(seeded_ordered.len());
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
use std::collections::{HashMap, hash_map};
|
use std::collections::{HashMap, hash_map};
|
||||||
|
|
||||||
use conduwuit::{Err, Event, EventTypeExt, PduEvent, Result, err};
|
use conduwuit::{Err, Event, EventTypeExt, PduEvent, Result, err, warn};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, OwnedEventId, ServerName,
|
OwnedEventId, ServerName, api::federation::authorization::get_event_authorization,
|
||||||
api::federation::authorization::get_event_authorization,
|
|
||||||
room_version_rules::RoomVersionRules,
|
room_version_rules::RoomVersionRules,
|
||||||
};
|
};
|
||||||
use tokio::join;
|
use tokio::join;
|
||||||
@@ -49,17 +48,52 @@ pub(super) async fn fetch_and_persist_event_auth<Pdu>(
|
|||||||
let mut auth_chain_map = HashMap::with_capacity(event_auth.auth_chain.len());
|
let mut auth_chain_map = HashMap::with_capacity(event_auth.auth_chain.len());
|
||||||
|
|
||||||
for auth_pdu_json in event_auth.auth_chain {
|
for auth_pdu_json in event_auth.auth_chain {
|
||||||
let (auth_event_room_id, auth_event_id, auth_pdu_json) =
|
let (auth_event_room_id, auth_event_id, auth_pdu_json) = match self
|
||||||
self.parse_incoming_pdu(&auth_pdu_json).await?;
|
.parse_incoming_pdu(&auth_pdu_json, Some(room_version_rules))
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(parsed) => parsed,
|
||||||
|
| Err(e) => {
|
||||||
|
warn!(error=?e, "Dropping auth chain event as it could not be parsed");
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if let Err(e) = Self::pdu_format_check_1(
|
||||||
|
&auth_pdu_json,
|
||||||
|
room_version_rules,
|
||||||
|
create_event.event_id(),
|
||||||
|
) {
|
||||||
|
// drop this PDU
|
||||||
|
warn!(%auth_event_id, error=?e, "Dropping auth chain event as it violates the room event format");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let auth_pdu_json = match self
|
||||||
|
.signature_hash_check_2_3(auth_pdu_json, room_version_rules)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(pdu_json) => pdu_json,
|
||||||
|
| Err(e) => {
|
||||||
|
// drop this PDU
|
||||||
|
warn!(
|
||||||
|
%auth_event_id,
|
||||||
|
error=?e,
|
||||||
|
"Dropping auth chain event as it has an invalid signature"
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// PDU check 4 is done when we've finished aggregating
|
||||||
if auth_event_room_id != incoming_event.room_id_or_hash() {
|
if auth_event_room_id != incoming_event.room_id_or_hash() {
|
||||||
return Err!(Request(Forbidden(
|
return Err!(Request(Forbidden(
|
||||||
"Auth event {auth_event_id} is in {auth_event_room_id}, not {}.",
|
"Auth chain event {auth_event_id} is in {auth_event_room_id}, not {}.",
|
||||||
incoming_event.room_id_or_hash()
|
incoming_event.room_id_or_hash()
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let auth_pdu = PduEvent::from_id_val(&auth_event_id, auth_pdu_json).map_err(|e| {
|
let auth_pdu = PduEvent::from_id_val(&auth_event_id, auth_pdu_json).map_err(|e| {
|
||||||
err!(Request(BadJson("Invalid PDU {auth_event_id} in auth chain: {e}")))
|
err!(Request(BadJson("Invalid PDU {auth_event_id} in auth chain: {e}")))
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if auth_pdu.state_key().is_none() {
|
if auth_pdu.state_key().is_none() {
|
||||||
return Err!(Request(BadJson(
|
return Err!(Request(BadJson(
|
||||||
"Invalid PDU {auth_event_id} in auth_chain: not a state event"
|
"Invalid PDU {auth_event_id} in auth_chain: not a state event"
|
||||||
@@ -97,17 +131,11 @@ async fn authorise_remote_auth_chain<Pdu>(
|
|||||||
where
|
where
|
||||||
Pdu: Event + Send + Sync,
|
Pdu: Event + Send + Sync,
|
||||||
{
|
{
|
||||||
// TODO(nex): build_local_dag's signature needs changing because all callsites
|
|
||||||
// seem to do this refmap hack.
|
|
||||||
let pdu_objects = auth_chain_map
|
let pdu_objects = auth_chain_map
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(event_id, pdu)| (event_id, pdu.to_canonical_object()))
|
.map(|(event_id, pdu)| (event_id.clone(), pdu.to_canonical_object()))
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
let refmap: HashMap<OwnedEventId, &CanonicalJsonObject> = pdu_objects
|
let auth_chain_topo = build_local_dag(&pdu_objects, DagBuilderTree::AuthEvents)
|
||||||
.iter()
|
|
||||||
.map(|(id, data)| ((*id).to_owned(), data))
|
|
||||||
.collect();
|
|
||||||
let auth_chain_topo = build_local_dag(&refmap, DagBuilderTree::AuthEvents)
|
|
||||||
.await?
|
.await?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|event_id| (event_id.clone(), auth_chain_map.get(&event_id).unwrap().to_owned()))
|
.map(|event_id| (event_id.clone(), auth_chain_map.get(&event_id).unwrap().to_owned()))
|
||||||
@@ -127,6 +155,12 @@ async fn authorise_remote_auth_chain<Pdu>(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: We can't use the handy dandy `handle_outlier_pdu` function here
|
||||||
|
// because it may then try to fetch missing auth events, resulting in deep
|
||||||
|
// recursion. We will do the minimum required steps to validate the PDU here.
|
||||||
|
// Checks 1-3 were already done before this function is called, so we only need
|
||||||
|
// to do check 4.
|
||||||
|
|
||||||
let mut auth_events_by_key: HashMap<_, _> =
|
let mut auth_events_by_key: HashMap<_, _> =
|
||||||
HashMap::with_capacity(pdu.auth_events.len());
|
HashMap::with_capacity(pdu.auth_events.len());
|
||||||
|
|
||||||
@@ -150,24 +184,21 @@ async fn authorise_remote_auth_chain<Pdu>(
|
|||||||
},
|
},
|
||||||
| hash_map::Entry::Occupied(_) => {
|
| hash_map::Entry::Occupied(_) => {
|
||||||
// Duplicate auth events by key are not allowed.
|
// Duplicate auth events by key are not allowed.
|
||||||
self.services
|
self.reject_and_persist(&event_id, &pdu.to_canonical_object());
|
||||||
.pdu_metadata
|
|
||||||
.mark_event_rejected(auth_event_id);
|
|
||||||
self.services.pdu_metadata.mark_event_rejected(&event_id);
|
|
||||||
continue 'outer;
|
continue 'outer;
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !self
|
if !self
|
||||||
.is_event_self_authorised(
|
.auth_state_check_4(
|
||||||
&pdu,
|
&pdu,
|
||||||
room_version_rules,
|
room_version_rules,
|
||||||
create_event.as_pdu(),
|
create_event.as_pdu(),
|
||||||
&auth_events_by_key,
|
&auth_events_by_key,
|
||||||
)
|
)
|
||||||
.await
|
.await?
|
||||||
{
|
{
|
||||||
self.services.pdu_metadata.mark_event_rejected(&event_id);
|
self.reject_and_persist(&event_id, &pdu.to_canonical_object());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
utils::{BoolExt, IterStream, stream::BroadbandExt},
|
utils::{BoolExt, IterStream, stream::BroadbandExt},
|
||||||
};
|
};
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use ruma::{CanonicalJsonObject, MilliSecondsSinceUnixEpoch, OwnedEventId, RoomId, ServerName};
|
use ruma::{MilliSecondsSinceUnixEpoch, RoomId, ServerName};
|
||||||
|
|
||||||
use crate::rooms::event_handler::{build_local_dag, fetch_and_handle_outliers::DagBuilderTree};
|
use crate::rooms::event_handler::{build_local_dag, fetch_and_handle_outliers::DagBuilderTree};
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Fetches any missing prev_events for this event and persists them before
|
/// Fetches any missing prev_events for this event and persists them before
|
||||||
/// returning.
|
/// returning. The caller is responsible for then handling the incoming PDU.
|
||||||
pub(super) async fn fetch_prevs(
|
pub(super) async fn fetch_prevs(
|
||||||
&self,
|
&self,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
@@ -84,13 +84,7 @@ pub(super) async fn fetch_prevs(
|
|||||||
})
|
})
|
||||||
.collect::<HashMap<_, _>>();
|
.collect::<HashMap<_, _>>();
|
||||||
|
|
||||||
let to_persist = if mapped.len() <= 1 {
|
let to_persist = build_local_dag(&mapped, DagBuilderTree::PrevEvents).await?;
|
||||||
mapped.keys().map(ToOwned::to_owned).collect()
|
|
||||||
} else {
|
|
||||||
let refmap: HashMap<OwnedEventId, &CanonicalJsonObject> =
|
|
||||||
mapped.iter().map(|(id, data)| (id.clone(), data)).collect();
|
|
||||||
build_local_dag(&refmap, DagBuilderTree::PrevEvents).await?
|
|
||||||
};
|
|
||||||
|
|
||||||
let job_start = Instant::now();
|
let job_start = Instant::now();
|
||||||
trace!("Starting to persist {} prev events", to_persist.len());
|
trace!("Starting to persist {} prev events", to_persist.len());
|
||||||
|
|||||||
@@ -314,7 +314,7 @@ pub(super) async fn fetch_full_state(
|
|||||||
.iter()
|
.iter()
|
||||||
.stream()
|
.stream()
|
||||||
.broad_filter_map(|raw_event_json| async {
|
.broad_filter_map(|raw_event_json| async {
|
||||||
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json).await.ok()
|
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json, None).await.ok()
|
||||||
&& parsed.0 == room_id
|
&& parsed.0 == room_id
|
||||||
{
|
{
|
||||||
Some(parsed)
|
Some(parsed)
|
||||||
@@ -351,7 +351,7 @@ pub(super) async fn fetch_full_state(
|
|||||||
.iter()
|
.iter()
|
||||||
.stream()
|
.stream()
|
||||||
.broad_filter_map(|raw_event_json| async {
|
.broad_filter_map(|raw_event_json| async {
|
||||||
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json).await.ok()
|
if let Some(parsed) = self.parse_incoming_pdu(raw_event_json, None).await.ok()
|
||||||
&& parsed.0 == room_id
|
&& parsed.0 == room_id
|
||||||
{
|
{
|
||||||
Some(parsed)
|
Some(parsed)
|
||||||
|
|||||||
@@ -4,46 +4,37 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Event, Result, debug, debug_error, debug_warn, defer, err, error, info,
|
Err, Event, Result, debug, debug_error, debug_warn, defer, matrix::PartialPdu, trace,
|
||||||
matrix::PartialPdu, result::DebugInspect, trace, utils::time::jitter, warn,
|
utils::time::jitter,
|
||||||
};
|
};
|
||||||
use futures::{
|
use futures::{FutureExt, StreamExt, future::try_join3};
|
||||||
FutureExt, StreamExt,
|
use ruma::{CanonicalJsonValue, EventId, RoomId, ServerName, UserId};
|
||||||
future::{OptionFuture, try_join4},
|
|
||||||
};
|
|
||||||
use ruma::{CanonicalJsonValue, EventId, OwnedUserId, RoomId, ServerName, UserId};
|
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
use crate::rooms::timeline::{RawPduId, pdu_fits};
|
use crate::rooms::timeline::RawPduId;
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// When receiving an event one needs to:
|
/// Handles an incoming PDU from federation.
|
||||||
/// 0. Check the server is in the room
|
///
|
||||||
/// 1. Skip the PDU if we already know about it
|
/// First checks that we want to receive this PDU. If we already have it as
|
||||||
/// 1.1. Remove unsigned field
|
/// a timeline PDU, or we don't want to receive the PDU (e.g. origin ACL'd,
|
||||||
/// 2. Check signatures, otherwise drop
|
/// room disabled/unknown), abort.
|
||||||
/// 3. Check content hash, redact if doesn't match
|
///
|
||||||
/// 4. Fetch any missing auth events doing all checks listed here starting
|
/// The PDU is then handled as an outlier event, which performs [PDU checks]
|
||||||
/// at 1. These are not timeline events
|
/// 1 through 4. See: `handle_outlier_pdu`.
|
||||||
/// 5. Reject "due to auth events" if can't get all the auth events or some
|
///
|
||||||
/// of the auth events are also rejected "due to auth events"
|
/// Once handled as an outlier, any missing prev events are fetched, and
|
||||||
/// 6. Reject "due to auth events" if the event doesn't pass auth based on
|
/// then the PDU will be promoted/upgraded from an outlier to a timeline
|
||||||
/// the auth events
|
/// event clients can see. See: `upgrade_outlier_to_timeline_pdu`. After
|
||||||
/// 7. Persist this event as an outlier
|
/// this finishes, the PDU is either accepted or left as an outlier.
|
||||||
/// 8. If not timeline event: stop
|
///
|
||||||
/// 9. Fetch any missing prev events doing all checks listed here starting
|
/// If the PDU is successfully upgraded, the remaining extremity count of
|
||||||
/// at 1. These are timeline events
|
/// the room is checked. If there are a potentially problematic number of
|
||||||
/// 10. Fetch missing state and auth chain events by calling `/state_ids` at
|
/// forward extremities, a squasher task is started with a debounce period,
|
||||||
/// backwards extremities doing all the checks in this list starting at
|
/// which will eventually send a dummy event that ties up as many DAG forks
|
||||||
/// 1. These are not timeline events
|
/// as possible.
|
||||||
/// 11. Check the auth of the event passes based on the state of the event
|
///
|
||||||
/// 12. Ensure that the state is derived from the previous current state
|
/// [PDU checks]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
|
||||||
/// (i.e. we calculated by doing state res where one of the inputs was a
|
|
||||||
/// previously trusted set of state, don't just trust a set of state we
|
|
||||||
/// got from a remote)
|
|
||||||
/// 13. Use state resolution to find new room state
|
|
||||||
/// 14. Check if the event passes auth based on the "current state" of the
|
|
||||||
/// room, if not soft fail it
|
|
||||||
#[tracing::instrument(
|
#[tracing::instrument(
|
||||||
name = "pdu",
|
name = "pdu",
|
||||||
skip_all,
|
skip_all,
|
||||||
@@ -55,51 +46,33 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
room_id: &'a RoomId,
|
room_id: &'a RoomId,
|
||||||
event_id: &'a EventId,
|
event_id: &'a EventId,
|
||||||
value: BTreeMap<String, CanonicalJsonValue>,
|
value: BTreeMap<String, CanonicalJsonValue>,
|
||||||
is_timeline_event: bool,
|
is_backfilled_event: bool,
|
||||||
) -> Result<Option<RawPduId>> {
|
) -> Result<Option<RawPduId>> {
|
||||||
// 1. Skip the PDU if we already have it as a timeline event
|
// Skip the PDU if we already have it as a timeline event. We still re-process
|
||||||
|
// outliers in this scenario.
|
||||||
if let Ok(pdu_id) = self.services.timeline.get_pdu_id(event_id).await {
|
if let Ok(pdu_id) = self.services.timeline.get_pdu_id(event_id).await {
|
||||||
|
debug!("Database hit for incoming PDU, skipping processing");
|
||||||
return Ok(Some(pdu_id));
|
return Ok(Some(pdu_id));
|
||||||
}
|
}
|
||||||
if !pdu_fits(&mut value.clone()) {
|
|
||||||
warn!(
|
|
||||||
"dropping incoming PDU {event_id} in room {room_id} from {origin} because it \
|
|
||||||
exceeds 65535 bytes or is otherwise too large."
|
|
||||||
);
|
|
||||||
return Err!(Request(TooLarge("PDU is too large")));
|
|
||||||
}
|
|
||||||
trace!(
|
trace!(
|
||||||
"processing incoming PDU from {origin} for room {room_id} with event id {event_id}"
|
"processing incoming PDU from {origin} for room {room_id} with event id {event_id}"
|
||||||
);
|
);
|
||||||
|
|
||||||
// 1.1 Check we even know about the room
|
// If this is a membership state event for a local user, we will be interested
|
||||||
let meta_exists = self.services.metadata.exists(room_id).map(Ok);
|
// in this event even if we aren't in the room. This allows us to process things
|
||||||
|
// like revoking invites over federation, without being in the room.
|
||||||
|
let is_interesting_member_event = value.get("type").and_then(|t| t.as_str())
|
||||||
|
== Some("m.room.member")
|
||||||
|
&& value
|
||||||
|
.get("state_key")
|
||||||
|
.and_then(|s| s.as_str())
|
||||||
|
.and_then(|s| UserId::parse(s).ok())
|
||||||
|
.is_some_and(|u| self.services.globals.user_is_local(&u));
|
||||||
|
|
||||||
// 1.2 Check if the room is disabled
|
let (room_exists, is_disabled, ()) = try_join3(
|
||||||
let is_disabled = self.services.metadata.is_disabled(room_id).map(Ok);
|
self.services.metadata.exists(room_id).map(Ok),
|
||||||
|
self.services.metadata.is_disabled(room_id).map(Ok),
|
||||||
// 1.3.1 Check room ACL on origin field/server
|
self.acl_check(origin, room_id),
|
||||||
let origin_acl_check = self.acl_check(origin, room_id);
|
|
||||||
|
|
||||||
// 1.3.2 Check room ACL on sender's server name
|
|
||||||
let sender: OwnedUserId = value
|
|
||||||
.get("sender")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.ok_or_else(|| err!("No sender in object"))
|
|
||||||
.and_then(|v| Ok(UserId::parse(v)?))
|
|
||||||
.map_err(|e| err!(Request(BadJson("PDU does not have a valid sender key: {e}"))))?;
|
|
||||||
|
|
||||||
let sender_acl_check: OptionFuture<_> = sender
|
|
||||||
.server_name()
|
|
||||||
.ne(origin)
|
|
||||||
.then(|| self.acl_check(sender.server_name(), room_id))
|
|
||||||
.into();
|
|
||||||
|
|
||||||
let (meta_exists, is_disabled, (), ()) = try_join4(
|
|
||||||
meta_exists,
|
|
||||||
is_disabled,
|
|
||||||
origin_acl_check,
|
|
||||||
sender_acl_check.map(|o| o.unwrap_or(Ok(()))),
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.inspect_err(
|
.inspect_err(
|
||||||
@@ -112,21 +85,11 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self
|
if !room_exists {
|
||||||
.services
|
if is_interesting_member_event {
|
||||||
.state_cache
|
// TODO: handle interesting membership events where we aren't in
|
||||||
.server_in_room(self.services.globals.server_name(), room_id)
|
// the room
|
||||||
.await
|
}
|
||||||
{
|
|
||||||
info!(
|
|
||||||
%origin,
|
|
||||||
%room_id,
|
|
||||||
"Dropping inbound PDU for room we aren't participating in"
|
|
||||||
);
|
|
||||||
return Err!(Request(NotFound("This server is not participating in that room.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
if !meta_exists {
|
|
||||||
return Err!(Request(NotFound("Room is unknown to this server")));
|
return Err!(Request(NotFound("Room is unknown to this server")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +115,10 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
.handle_outlier_pdu(origin, create_event, event_id, room_id, value)
|
.handle_outlier_pdu(origin, create_event, event_id, room_id, value)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// 8. if not timeline event: stop
|
// If this event is being processed as part of backfill, we don't want to end up
|
||||||
if !is_timeline_event {
|
// *appending* it during the upgrade process, so we return early.
|
||||||
|
if is_backfilled_event {
|
||||||
|
debug!("Not promoting incoming event as it is being backfilled");
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +131,16 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
.await?
|
.await?
|
||||||
.origin_server_ts();
|
.origin_server_ts();
|
||||||
if incoming_pdu.origin_server_ts() < first_ts_in_room {
|
if incoming_pdu.origin_server_ts() < first_ts_in_room {
|
||||||
|
debug_warn!(
|
||||||
|
"Not promoting incoming event as it is sent before we joined the room (but was \
|
||||||
|
not backfilled)"
|
||||||
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 9. Fetch any missing prev events doing all checks listed here starting at 1.
|
// Fetch any missing prev events doing all checks listed here starting at 1.
|
||||||
// These are timeline events
|
// These are timeline events.
|
||||||
|
// TODO: This part needs to be done in a background queue somewhere.
|
||||||
|
|
||||||
debug!("Fetching and persisting any missing prev events");
|
debug!("Fetching and persisting any missing prev events");
|
||||||
Box::pin(self.fetch_prevs(
|
Box::pin(self.fetch_prevs(
|
||||||
@@ -181,13 +151,14 @@ pub async fn handle_incoming_pdu<'a>(
|
|||||||
first_ts_in_room,
|
first_ts_in_room,
|
||||||
))
|
))
|
||||||
.await
|
.await
|
||||||
.debug_inspect_err(|e| {
|
.inspect_err(|e| {
|
||||||
error!("Failed to fetch and persist incoming event's prev_events: {e:?}");
|
debug_error!("Failed to fetch and persist incoming event's prev_events: {e:?}");
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let is_dummy_event = incoming_pdu.event_type().to_string() == "org.matrix.dummy_event";
|
let is_dummy_event = incoming_pdu.event_type().to_string() == "org.matrix.dummy_event"
|
||||||
|
&& incoming_pdu.state_key().is_none();
|
||||||
|
|
||||||
// Done with prev events, now handling the incoming event
|
// Done with prev events, now we can handle promoting the PDU
|
||||||
let pdu_id = Box::pin(self.upgrade_outlier_to_timeline_pdu(
|
let pdu_id = Box::pin(self.upgrade_outlier_to_timeline_pdu(
|
||||||
incoming_pdu,
|
incoming_pdu,
|
||||||
val,
|
val,
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
use std::collections::{BTreeMap, HashMap, hash_map};
|
use std::collections::{BTreeMap, HashMap, hash_map};
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Event, EventTypeExt, PduEvent, Result, debug, debug_info, debug_warn, err, info,
|
Err, Event, EventTypeExt, PduEvent, Result, debug, debug_warn, err, info, trace,
|
||||||
matrix::StateKey, state_res::auth_check, trace, warn,
|
|
||||||
};
|
|
||||||
use futures::future::ready;
|
|
||||||
use ruma::{
|
|
||||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
|
||||||
canonical_json::redact, events::StateEventType, room_version_rules::RoomVersionRules,
|
|
||||||
};
|
};
|
||||||
|
use ruma::{CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName};
|
||||||
|
|
||||||
use super::{check_room_id, get_room_version_rules};
|
use super::get_room_version_rules;
|
||||||
use crate::rooms::timeline::pdu_fits;
|
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Handles a PDU as an outlier, performing basic checks like signatures and
|
/// Handles a PDU as an outlier, performing basic checks like signatures and
|
||||||
/// hashes, proclaimed event auth, and then adding it to the outlier tree.
|
/// hashes, proclaimed event auth, and then adding it to the outlier tree.
|
||||||
|
///
|
||||||
|
/// This performs steps 1 through 4 of [S-S section 5.1][spec], returning
|
||||||
|
/// the parsed PDU and modified JSON object.
|
||||||
|
///
|
||||||
|
/// **External callers likely want `handle_incoming_pdu` instead.**
|
||||||
|
///
|
||||||
|
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
#[tracing::instrument(name="handle_outlier", skip_all, fields(%event_id))]
|
#[tracing::instrument(name = "handle_outlier", skip_all)]
|
||||||
pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
pub async fn handle_outlier_pdu<'a, Pdu>(
|
||||||
&self,
|
&self,
|
||||||
origin: &'a ServerName,
|
origin: &'a ServerName,
|
||||||
create_event: &'a Pdu,
|
create_event: &'a Pdu,
|
||||||
@@ -29,61 +30,38 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
where
|
where
|
||||||
Pdu: Event + Send + Sync,
|
Pdu: Event + Send + Sync,
|
||||||
{
|
{
|
||||||
if !pdu_fits(&mut value.clone()) {
|
// Skip outlier handling if we already have this event as either a timeline or
|
||||||
warn!(
|
// outlier PDU.
|
||||||
"dropping incoming PDU {event_id} in room {room_id} from {origin} because it \
|
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
|
||||||
exceeds 65535 bytes or is otherwise too large."
|
debug!(
|
||||||
|
"Database hit for {event_id} (event is either an outlier or already promoted), \
|
||||||
|
skipping outlier handling"
|
||||||
);
|
);
|
||||||
return Err!(Request(TooLarge("PDU is too large")));
|
value.insert(
|
||||||
|
"event_id".to_owned(),
|
||||||
|
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
||||||
|
);
|
||||||
|
return Ok((pdu_event, value));
|
||||||
}
|
}
|
||||||
// 1. Remove unsigned field
|
|
||||||
|
// 1. Check that the PDU follows the format for the room version
|
||||||
|
// (in this case, just size check)
|
||||||
|
let room_version_rules = get_room_version_rules(create_event)?;
|
||||||
|
Self::pdu_format_check_1(&value, &room_version_rules, create_event.event_id())
|
||||||
|
.inspect_err(|e| {
|
||||||
|
info!(
|
||||||
|
err=?e,
|
||||||
|
"Dropping incoming PDU from {origin} because it violates the room event format"
|
||||||
|
);
|
||||||
|
})?;
|
||||||
|
|
||||||
value.remove("unsigned");
|
value.remove("unsigned");
|
||||||
|
|
||||||
// 2. Check signatures, otherwise drop
|
// 2. Check signatures, otherwise drop.
|
||||||
// 3. check content hash, redact if doesn't match
|
// 3. Check content hash, redacting the event if it fails.
|
||||||
let room_version_rules = get_room_version_rules(create_event)?;
|
let mut incoming_pdu = self
|
||||||
let mut incoming_pdu = match self
|
.signature_hash_check_2_3(value, &room_version_rules)
|
||||||
.services
|
.await?;
|
||||||
.server_keys
|
|
||||||
.verify_event(&value, &room_version_rules)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(ruma::signatures::Verified::All) => {
|
|
||||||
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
|
|
||||||
debug!(
|
|
||||||
"Already have event {event_id} as an outlier or timeline event, not \
|
|
||||||
re-processing"
|
|
||||||
);
|
|
||||||
value.insert(
|
|
||||||
"event_id".to_owned(),
|
|
||||||
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
|
||||||
);
|
|
||||||
check_room_id(room_id, &pdu_event)?;
|
|
||||||
return Ok((pdu_event, value));
|
|
||||||
}
|
|
||||||
value
|
|
||||||
},
|
|
||||||
| Ok(ruma::signatures::Verified::Signatures) => {
|
|
||||||
if let Ok(pdu_event) = self.services.timeline.get_pdu(event_id).await {
|
|
||||||
debug!(
|
|
||||||
"Received a redacted copy of {event_id}, but we already knew about it. \
|
|
||||||
Re-using known content instead."
|
|
||||||
);
|
|
||||||
check_room_id(room_id, &pdu_event)?;
|
|
||||||
let obj = pdu_event.to_canonical_object();
|
|
||||||
return Ok((pdu_event, obj));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug_info!("Calculated hash does not match (redaction): {event_id}");
|
|
||||||
redact(value, &room_version_rules.redaction, None)
|
|
||||||
.map_err(|e| err!(Request(BadJson("Failed to redact {event_id}: {e}"))))?
|
|
||||||
},
|
|
||||||
| Err(e) => {
|
|
||||||
return Err!(Request(Forbidden(debug_error!(
|
|
||||||
"Signature verification failed for {event_id}: {e}"
|
|
||||||
))));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now that we have checked the signature and hashes we can add the eventID and
|
// Now that we have checked the signature and hashes we can add the eventID and
|
||||||
// convert to our PduEvent type
|
// convert to our PduEvent type
|
||||||
@@ -91,14 +69,11 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
"event_id".to_owned(),
|
"event_id".to_owned(),
|
||||||
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
CanonicalJsonValue::String(event_id.as_str().to_owned()),
|
||||||
);
|
);
|
||||||
|
|
||||||
let pdu_event = serde_json::from_value::<PduEvent>(
|
let pdu_event = serde_json::from_value::<PduEvent>(
|
||||||
serde_json::to_value(&incoming_pdu).expect("CanonicalJsonObj is a valid JsonValue"),
|
serde_json::to_value(&incoming_pdu).expect("CanonicalJsonObj is a valid JsonValue"),
|
||||||
)
|
)
|
||||||
.map_err(|e| err!(Request(BadJson(debug_warn!("Event is not a valid PDU: {e}")))))?;
|
.map_err(|e| err!(Request(BadJson(debug_warn!("Event is not a valid PDU: {e}")))))?;
|
||||||
|
|
||||||
check_room_id(room_id, &pdu_event)?;
|
|
||||||
|
|
||||||
// TODO(nex): From hereon the event is not dropped, and thus always added as an
|
// TODO(nex): From hereon the event is not dropped, and thus always added as an
|
||||||
// outlier. However, we only do that at the end of this function, which means
|
// outlier. However, we only do that at the end of this function, which means
|
||||||
// several duplicated calls to add_pdu_outlier. Shouldn't we just do it here
|
// several duplicated calls to add_pdu_outlier. Shouldn't we just do it here
|
||||||
@@ -116,7 +91,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
|
|
||||||
for auth_event_id in pdu_event.auth_events() {
|
for auth_event_id in pdu_event.auth_events() {
|
||||||
if let Ok(auth_event) = self.services.timeline.get_pdu(auth_event_id).await {
|
if let Ok(auth_event) = self.services.timeline.get_pdu(auth_event_id).await {
|
||||||
check_room_id(room_id, &auth_event)?;
|
|
||||||
trace!("Found auth event {auth_event_id} for outlier event {event_id} locally");
|
trace!("Found auth event {auth_event_id} for outlier event {event_id} locally");
|
||||||
auth_events.insert(auth_event_id.to_owned(), auth_event);
|
auth_events.insert(auth_event_id.to_owned(), auth_event);
|
||||||
} else {
|
} else {
|
||||||
@@ -154,7 +128,7 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure none of the auth events are rejected - if they are, reject too.
|
// Ensure none of the auth events are rejected - if they are, reject too.
|
||||||
for auth_event_id in auth_events.keys() {
|
for (auth_event_id, auth_event) in &auth_events {
|
||||||
if self
|
if self
|
||||||
.services
|
.services
|
||||||
.pdu_metadata
|
.pdu_metadata
|
||||||
@@ -166,15 +140,27 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
{auth_event_id}",
|
{auth_event_id}",
|
||||||
event_id,
|
event_id,
|
||||||
);
|
);
|
||||||
self.services.pdu_metadata.mark_event_rejected(event_id);
|
self.reject_and_persist(event_id, &incoming_pdu);
|
||||||
return Err!(Request(Forbidden(
|
return Err!(Request(Forbidden(
|
||||||
"Event has rejected auth event: {auth_event_id}"
|
"Event has rejected auth event: {auth_event_id}"
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
if auth_event.room_id_or_hash() != room_id {
|
||||||
|
debug_warn!(
|
||||||
|
%auth_event_id,
|
||||||
|
auth_event_room_id=%auth_event.room_id_or_hash(),
|
||||||
|
expected_room_id=%room_id,
|
||||||
|
"Rejecting incoming event which depends on an auth event in another room.",
|
||||||
|
);
|
||||||
|
self.reject_and_persist(event_id, &incoming_pdu);
|
||||||
|
return Err!(Request(Forbidden(
|
||||||
|
"Event depends on a cross-room auth event: {auth_event_id}"
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
|
// 4. Reject "due to auth events" if the event doesn't pass auth based on the
|
||||||
// auth events
|
// claimed auth events
|
||||||
debug!("Checking based on auth events");
|
debug!("Checking based on auth events");
|
||||||
let mut auth_events_by_key: HashMap<_, _> = HashMap::with_capacity(auth_events.len());
|
let mut auth_events_by_key: HashMap<_, _> = HashMap::with_capacity(auth_events.len());
|
||||||
// Build map of auth events
|
// Build map of auth events
|
||||||
@@ -184,8 +170,6 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
.expect("we just checked that we have all auth events")
|
.expect("we just checked that we have all auth events")
|
||||||
.to_owned();
|
.to_owned();
|
||||||
|
|
||||||
check_room_id(room_id, &auth_event)?;
|
|
||||||
|
|
||||||
let key = auth_event.kind().with_state_key(
|
let key = auth_event.kind().with_state_key(
|
||||||
auth_event
|
auth_event
|
||||||
.state_key
|
.state_key
|
||||||
@@ -197,98 +181,45 @@ pub(super) async fn handle_outlier_pdu<'a, Pdu>(
|
|||||||
v.insert(auth_event);
|
v.insert(auth_event);
|
||||||
},
|
},
|
||||||
| hash_map::Entry::Occupied(_) => {
|
| hash_map::Entry::Occupied(_) => {
|
||||||
self.services
|
self.reject_and_persist(event_id, &incoming_pdu);
|
||||||
.outlier
|
return Err!(Request(Forbidden(debug_warn!(
|
||||||
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
|
|
||||||
self.services.pdu_metadata.mark_event_rejected(event_id);
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"Auth event's type and state_key combination exists multiple times: {}, \
|
"Auth event's type and state_key combination exists multiple times: {}, \
|
||||||
{}",
|
{}",
|
||||||
auth_event.kind,
|
auth_event.kind,
|
||||||
auth_event.state_key().unwrap_or("")
|
auth_event.state_key().unwrap_or("")
|
||||||
)));
|
))));
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self
|
if !self
|
||||||
.is_event_self_authorised(
|
.auth_state_check_4(
|
||||||
&pdu_event,
|
&pdu_event,
|
||||||
&room_version_rules,
|
&room_version_rules,
|
||||||
create_event.as_pdu(),
|
create_event.as_pdu(),
|
||||||
&auth_events_by_key,
|
&auth_events_by_key,
|
||||||
)
|
)
|
||||||
.await
|
.await?
|
||||||
{
|
{
|
||||||
self.services.pdu_metadata.mark_event_rejected(event_id);
|
self.reject_and_persist(event_id, &incoming_pdu);
|
||||||
self.services
|
return Err!(Request(Forbidden(debug_warn!(
|
||||||
.outlier
|
|
||||||
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"Event authorisation fails based on event's claimed auth events"
|
"Event authorisation fails based on event's claimed auth events"
|
||||||
)));
|
))));
|
||||||
}
|
}
|
||||||
|
|
||||||
trace!("Validation successful.");
|
|
||||||
|
|
||||||
// 7. Persist the event as an outlier.
|
// 7. Persist the event as an outlier.
|
||||||
self.services
|
self.services
|
||||||
.outlier
|
.outlier
|
||||||
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
|
.add_pdu_outlier(pdu_event.event_id(), &incoming_pdu);
|
||||||
|
|
||||||
trace!("Added pdu as outlier.");
|
debug!("PDU passed checks and has been persisted as an outlier");
|
||||||
|
|
||||||
Ok((pdu_event, incoming_pdu))
|
Ok((pdu_event, incoming_pdu))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper method that turns the return value of `is_event_self_authorised`
|
/// Marks the event as rejected and then saves it as an outlier.
|
||||||
/// into a `Result` depending on the value.
|
pub(super) fn reject_and_persist(&self, event_id: &EventId, pdu: &CanonicalJsonObject) {
|
||||||
///
|
self.services.pdu_metadata.mark_event_rejected(event_id);
|
||||||
/// If the event is not authorised, a Forbidden error is returned.
|
self.services.outlier.add_pdu_outlier(event_id, pdu);
|
||||||
/// Otherwise, an empty `Ok`.
|
|
||||||
pub(super) async fn is_event_self_authorised(
|
|
||||||
&self,
|
|
||||||
pdu: &PduEvent,
|
|
||||||
room_version_rules: &RoomVersionRules,
|
|
||||||
create_event: &PduEvent,
|
|
||||||
auth_events_by_key: &HashMap<(StateEventType, StateKey), PduEvent>,
|
|
||||||
) -> bool {
|
|
||||||
self.expect_event_is_self_authorised(
|
|
||||||
pdu,
|
|
||||||
room_version_rules,
|
|
||||||
create_event,
|
|
||||||
auth_events_by_key,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Checks PDU check 4: Passes authorisation rules based on the event's auth
|
|
||||||
/// events ([spec]).
|
|
||||||
///
|
|
||||||
/// If the auth check fails, false is returned, otherwise true.
|
|
||||||
///
|
|
||||||
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
|
|
||||||
pub(super) async fn expect_event_is_self_authorised(
|
|
||||||
&self,
|
|
||||||
pdu: &PduEvent,
|
|
||||||
room_version_rules: &RoomVersionRules,
|
|
||||||
create_event: &PduEvent,
|
|
||||||
auth_events_by_key: &HashMap<(StateEventType, StateKey), PduEvent>,
|
|
||||||
) -> Result<bool> {
|
|
||||||
let state_fetch = |ty: &StateEventType, sk: &str| {
|
|
||||||
let key = (ty.to_owned(), sk.into());
|
|
||||||
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
|
|
||||||
};
|
|
||||||
|
|
||||||
auth_check(
|
|
||||||
room_version_rules,
|
|
||||||
pdu,
|
|
||||||
None, // TODO: third party invite
|
|
||||||
state_fetch,
|
|
||||||
create_event,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| err!("Event self-authentication failed: {e:?}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
mod handle_incoming_pdu;
|
mod handle_incoming_pdu;
|
||||||
mod handle_outlier_pdu;
|
mod handle_outlier_pdu;
|
||||||
mod parse_incoming_pdu;
|
mod parse_incoming_pdu;
|
||||||
|
pub mod pdu_checks;
|
||||||
mod policy_server;
|
mod policy_server;
|
||||||
mod resolve_state;
|
mod resolve_state;
|
||||||
mod state_at_incoming;
|
mod state_at_incoming;
|
||||||
@@ -19,7 +20,7 @@
|
|||||||
DagBuilderTree, GET_MISSING_EVENTS_MAX_BATCH_SIZE, build_local_dag,
|
DagBuilderTree, GET_MISSING_EVENTS_MAX_BATCH_SIZE, build_local_dag,
|
||||||
};
|
};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
OwnedEventId, OwnedRoomId, RoomId, events::room::create::RoomCreateEventContent,
|
OwnedEventId, OwnedRoomId, events::room::create::RoomCreateEventContent,
|
||||||
room_version_rules::RoomVersionRules,
|
room_version_rules::RoomVersionRules,
|
||||||
};
|
};
|
||||||
use tokio::sync::{Notify, mpsc};
|
use tokio::sync::{Notify, mpsc};
|
||||||
@@ -114,22 +115,6 @@ async fn event_fetch(&self, event_id: OwnedEventId) -> Option<PduEvent> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_room_id<Pdu: Event>(room_id: &RoomId, pdu: &Pdu) -> Result {
|
|
||||||
if pdu
|
|
||||||
.room_id()
|
|
||||||
.is_some_and(|claimed_room_id| claimed_room_id != room_id)
|
|
||||||
{
|
|
||||||
return Err!(Request(InvalidParam(error!(
|
|
||||||
pdu_event_id = %pdu.event_id(),
|
|
||||||
pdu_room_id = pdu.room_id().map(tracing::field::display),
|
|
||||||
%room_id,
|
|
||||||
"Found event from room in room",
|
|
||||||
))));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_room_version_rules<Pdu: Event>(create_event: &Pdu) -> Result<RoomVersionRules> {
|
fn get_room_version_rules<Pdu: Event>(create_event: &Pdu) -> Result<RoomVersionRules> {
|
||||||
let content: RoomCreateEventContent = create_event.get_content()?;
|
let content: RoomCreateEventContent = create_event.get_content()?;
|
||||||
let Some(room_version_rules) = content.room_version.rules() else {
|
let Some(room_version_rules) = content.room_version.rules() else {
|
||||||
|
|||||||
@@ -23,7 +23,12 @@ fn extract_room_id(event_type: &str, pdu: &CanonicalJsonObject) -> Result<OwnedR
|
|||||||
.map_err(|e| err!(Request(BadJson("Invalid room_id {room_id:?} in pdu: {e}"))));
|
.map_err(|e| err!(Request(BadJson("Invalid room_id {room_id:?} in pdu: {e}"))));
|
||||||
}
|
}
|
||||||
// If there's no room ID, and this is not a create event, it is illegal.
|
// If there's no room ID, and this is not a create event, it is illegal.
|
||||||
if event_type != "m.room.create" || pdu.get("state_key").is_none() {
|
let is_create = event_type == "m.room.create"
|
||||||
|
&& pdu
|
||||||
|
.get("state_key")
|
||||||
|
.and_then(|v| v.as_str())
|
||||||
|
.is_some_and(str::is_empty);
|
||||||
|
if !is_create {
|
||||||
return Err!(Request(BadJson("Missing room_id in pdu")));
|
return Err!(Request(BadJson("Missing room_id in pdu")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,10 +47,7 @@ fn extract_room_id(event_type: &str, pdu: &CanonicalJsonObject) -> Result<OwnedR
|
|||||||
return Err!(Request(BadJson("Unknown room version in pdu")));
|
return Err!(Request(BadJson("Unknown room version in pdu")));
|
||||||
};
|
};
|
||||||
|
|
||||||
if !room_version_rules
|
if room_version_rules.event_format.require_room_create_room_id {
|
||||||
.authorization
|
|
||||||
.room_create_event_id_as_room_id
|
|
||||||
{
|
|
||||||
return Err!(Request(BadJson("Missing room_id in pdu")));
|
return Err!(Request(BadJson("Missing room_id in pdu")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,45 +82,14 @@ pub(super) fn expect_event_id_array(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Performs some basic validation on the PDU to make sure it's not
|
/// Parses an incoming PDU JSON object, generating an event ID for it and
|
||||||
/// obviously malformed. This is not a full validation, but guards against
|
/// attempts to discover the associated room ID. Does not insert the event
|
||||||
/// extreme errors.
|
/// ID into the returned object.
|
||||||
///
|
pub async fn parse_incoming_pdu(
|
||||||
/// Currently, this just validates that prev/auth events are within
|
|
||||||
/// acceptable ranges. Other servers do some additional things like
|
|
||||||
/// checking depth range, but serde will do that later when converting the
|
|
||||||
/// object to a PduEvent.
|
|
||||||
pub fn validate_pdu(&self, pdu: &CanonicalJsonObject) -> Result {
|
|
||||||
// Since v3:
|
|
||||||
// `event_id` should not be present on the PDU.
|
|
||||||
// NOTE: The above is ignored since technically it's still allowed to be
|
|
||||||
// included, but should be ignored instead.
|
|
||||||
// `auth_events` and `prev_events` must be an array of event IDs
|
|
||||||
let auth_events = expect_event_id_array(pdu, "auth_events")?;
|
|
||||||
if auth_events.len() > 10 {
|
|
||||||
return Err!(Request(BadJson("PDU has too many auth events")));
|
|
||||||
}
|
|
||||||
let prev_events = expect_event_id_array(pdu, "prev_events")?;
|
|
||||||
if prev_events.len() > 20 {
|
|
||||||
return Err!(Request(BadJson("PDU has too many prev events")));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn parse_incoming_pdu_with_known_room(
|
|
||||||
&self,
|
&self,
|
||||||
pdu: &RawJsonValue,
|
pdu: &RawJsonValue,
|
||||||
room_version_rules: &RoomVersionRules,
|
room_version_rules: Option<&RoomVersionRules>,
|
||||||
) -> Result<(OwnedEventId, CanonicalJsonObject)> {
|
) -> Result<Parsed> {
|
||||||
let (event_id, value) =
|
|
||||||
gen_event_id_canonical_json(pdu, room_version_rules).map_err(|e| {
|
|
||||||
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
|
|
||||||
})?;
|
|
||||||
self.validate_pdu(&value)?;
|
|
||||||
Ok((event_id, value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
|
|
||||||
let value = serde_json::from_str::<CanonicalJsonObject>(pdu.get()).map_err(|e| {
|
let value = serde_json::from_str::<CanonicalJsonObject>(pdu.get()).map_err(|e| {
|
||||||
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
|
err!(BadServerResponse(debug_warn!("Error parsing incoming event {e:?}")))
|
||||||
})?;
|
})?;
|
||||||
@@ -129,20 +100,23 @@ pub async fn parse_incoming_pdu(&self, pdu: &RawJsonValue) -> Result<Parsed> {
|
|||||||
|
|
||||||
let room_id = extract_room_id(event_type, &value)?;
|
let room_id = extract_room_id(event_type, &value)?;
|
||||||
|
|
||||||
let room_version_rules = self
|
let room_version_rules = match room_version_rules {
|
||||||
.services
|
| Some(r) => r,
|
||||||
.state
|
| None => &self
|
||||||
.get_room_version(&room_id)
|
.services
|
||||||
.await
|
.state
|
||||||
.unwrap_or(RoomVersionId::V1)
|
.get_room_version(&room_id)
|
||||||
.rules()
|
.await
|
||||||
.unwrap();
|
.unwrap_or(RoomVersionId::V1)
|
||||||
|
.rules()
|
||||||
|
.expect("room version must be supported"),
|
||||||
|
};
|
||||||
|
|
||||||
let (event_id, value) =
|
let (event_id, value) =
|
||||||
gen_event_id_canonical_json(pdu, &room_version_rules).map_err(|e| {
|
gen_event_id_canonical_json(pdu, room_version_rules).map_err(|e| {
|
||||||
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
|
err!(Request(InvalidParam("Could not convert event to canonical json: {e}")))
|
||||||
})?;
|
})?;
|
||||||
self.validate_pdu(&value)?;
|
// NOTE: validation checks are now performed by `pdu_format_check_1`.
|
||||||
Ok((room_id, event_id, value))
|
Ok((room_id, event_id, value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use conduwuit::{
|
||||||
|
Err, Event, EventTypeExt, PduEvent, Result, debug, debug::DebugInspect, debug_error,
|
||||||
|
debug_info, err, info, matrix::StateKey, state_res, trace,
|
||||||
|
};
|
||||||
|
use futures::future::ready;
|
||||||
|
use ruma::{
|
||||||
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, ServerName,
|
||||||
|
api::error::ErrorKind, canonical_json::redact, events::StateEventType,
|
||||||
|
room_version_rules::RoomVersionRules,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::rooms::{
|
||||||
|
event_handler::parse_incoming_pdu::expect_event_id_array, timeline::pdu_fits,
|
||||||
|
};
|
||||||
|
|
||||||
|
impl super::Service {
|
||||||
|
/// Checks that the PDU conforms to the PDU format (check 1). This is
|
||||||
|
/// already mostly done during deserialisation, so this function just checks
|
||||||
|
/// that the PDU isn't a too large.
|
||||||
|
pub fn pdu_format_check_1(
|
||||||
|
pdu_json: &CanonicalJsonObject,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
create_event_id: &EventId,
|
||||||
|
) -> Result<()> {
|
||||||
|
let event_format = &room_version_rules.event_format;
|
||||||
|
// NOTE: if we do any more validation outside of deserialisation, it has to be
|
||||||
|
// done here.
|
||||||
|
|
||||||
|
if !pdu_fits(pdu_json) {
|
||||||
|
return Err!(Request(TooLarge("PDU is too large")));
|
||||||
|
}
|
||||||
|
|
||||||
|
if event_format.require_room_create_room_id {
|
||||||
|
if pdu_json.get("room_id").is_none() {
|
||||||
|
return Err!(Request(BadJson("Missing required PDU field: `room_id`")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_events = expect_event_id_array(pdu_json, "auth_events")?;
|
||||||
|
if auth_events.len() > 10 {
|
||||||
|
return Err!(Request(BadJson("PDU has too many auth events")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// The m.room.create event is the genesis event and has empty auth_events
|
||||||
|
// by definition, so it is exempt from the checks below requiring or
|
||||||
|
// forbidding the create event in auth_events (it cannot reference itself).
|
||||||
|
let Some(event_type) = pdu_json.get("type").and_then(CanonicalJsonValue::as_str) else {
|
||||||
|
return Err!(Request(BadJson("PDU is missing a type")));
|
||||||
|
};
|
||||||
|
let state_key = pdu_json
|
||||||
|
.get("state_key")
|
||||||
|
.and_then(CanonicalJsonValue::as_str);
|
||||||
|
|
||||||
|
let is_create_event = event_type == "m.room.create" && state_key == Some("");
|
||||||
|
|
||||||
|
if !is_create_event {
|
||||||
|
let create_event_in_auth_events = auth_events.iter().any(|id| id == create_event_id);
|
||||||
|
if !event_format.allow_room_create_in_auth_events && create_event_in_auth_events {
|
||||||
|
return Err!(Request(BadJson("PDU references a create event")));
|
||||||
|
} else if event_format.allow_room_create_in_auth_events
|
||||||
|
&& !create_event_in_auth_events
|
||||||
|
{
|
||||||
|
return Err!(Request(BadJson("PDU does not reference the room create event")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prev_events = expect_event_id_array(pdu_json, "prev_events")?;
|
||||||
|
if prev_events.len() > 20 {
|
||||||
|
return Err!(Request(BadJson("PDU has too many prev events")));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that the PDU has a valid signature (check 2), and redacts it if
|
||||||
|
/// the content hash verification fails (check 3), returning the
|
||||||
|
/// potentially modified JSON. Returns an error if the PDU cannot be
|
||||||
|
/// redacted, or fails signature verification.
|
||||||
|
pub async fn signature_hash_check_2_3(
|
||||||
|
&self,
|
||||||
|
pdu_json: CanonicalJsonObject,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
) -> Result<CanonicalJsonObject> {
|
||||||
|
match self
|
||||||
|
.services
|
||||||
|
.server_keys
|
||||||
|
.verify_event(&pdu_json, room_version_rules)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
| Ok(ruma::signatures::Verified::All) => {
|
||||||
|
trace!("Signatures and hashes verified successfully");
|
||||||
|
Ok(pdu_json)
|
||||||
|
},
|
||||||
|
| Ok(ruma::signatures::Verified::Signatures) => {
|
||||||
|
debug_info!("Content hash mismatch, redacting event and continuing");
|
||||||
|
let redacted = redact(pdu_json, &room_version_rules.redaction, None)
|
||||||
|
.map_err(|e| err!(Request(BadJson("Unable to redact event: {e}"))))?;
|
||||||
|
Ok(redacted)
|
||||||
|
},
|
||||||
|
| Err(e) => {
|
||||||
|
Err!(Request(Forbidden(debug_error!("Signature verification failed: {e}"))))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks PDU check 4: Passes authorisation rules based on the event's auth
|
||||||
|
/// events ([spec]).
|
||||||
|
///
|
||||||
|
/// If the auth check fails, false is returned, otherwise true.
|
||||||
|
///
|
||||||
|
/// [spec]: https://spec.matrix.org/v1.19/server-server-api/#checks-performed-on-receipt-of-a-pdu
|
||||||
|
pub async fn auth_state_check_4(
|
||||||
|
&self,
|
||||||
|
pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
create_event: &PduEvent,
|
||||||
|
auth_events_by_key: &HashMap<(StateEventType, StateKey), PduEvent>,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let state_fetch = |ty: &StateEventType, sk: &str| {
|
||||||
|
let key = (ty.to_owned(), sk.into());
|
||||||
|
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
|
||||||
|
};
|
||||||
|
|
||||||
|
state_res::event_auth::auth_check(
|
||||||
|
room_version_rules,
|
||||||
|
pdu,
|
||||||
|
None, // TODO: third party invite
|
||||||
|
state_fetch,
|
||||||
|
create_event,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!("Event self-authentication failed: {e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that the event passes PDU check 5, which ensures that the event
|
||||||
|
/// is authorised based on the state before the event (which is the resolved
|
||||||
|
/// state across all prev events).
|
||||||
|
///
|
||||||
|
/// Returns a boolean indicating whether the event is authorised, and also
|
||||||
|
/// the resolved state before the event for later use. Returns an error if
|
||||||
|
/// state fetching or auth checking fails.
|
||||||
|
pub(super) async fn state_before_check_5(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
create_event: &PduEvent,
|
||||||
|
origin: &ServerName,
|
||||||
|
) -> Result<(bool, HashMap<u64, OwnedEventId>)> {
|
||||||
|
debug!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
"Resolving state at event"
|
||||||
|
);
|
||||||
|
let room_id = incoming_pdu.room_id_or_hash();
|
||||||
|
|
||||||
|
// If the incoming event only has one prev event, we can just use the state at
|
||||||
|
// that event, but otherwise we have to resolve across each fork. If we're
|
||||||
|
// missing even one of the prev events, we have to ask a remote server for help.
|
||||||
|
//
|
||||||
|
// TODO: this can be optimised by only loading auth chain events into memory,
|
||||||
|
// rather than the entire state.
|
||||||
|
let state_before = self
|
||||||
|
.state_before_incoming(&incoming_pdu, room_version_rules)
|
||||||
|
.await?;
|
||||||
|
let state_before = match state_before {
|
||||||
|
| Some(s) => s,
|
||||||
|
| None => {
|
||||||
|
trace!("Could not calculate incoming state, asking remote {origin} for it");
|
||||||
|
self.fetch_state(origin, create_event, &room_id, incoming_pdu.event_id())
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
debug_error!("Could not fetch state from {origin}: {e}");
|
||||||
|
})?
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if state_before.is_empty()
|
||||||
|
&& *incoming_pdu.event_type() != StateEventType::RoomCreate.into()
|
||||||
|
{
|
||||||
|
// This can happen if the remote sends an event but cannot be reached to fetch
|
||||||
|
// the state at it, and all other servers in the room (which might just be the
|
||||||
|
// unreachable server) are unable to provide required info.
|
||||||
|
// returning an error here allows the upgrade to be attempted at another time.
|
||||||
|
return Err!(Request(Forbidden("Could not resolve incoming state before event")));
|
||||||
|
}
|
||||||
|
trace!(state_events = state_before.len(), "Calculated incoming state");
|
||||||
|
|
||||||
|
let state_fetch_state = &state_before;
|
||||||
|
let state_fetch = |k: StateEventType, s: StateKey| async move {
|
||||||
|
let shortstatekey = self.services.short.get_shortstatekey(&k, &s).await.ok()?;
|
||||||
|
|
||||||
|
let event_id = state_fetch_state.get(&shortstatekey)?;
|
||||||
|
self.services.timeline.get_pdu(event_id).await.ok()
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
"Running state-before auth check"
|
||||||
|
);
|
||||||
|
|
||||||
|
// PDU check: 5
|
||||||
|
let auth_check = state_res::event_auth::auth_check(
|
||||||
|
room_version_rules,
|
||||||
|
incoming_pdu,
|
||||||
|
None, // TODO: third party invite
|
||||||
|
|ty, sk| state_fetch(ty.clone(), sk.into()),
|
||||||
|
create_event.as_pdu(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
|
||||||
|
Ok((auth_check, state_before))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks that the event passes PDU check 6, which ensures that the event
|
||||||
|
/// is authorised based on the room's current state (which is the resolved
|
||||||
|
/// state across all current forward extremities).
|
||||||
|
///
|
||||||
|
/// Returns a boolean indicating whether the event is authorised, or an
|
||||||
|
/// error if the auth check fails.
|
||||||
|
pub(super) async fn current_state_check_6(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
create_event: &PduEvent,
|
||||||
|
) -> Result<bool> {
|
||||||
|
debug!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
"Gathering auth events"
|
||||||
|
);
|
||||||
|
let auth_events = self
|
||||||
|
.services
|
||||||
|
.state
|
||||||
|
.get_auth_events(
|
||||||
|
&incoming_pdu.room_id_or_hash(),
|
||||||
|
incoming_pdu.kind(),
|
||||||
|
incoming_pdu.sender(),
|
||||||
|
incoming_pdu.state_key(),
|
||||||
|
incoming_pdu.content(),
|
||||||
|
room_version_rules,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let state_fetch = |k: &StateEventType, s: &str| {
|
||||||
|
let key = k.with_state_key(s);
|
||||||
|
ready(auth_events.get(&key).map(ToOwned::to_owned))
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
"Running current state auth check"
|
||||||
|
);
|
||||||
|
state_res::event_auth::auth_check(
|
||||||
|
room_version_rules,
|
||||||
|
incoming_pdu,
|
||||||
|
None, // third-party invite
|
||||||
|
state_fetch,
|
||||||
|
create_event.as_pdu(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs PDU check 7 - does the policy server allow this event.
|
||||||
|
///
|
||||||
|
/// If the policy server forbids the event, false is returned. If there is a
|
||||||
|
/// problem contacting the policy server, or it returns an unrecognised
|
||||||
|
/// response, an appropriate error is returned.
|
||||||
|
pub(super) async fn policy_server_check_7(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &PduEvent,
|
||||||
|
pdu_json: &mut CanonicalJsonObject,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
) -> Result<bool> {
|
||||||
|
let event_id = pdu_json
|
||||||
|
.remove("event_id")
|
||||||
|
.expect("event_id should be present in pdu_json at this stage");
|
||||||
|
if let Err(e) = self
|
||||||
|
.policy_server_allows_event(
|
||||||
|
incoming_pdu,
|
||||||
|
pdu_json,
|
||||||
|
&incoming_pdu.room_id_or_hash(),
|
||||||
|
room_version_rules,
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.debug_inspect(|()| {
|
||||||
|
debug!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
"Event has passed policy server check."
|
||||||
|
);
|
||||||
|
}) {
|
||||||
|
return if matches!(e.kind(), ErrorKind::Forbidden) {
|
||||||
|
info!(
|
||||||
|
event_id = %incoming_pdu.event_id,
|
||||||
|
error = %e,
|
||||||
|
"Event has been marked as spam by policy server: {}",
|
||||||
|
e.message(),
|
||||||
|
);
|
||||||
|
Ok(false)
|
||||||
|
} else {
|
||||||
|
Err(e)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
pdu_json.insert("event_id".to_owned(), event_id);
|
||||||
|
Ok(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,10 +16,31 @@
|
|||||||
use crate::rooms::short::ShortStateHash;
|
use crate::rooms::short::ShortStateHash;
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
// TODO: if we know the prev_events of the incoming event we can avoid the
|
/// Resolves the state before the incoming event.
|
||||||
// request and build the state from a known point and resolve if > 1 prev_event
|
///
|
||||||
|
/// If we do not know enough information to resolve the state, `Ok(None)` is
|
||||||
|
/// returned, and the caller will have to figure it out some other way (e.g.
|
||||||
|
/// by fetching the state from a remote server).
|
||||||
|
pub(super) async fn state_before_incoming<Pdu>(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &Pdu,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
) -> Result<Option<HashMap<u64, OwnedEventId>>>
|
||||||
|
where
|
||||||
|
Pdu: Event + Send + Sync,
|
||||||
|
{
|
||||||
|
if incoming_pdu.prev_events().count() == 1 {
|
||||||
|
self.state_before_incoming_degree_one(incoming_pdu).await
|
||||||
|
} else {
|
||||||
|
self.state_before_incoming_resolved(incoming_pdu, room_version_rules)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determines the state before the incoming pdu, when it has only one prev
|
||||||
|
/// event. This is a special case that does not require state resolution.
|
||||||
#[tracing::instrument(name = "state", level = "debug", skip_all)]
|
#[tracing::instrument(name = "state", level = "debug", skip_all)]
|
||||||
pub(super) async fn state_at_incoming_degree_one<Pdu>(
|
async fn state_before_incoming_degree_one<Pdu>(
|
||||||
&self,
|
&self,
|
||||||
incoming_pdu: &Pdu,
|
incoming_pdu: &Pdu,
|
||||||
) -> Result<Option<HashMap<u64, OwnedEventId>>>
|
) -> Result<Option<HashMap<u64, OwnedEventId>>>
|
||||||
@@ -66,7 +87,7 @@ pub(super) async fn state_at_incoming_degree_one<Pdu>(
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
state.insert(shortstatekey, prev_event.to_owned());
|
state.insert(shortstatekey, prev_event.to_owned());
|
||||||
// Now it's the state after the pdu
|
// Now it's the state at the pdu
|
||||||
}
|
}
|
||||||
|
|
||||||
debug_assert!(!state.is_empty(), "should be returning None for empty HashMap result");
|
debug_assert!(!state.is_empty(), "should be returning None for empty HashMap result");
|
||||||
@@ -74,11 +95,14 @@ pub(super) async fn state_at_incoming_degree_one<Pdu>(
|
|||||||
Ok(Some(state))
|
Ok(Some(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolves the state before the incoming pdu across all of its prev
|
||||||
|
/// events. If we do not know enough information to resolve the state,
|
||||||
|
/// `Ok(None)` is returned, and the caller will have to figure it out some
|
||||||
|
/// other way (e.g. by fetching the state from a remote server).
|
||||||
#[tracing::instrument(name = "state", level = "debug", skip_all)]
|
#[tracing::instrument(name = "state", level = "debug", skip_all)]
|
||||||
pub(super) async fn state_at_incoming_resolved<Pdu>(
|
async fn state_before_incoming_resolved<Pdu>(
|
||||||
&self,
|
&self,
|
||||||
incoming_pdu: &Pdu,
|
incoming_pdu: &Pdu,
|
||||||
room_id: &RoomId,
|
|
||||||
room_version_rules: &RoomVersionRules,
|
room_version_rules: &RoomVersionRules,
|
||||||
) -> Result<Option<HashMap<u64, OwnedEventId>>>
|
) -> Result<Option<HashMap<u64, OwnedEventId>>>
|
||||||
where
|
where
|
||||||
@@ -108,6 +132,7 @@ pub(super) async fn state_at_incoming_resolved<Pdu>(
|
|||||||
};
|
};
|
||||||
|
|
||||||
trace!("Calculating fork states...");
|
trace!("Calculating fork states...");
|
||||||
|
let room_id = &incoming_pdu.room_id_or_hash();
|
||||||
let (fork_states, auth_chain_sets): (Vec<StateMap<_>>, Vec<HashSet<_>>) =
|
let (fork_states, auth_chain_sets): (Vec<StateMap<_>>, Vec<HashSet<_>>) =
|
||||||
extremity_sstatehashes
|
extremity_sstatehashes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -145,6 +170,8 @@ pub(super) async fn state_at_incoming_resolved<Pdu>(
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Determines the state at an incoming fork (aka the state at a prev
|
||||||
|
/// event), returning the resolved state and its associated auth chain.
|
||||||
async fn state_at_incoming_fork<Pdu>(
|
async fn state_at_incoming_fork<Pdu>(
|
||||||
&self,
|
&self,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
|
|||||||
@@ -1,27 +1,15 @@
|
|||||||
use std::{borrow::Borrow, sync::Arc, time::Instant};
|
use std::time::Instant;
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
Err, Result, debug, debug_error, debug_info, err, info, is_equal_to,
|
Err, Result, debug, debug_info, debug_warn, is_true,
|
||||||
matrix::{Event, EventTypeExt, PduEvent, StateKey, state_res},
|
matrix::{Event, PduEvent},
|
||||||
result::DebugInspect,
|
|
||||||
trace,
|
trace,
|
||||||
utils::{
|
|
||||||
IterStream,
|
|
||||||
stream::{BroadbandExt, ReadyExt},
|
|
||||||
},
|
|
||||||
warn,
|
|
||||||
};
|
|
||||||
use futures::{FutureExt, StreamExt, future::ready};
|
|
||||||
use ruma::{
|
|
||||||
CanonicalJsonObject, RoomId, ServerName, api::error::ErrorKind, events::StateEventType,
|
|
||||||
};
|
};
|
||||||
|
use ruma::{CanonicalJsonObject, RoomId, ServerName, events::StateEventType};
|
||||||
use tokio::join;
|
use tokio::join;
|
||||||
|
|
||||||
use super::get_room_version_rules;
|
use super::get_room_version_rules;
|
||||||
use crate::rooms::{
|
use crate::rooms::timeline::RawPduId;
|
||||||
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
|
||||||
timeline::RawPduId,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
#[tracing::instrument(name="upgrade_outlier", skip_all, fields(event_id=%incoming_pdu.event_id()))]
|
#[tracing::instrument(name="upgrade_outlier", skip_all, fields(event_id=%incoming_pdu.event_id()))]
|
||||||
@@ -46,16 +34,24 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu(
|
|||||||
trace!(event_id=%incoming_pdu.event_id(), "Skipping upgrade of already upgraded PDU");
|
trace!(event_id=%incoming_pdu.event_id(), "Skipping upgrade of already upgraded PDU");
|
||||||
return Ok(Some(id));
|
return Ok(Some(id));
|
||||||
} else if rejected {
|
} else if rejected {
|
||||||
return Err!(Request(Forbidden("Event has been rejected")));
|
return Err!(Request(Forbidden(debug_info!("Event has been rejected"))));
|
||||||
} else if soft_failed {
|
} else if soft_failed {
|
||||||
return Err!(Request(Forbidden("Event has been soft-failed")));
|
// Soft-failed events cannot be promoted.
|
||||||
|
return Err!(Request(Forbidden(debug_info!("Event has been soft-failed"))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// These should never happen, but they're good last-minute sanity checks to
|
||||||
|
// ensure we never promote totally illegal events.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
*create_event.kind(),
|
*create_event.kind(),
|
||||||
StateEventType::RoomCreate.into(),
|
StateEventType::RoomCreate.into(),
|
||||||
"tried to upgrade a PDU with a create_event that is not a room create event"
|
"tried to upgrade a PDU with a create_event that is not a room create event"
|
||||||
);
|
);
|
||||||
|
assert_eq!(
|
||||||
|
incoming_pdu.room_id_or_hash(),
|
||||||
|
*room_id,
|
||||||
|
"room ID mismatch: PDU room ID differs from parameter"
|
||||||
|
);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
event_id = %incoming_pdu.event_id,
|
event_id = %incoming_pdu.event_id,
|
||||||
@@ -65,326 +61,118 @@ pub(super) async fn upgrade_outlier_to_timeline_pdu(
|
|||||||
let min_depth = self.services.metadata.get_mindepth(room_id).await;
|
let min_depth = self.services.metadata.get_mindepth(room_id).await;
|
||||||
let room_version_rules = get_room_version_rules(create_event)?;
|
let room_version_rules = get_room_version_rules(create_event)?;
|
||||||
|
|
||||||
// 10. Fetch missing state and auth chain events by calling /state_ids at
|
// We now need to resolve the state before the event so that we can perform PDU
|
||||||
// backwards extremities doing all the checks in this list starting at 1.
|
// check 5 (event auth passes based on state before the event). To do this, we
|
||||||
// These are not timeline events.
|
// either need to have all the prev events locally, or ask a remote server
|
||||||
|
// for the state at the event.
|
||||||
debug!(
|
let (passes_state_before, state_before) = self
|
||||||
event_id = %incoming_pdu.event_id,
|
.state_before_check_5(&incoming_pdu, &room_version_rules, create_event, origin)
|
||||||
"Resolving state at event"
|
|
||||||
);
|
|
||||||
let state_at_incoming_event = if incoming_pdu.prev_events().count() == 1 {
|
|
||||||
self.state_at_incoming_degree_one(&incoming_pdu).await?
|
|
||||||
} else {
|
|
||||||
self.state_at_incoming_resolved(&incoming_pdu, room_id, &room_version_rules)
|
|
||||||
.await?
|
|
||||||
};
|
|
||||||
let state_at_incoming_event = match state_at_incoming_event {
|
|
||||||
| Some(s) => s,
|
|
||||||
| None => {
|
|
||||||
trace!("Could not calculate incoming state, asking remote {origin} for it");
|
|
||||||
self.fetch_state(origin, create_event, room_id, incoming_pdu.event_id())
|
|
||||||
.await
|
|
||||||
.debug_inspect_err(|e| {
|
|
||||||
debug_error!("Could not fetch state from {origin}: {e}");
|
|
||||||
})?
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
if state_at_incoming_event.is_empty()
|
|
||||||
&& *incoming_pdu.event_type() != StateEventType::RoomCreate.into()
|
|
||||||
{
|
|
||||||
// This can happen if the remote sends an event but cannot be reached to fetch
|
|
||||||
// the state at it, and all other servers in the room (which might just be the
|
|
||||||
// unreachable server) are unable to provide required info.
|
|
||||||
// returning an error here allows the upgrade to be attempted at another time.
|
|
||||||
return Err!(Request(Forbidden("Could not resolve incoming state at event")));
|
|
||||||
}
|
|
||||||
trace!(state_events = state_at_incoming_event.len(), "Calculated incoming state");
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Performing auth check to upgrade"
|
|
||||||
);
|
|
||||||
// 11. Check the auth of the event passes based on the state of the event
|
|
||||||
let state_fetch_state = &state_at_incoming_event;
|
|
||||||
let state_fetch = |k: StateEventType, s: StateKey| async move {
|
|
||||||
let shortstatekey = self.services.short.get_shortstatekey(&k, &s).await.ok()?;
|
|
||||||
|
|
||||||
let event_id = state_fetch_state.get(&shortstatekey)?;
|
|
||||||
self.services.timeline.get_pdu(event_id).await.ok()
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Running initial auth check"
|
|
||||||
);
|
|
||||||
// PDU check: 5
|
|
||||||
let auth_check = state_res::event_auth::auth_check(
|
|
||||||
&room_version_rules,
|
|
||||||
&incoming_pdu,
|
|
||||||
None, // TODO: third party invite
|
|
||||||
|ty, sk| state_fetch(ty.clone(), sk.into()),
|
|
||||||
create_event.as_pdu(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
|
|
||||||
|
|
||||||
if !auth_check {
|
|
||||||
self.services
|
|
||||||
.pdu_metadata
|
|
||||||
.mark_event_rejected(incoming_pdu.event_id());
|
|
||||||
return Err!(Request(Forbidden(
|
|
||||||
"Event authorisation fails based on the state before the event"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
|
|
||||||
debug!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Gathering auth events"
|
|
||||||
);
|
|
||||||
let auth_events = self
|
|
||||||
.services
|
|
||||||
.state
|
|
||||||
.get_auth_events(
|
|
||||||
room_id,
|
|
||||||
incoming_pdu.kind(),
|
|
||||||
incoming_pdu.sender(),
|
|
||||||
incoming_pdu.state_key(),
|
|
||||||
incoming_pdu.content(),
|
|
||||||
&room_version_rules,
|
|
||||||
)
|
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let state_fetch = |k: &StateEventType, s: &str| {
|
if !passes_state_before {
|
||||||
let key = k.with_state_key(s);
|
self.reject_and_persist(incoming_pdu.event_id(), &val);
|
||||||
ready(auth_events.get(&key).map(ToOwned::to_owned))
|
return Err!(Request(Forbidden(debug_warn!(
|
||||||
};
|
"Event authorisation fails based on the state before the event"
|
||||||
|
))));
|
||||||
debug!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Running auth check with claimed state auth"
|
|
||||||
);
|
|
||||||
// PDU check: 6
|
|
||||||
let auth_check = state_res::event_auth::auth_check(
|
|
||||||
&room_version_rules,
|
|
||||||
&incoming_pdu,
|
|
||||||
None, // third-party invite
|
|
||||||
state_fetch,
|
|
||||||
create_event.as_pdu(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(|e| err!(Request(Forbidden("Auth check failed: {e:?}"))))?;
|
|
||||||
if !auth_check {
|
|
||||||
warn!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Event authorization fails based on the current state of the room"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Soft fail check before doing state res
|
// Now that we know the event passes both self-authentication, and
|
||||||
debug!(
|
// authentication based on the state before the event, we need to check that it
|
||||||
event_id = %incoming_pdu.event_id,
|
// passes based on the *current* room state (state across all forward
|
||||||
"Performing soft-fail check"
|
// extremities). If it doesn't, we accept it, but soft-fail it, and this
|
||||||
);
|
// prevents it being promoted.
|
||||||
let mut soft_fail = match (auth_check, incoming_pdu.redacts_id(&room_version_rules)) {
|
|
||||||
| (false, _) => true,
|
|
||||||
| (true, None) => false,
|
|
||||||
| (true, Some(redact_id)) => {
|
|
||||||
if !self
|
|
||||||
.services
|
|
||||||
.state_accessor
|
|
||||||
.user_can_redact(&redact_id, incoming_pdu.sender(), room_id, true)
|
|
||||||
.await?
|
|
||||||
{
|
|
||||||
warn!(redacts = %redact_id, "User is not allowed to redact event");
|
|
||||||
true
|
|
||||||
} else {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// 13. Use state resolution to find new room state
|
// We lock the room here to prevent the current state from changing beneath us
|
||||||
// We start looking at current room state now, so lets lock the room
|
// mid-check.
|
||||||
trace!(
|
trace!(
|
||||||
room_id = %room_id,
|
room_id = %room_id,
|
||||||
"Locking the room"
|
"Locking the room"
|
||||||
);
|
);
|
||||||
let state_lock = self.services.state.mutex.lock(room_id).await;
|
let state_lock = self.services.state.mutex.lock(room_id).await;
|
||||||
|
let passes_current_state = self
|
||||||
let state_ids_compressed: Arc<CompressedState> = self
|
.current_state_check_6(&incoming_pdu, &room_version_rules, create_event)
|
||||||
.services
|
.await
|
||||||
.state_compressor
|
.inspect(|passes| {
|
||||||
.compress_state_events(
|
if !*passes {
|
||||||
state_at_incoming_event
|
debug_warn!(
|
||||||
.iter()
|
"Event authorisation fails based on the current room state - will be \
|
||||||
.map(|(ssk, eid)| (ssk, eid.borrow())),
|
soft-failed"
|
||||||
)
|
|
||||||
.collect()
|
|
||||||
.map(Arc::new)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if incoming_pdu.state_key().is_some() {
|
|
||||||
debug!("Event is a state-event. Deriving new room state");
|
|
||||||
|
|
||||||
// We also add state after incoming event to the fork states
|
|
||||||
let mut state_after = state_at_incoming_event.clone();
|
|
||||||
if let Some(state_key) = incoming_pdu.state_key() {
|
|
||||||
let shortstatekey = self
|
|
||||||
.services
|
|
||||||
.short
|
|
||||||
.get_or_create_shortstatekey(
|
|
||||||
&incoming_pdu.kind().to_string().into(),
|
|
||||||
state_key,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let event_id = incoming_pdu.event_id();
|
|
||||||
state_after.insert(shortstatekey, event_id.to_owned());
|
|
||||||
}
|
|
||||||
|
|
||||||
let new_room_state = self
|
|
||||||
.resolve_state(room_id, &room_version_rules, state_after)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
// Set the new room state to the resolved state
|
|
||||||
debug!("Forcing new room state");
|
|
||||||
let HashSetCompressStateEvent { shortstatehash, added, removed } = self
|
|
||||||
.services
|
|
||||||
.state_compressor
|
|
||||||
.save_state(room_id, new_room_state)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
self.services
|
|
||||||
.state
|
|
||||||
.force_state(room_id, shortstatehash, added, removed, &state_lock)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !soft_fail {
|
|
||||||
// Don't call the below checks on events that have already soft-failed, there's
|
|
||||||
// no reason to re-calculate that.
|
|
||||||
// 14-pre. ask the policy server to sign the event, if possible
|
|
||||||
debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event");
|
|
||||||
let tmp_evt_id = val.remove("event_id");
|
|
||||||
if let Err(e) = self
|
|
||||||
.policy_server_allows_event(
|
|
||||||
&incoming_pdu,
|
|
||||||
&mut val,
|
|
||||||
room_id,
|
|
||||||
&room_version_rules,
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if matches!(e.kind(), ErrorKind::Forbidden) {
|
|
||||||
info!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
error = %e,
|
|
||||||
"Event has been marked as spam by policy server: {}",
|
|
||||||
e.message(),
|
|
||||||
);
|
);
|
||||||
soft_fail = true;
|
|
||||||
} else {
|
|
||||||
return Err(e);
|
|
||||||
}
|
}
|
||||||
} else {
|
})?;
|
||||||
debug!(
|
|
||||||
event_id = %incoming_pdu.event_id,
|
|
||||||
"Event has passed policy server check."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if let Some(id) = tmp_evt_id {
|
|
||||||
val.insert("event_id".to_owned(), id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Additionally, if this is a redaction for a soft-failed event, we soft-fail it
|
// Determine whether this PDU should be soft-failed.
|
||||||
// also.
|
// If the auth check failed, invariably yes. Otherwise, only if the user isn't
|
||||||
|
// allowed to redact the target event (if any).
|
||||||
|
let mut should_soft_fail =
|
||||||
|
match (passes_current_state, incoming_pdu.redacts_id(&room_version_rules)) {
|
||||||
|
| (false, _) => true,
|
||||||
|
| (true, None) => false,
|
||||||
|
| (true, Some(redact_id)) => self
|
||||||
|
.services
|
||||||
|
.state_accessor
|
||||||
|
.user_can_redact(&redact_id, incoming_pdu.sender(), room_id, true)
|
||||||
|
.await
|
||||||
|
.is_ok_and(is_true!()),
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: this is supposed to hide redactions from policy servers, however, for
|
if !should_soft_fail {
|
||||||
// full efficacy it also needs to hide redactions for unknown events. This
|
// Now we can perform check 7, which is ensuring the event passes policy server
|
||||||
// needs to be investigated at a later time.
|
// checks.
|
||||||
|
// We explicitly only do this if we aren't already going to soft-fail the event,
|
||||||
|
// since the policy server refusing this event also soft-fails it.
|
||||||
|
debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event");
|
||||||
|
should_soft_fail = !self
|
||||||
|
.policy_server_check_7(&incoming_pdu, &mut val, &room_version_rules)
|
||||||
|
.await
|
||||||
|
.inspect(|passes| {
|
||||||
|
if !*passes {
|
||||||
|
debug_warn!(
|
||||||
|
"Event did not pass the policy server check and will be soft-failed"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// TODO: this is supposed to hide redactions from policy servers and janitorial
|
||||||
|
// bots, however, for full efficacy it also needs to hide redactions for
|
||||||
|
// unknown events. This needs to be investigated at a later time.
|
||||||
if let Some(redact_id) = incoming_pdu.redacts_id(&room_version_rules) {
|
if let Some(redact_id) = incoming_pdu.redacts_id(&room_version_rules) {
|
||||||
debug!(
|
debug!(
|
||||||
redact_id = %redact_id,
|
redact_id = %redact_id,
|
||||||
"Checking if redaction is for a soft-failed event"
|
"Checking if redaction is for a soft-failed/rejected event"
|
||||||
);
|
);
|
||||||
if self
|
if !self
|
||||||
.services
|
.services
|
||||||
.pdu_metadata
|
.pdu_metadata
|
||||||
.is_event_soft_failed(&redact_id)
|
.is_event_accepted(&redact_id)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
info!(
|
debug_info!(
|
||||||
redact_id = %redact_id,
|
"Soft-failing valid redaction because it targets a non-accepted event"
|
||||||
"Redaction is for a soft-failed event"
|
|
||||||
);
|
);
|
||||||
soft_fail = true;
|
should_soft_fail = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The PDU has now passed all checks! We can now promote it (or soft-fail it if
|
||||||
|
// the verdict is such).
|
||||||
trace!("Appending pdu to timeline");
|
trace!("Appending pdu to timeline");
|
||||||
let mut extremities: Vec<_> = self
|
|
||||||
.services
|
|
||||||
.state
|
|
||||||
.get_forward_extremities(room_id)
|
|
||||||
.collect()
|
|
||||||
.await;
|
|
||||||
if !soft_fail {
|
|
||||||
// Per https://spec.matrix.org/unstable/server-server-api/#soft-failure, soft-failed events
|
|
||||||
// are not added as forward extremities.
|
|
||||||
|
|
||||||
// Now we calculate the set of extremities this room has after the incoming
|
|
||||||
// event has been applied. We start with the previous extremities (aka leaves)
|
|
||||||
trace!("Calculating extremities");
|
|
||||||
extremities = extremities
|
|
||||||
.into_iter()
|
|
||||||
.stream()
|
|
||||||
.ready_filter(|event_id| {
|
|
||||||
// Remove any that are referenced by this incoming event's prev_events
|
|
||||||
!incoming_pdu.prev_events().any(is_equal_to!(event_id))
|
|
||||||
})
|
|
||||||
.broad_filter_map(|event_id| async move {
|
|
||||||
// Only keep those extremities were not referenced yet
|
|
||||||
self.services
|
|
||||||
.pdu_metadata
|
|
||||||
.is_event_referenced(room_id, &event_id)
|
|
||||||
.await
|
|
||||||
.eq(&false)
|
|
||||||
.then_some(event_id)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.await;
|
|
||||||
extremities.push(incoming_pdu.event_id().to_owned());
|
|
||||||
debug!(
|
|
||||||
"Retained {} extremities checked against {} prev_events",
|
|
||||||
extremities.len(),
|
|
||||||
incoming_pdu.prev_events().count()
|
|
||||||
);
|
|
||||||
assert!(!extremities.is_empty(), "extremities must not empty");
|
|
||||||
}
|
|
||||||
|
|
||||||
let pdu_id = self
|
let pdu_id = self
|
||||||
.services
|
.services
|
||||||
.timeline
|
.timeline
|
||||||
.append_incoming_pdu(
|
.append_incoming_pdu(
|
||||||
&incoming_pdu,
|
&incoming_pdu,
|
||||||
val,
|
val,
|
||||||
extremities.iter().map(Borrow::borrow),
|
&room_version_rules,
|
||||||
state_ids_compressed,
|
state_before,
|
||||||
soft_fail,
|
should_soft_fail,
|
||||||
&state_lock,
|
&state_lock,
|
||||||
room_id,
|
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
if soft_fail {
|
|
||||||
self.services
|
|
||||||
.pdu_metadata
|
|
||||||
.mark_event_soft_failed(incoming_pdu.event_id());
|
|
||||||
|
|
||||||
info!(
|
if should_soft_fail {
|
||||||
|
debug_info!(
|
||||||
elapsed = ?timer.elapsed(),
|
elapsed = ?timer.elapsed(),
|
||||||
event_id = %incoming_pdu.event_id,
|
event_id = %incoming_pdu.event_id,
|
||||||
"Event was soft failed"
|
"Event was soft failed"
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ pub async fn join_remote_room(
|
|||||||
return state;
|
return state;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if !pdu_fits(&mut value.clone()) {
|
if !pdu_fits(&value.clone()) {
|
||||||
warn!(
|
warn!(
|
||||||
"dropping incoming PDU {event_id} in room {room_id} from room join \
|
"dropping incoming PDU {event_id} in room {room_id} from room join \
|
||||||
because it exceeds 65535 bytes or is otherwise too large."
|
because it exceeds 65535 bytes or is otherwise too large."
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::{collections::HashMap, fmt::Write, sync::Arc};
|
use std::{collections::HashMap, fmt::Write, sync::Arc};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::debug;
|
use conduwuit::{debug, utils::stream::WidebandExt};
|
||||||
use conduwuit_core::{
|
use conduwuit_core::{
|
||||||
Event, PduEvent, Result, err,
|
Event, PduEvent, Result, err,
|
||||||
result::FlatOk,
|
result::FlatOk,
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
short::{ShortEventId, ShortStateHash},
|
short::{ShortEventId, ShortStateHash},
|
||||||
state_compressor::{CompressedState, parse_compressed_state_event},
|
state_compressor::{CompressedState, parse_compressed_state_event},
|
||||||
},
|
},
|
||||||
sync,
|
sending, sync,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
@@ -45,6 +45,7 @@ struct Services {
|
|||||||
state_cache: Dep<rooms::state_cache::Service>,
|
state_cache: Dep<rooms::state_cache::Service>,
|
||||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||||
state_compressor: Dep<rooms::state_compressor::Service>,
|
state_compressor: Dep<rooms::state_compressor::Service>,
|
||||||
|
sending: Dep<sending::Service>,
|
||||||
sync: Dep<sync::Service>,
|
sync: Dep<sync::Service>,
|
||||||
timeline: Dep<rooms::timeline::Service>,
|
timeline: Dep<rooms::timeline::Service>,
|
||||||
}
|
}
|
||||||
@@ -71,6 +72,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||||
state_compressor: args
|
state_compressor: args
|
||||||
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
|
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
|
||||||
|
sending: args.depend::<sending::Service>("sending"),
|
||||||
sync: args.depend::<sync::Service>("sync"),
|
sync: args.depend::<sync::Service>("sync"),
|
||||||
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
|
||||||
},
|
},
|
||||||
@@ -307,6 +309,7 @@ pub async fn summary_stripped(
|
|||||||
event: &PduEvent,
|
event: &PduEvent,
|
||||||
room_id: &RoomId,
|
room_id: &RoomId,
|
||||||
target_user: &UserId,
|
target_user: &UserId,
|
||||||
|
federation: bool,
|
||||||
) -> Vec<RawStrippedState> {
|
) -> Vec<RawStrippedState> {
|
||||||
let mut state_events = [
|
let mut state_events = [
|
||||||
(&StateEventType::RoomCreate, ""),
|
(&StateEventType::RoomCreate, ""),
|
||||||
@@ -343,8 +346,20 @@ pub async fn summary_stripped(
|
|||||||
.await
|
.await
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(Result::ok)
|
.filter_map(Result::ok)
|
||||||
.map(|pdu| RawStrippedState::Pdu(serde_json::value::to_raw_value(&pdu).unwrap()))
|
.stream()
|
||||||
|
.wide_then(async |pdu| {
|
||||||
|
let formatted = if federation {
|
||||||
|
self.services
|
||||||
|
.sending
|
||||||
|
.convert_to_outgoing_federation_event(pdu.to_canonical_object())
|
||||||
|
.await
|
||||||
|
} else {
|
||||||
|
serde_json::value::to_raw_value(&pdu).unwrap()
|
||||||
|
};
|
||||||
|
RawStrippedState::Pdu(formatted)
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the state hash to a new version, but does not update state_cache.
|
/// Set the state hash to a new version, but does not update state_cache.
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ pub async fn update_membership(
|
|||||||
let invite_state = if is_local {
|
let invite_state = if is_local {
|
||||||
self.services
|
self.services
|
||||||
.state
|
.state
|
||||||
.summary_stripped(pdu, room_id, user_id)
|
.summary_stripped(pdu, room_id, user_id, false)
|
||||||
.await
|
.await
|
||||||
} else {
|
} else {
|
||||||
vec![]
|
vec![]
|
||||||
|
|||||||
@@ -1,69 +1,113 @@
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashSet},
|
borrow::Borrow,
|
||||||
|
collections::{BTreeMap, HashMap, HashSet},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
|
|
||||||
use conduwuit::{
|
use conduwuit::{
|
||||||
debug_warn,
|
Result, debug, debug_warn, is_equal_to,
|
||||||
|
matrix::{Event, PduEvent},
|
||||||
pdu::{Count, ShortRoomId},
|
pdu::{Count, ShortRoomId},
|
||||||
trace,
|
trace,
|
||||||
utils::{IterStream, TryFutureExtExt, stream::BroadbandExt},
|
utils::{
|
||||||
|
IterStream, TryFutureExtExt,
|
||||||
|
mutex_map::Guard,
|
||||||
|
stream::{BroadbandExt, ReadyExt},
|
||||||
|
},
|
||||||
warn,
|
warn,
|
||||||
};
|
};
|
||||||
use conduwuit_core::{
|
use conduwuit_core::{
|
||||||
Result, err, error,
|
err, error,
|
||||||
matrix::{
|
matrix::pdu::{PduCount, PduId, RawPduId},
|
||||||
event::Event,
|
utils::{self},
|
||||||
pdu::{PduCount, PduEvent, PduId, RawPduId},
|
|
||||||
},
|
|
||||||
utils::{self, ReadyExt},
|
|
||||||
};
|
};
|
||||||
use futures::{StreamExt, TryFutureExt};
|
use futures::{FutureExt, StreamExt, TryFutureExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, RoomVersionId, UserId,
|
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, RoomId,
|
||||||
|
RoomVersionId, UserId,
|
||||||
events::{
|
events::{
|
||||||
GlobalAccountDataEventType, TimelineEventType,
|
GlobalAccountDataEventType, TimelineEventType,
|
||||||
push_rules::PushRulesEvent,
|
push_rules::PushRulesEvent,
|
||||||
room::{encrypted::Relation, redaction::RoomRedactionEventContent},
|
room::{encrypted::Relation, redaction::RoomRedactionEventContent},
|
||||||
},
|
},
|
||||||
push::{Action, Ruleset, Tweak},
|
push::{Action, Ruleset, Tweak},
|
||||||
|
room_version_rules::RoomVersionRules,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{ExtractBody, ExtractRelatesTo, ExtractRelatesToEventId, RoomMutexGuard};
|
use super::{ExtractBody, ExtractRelatesTo, ExtractRelatesToEventId, RoomMutexGuard};
|
||||||
use crate::{appservice::RegistrationInfo, rooms::state_compressor::CompressedState};
|
use crate::{
|
||||||
|
appservice::RegistrationInfo,
|
||||||
|
rooms::state_compressor::{CompressedState, HashSetCompressStateEvent},
|
||||||
|
};
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Append the incoming event setting the state snapshot to the state from
|
/// Append the incoming event setting the state snapshot to the state from
|
||||||
/// the server that sent the event.
|
/// the server that sent the event.
|
||||||
#[allow(clippy::too_many_arguments)]
|
pub async fn append_incoming_pdu<'a>(
|
||||||
pub async fn append_incoming_pdu<'a, Leaves>(
|
|
||||||
&'a self,
|
&'a self,
|
||||||
pdu: &'a PduEvent,
|
pdu: &'a PduEvent,
|
||||||
pdu_json: CanonicalJsonObject,
|
pdu_json: CanonicalJsonObject,
|
||||||
new_room_leaves: Leaves,
|
room_version_rules: &RoomVersionRules,
|
||||||
state_ids_compressed: Arc<CompressedState>,
|
state_before: HashMap<u64, OwnedEventId>,
|
||||||
soft_fail: bool,
|
should_soft_fail: bool,
|
||||||
state_lock: &'a RoomMutexGuard,
|
state_lock: &'a RoomMutexGuard,
|
||||||
room_id: &'a RoomId,
|
) -> Result<Option<RawPduId>> {
|
||||||
) -> Result<Option<RawPduId>>
|
let room_id = pdu.room_id_or_hash();
|
||||||
where
|
|
||||||
Leaves: Iterator<Item = &'a EventId> + Send + 'a,
|
let sb = state_before.iter().map(|(ssk, eid)| (ssk, eid.as_ref()));
|
||||||
{
|
let state_ids_compressed: Arc<CompressedState> = self
|
||||||
|
.services
|
||||||
|
.state_compressor
|
||||||
|
.compress_state_events(sb)
|
||||||
|
.collect()
|
||||||
|
.map(Arc::new)
|
||||||
|
.await;
|
||||||
|
|
||||||
// We append to state before appending the pdu, so we don't have a moment in
|
// We append to state before appending the pdu, so we don't have a moment in
|
||||||
// time with the pdu without it's state. This is okay because append_pdu can't
|
// time with the pdu without it's state. This is okay because append_pdu can't
|
||||||
// fail.
|
// fail.
|
||||||
self.services
|
self.services
|
||||||
.state
|
.state
|
||||||
.set_event_state(&pdu.event_id, room_id, state_ids_compressed)
|
.set_event_state(&pdu.event_id, &room_id, state_ids_compressed)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if soft_fail {
|
if should_soft_fail {
|
||||||
// Nothing else to do with a soft-failed event.
|
// Nothing else to do with a soft-failed event.
|
||||||
|
self.services
|
||||||
|
.pdu_metadata
|
||||||
|
.mark_event_soft_failed(pdu.event_id());
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per https://spec.matrix.org/unstable/server-server-api/#soft-failure, soft-failed events
|
||||||
|
// are not added as forward extremities.
|
||||||
|
// This also means we set the state here.
|
||||||
|
// We do this BEFORE setting the extremities so that there's never a point in
|
||||||
|
// time where we have fresh extremities referencing stale state.
|
||||||
|
let mut extremities: Vec<_> = self
|
||||||
|
.services
|
||||||
|
.state
|
||||||
|
.get_forward_extremities(&room_id)
|
||||||
|
.collect()
|
||||||
|
.await;
|
||||||
|
extremities = self
|
||||||
|
.progress_state_and_extremities(
|
||||||
|
pdu,
|
||||||
|
room_version_rules,
|
||||||
|
state_before.clone(),
|
||||||
|
extremities,
|
||||||
|
state_lock,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
let pdu_id = self
|
let pdu_id = self
|
||||||
.append_pdu(pdu, pdu_json, new_room_leaves, state_lock, room_id)
|
.append_pdu(
|
||||||
|
pdu,
|
||||||
|
pdu_json,
|
||||||
|
extremities.iter().map(Borrow::borrow),
|
||||||
|
state_lock,
|
||||||
|
&room_id,
|
||||||
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
// Process admin commands for federation events
|
// Process admin commands for federation events
|
||||||
@@ -86,11 +130,110 @@ pub async fn append_incoming_pdu<'a, Leaves>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.services.sync.wake_all_joined(room_id).await;
|
self.services.sync.wake_all_joined(&room_id).await;
|
||||||
|
|
||||||
Ok(Some(pdu_id))
|
Ok(Some(pdu_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derives new room state from the incoming event and filters forward
|
||||||
|
/// extremities accordingly. Does not set forward extremities.
|
||||||
|
///
|
||||||
|
/// Only call this function if the incoming PDU is not soft-failed or
|
||||||
|
/// rejected.
|
||||||
|
async fn progress_state_and_extremities(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
state_before: HashMap<u64, OwnedEventId>,
|
||||||
|
forward_extremities: Vec<OwnedEventId>,
|
||||||
|
state_lock: &Guard<OwnedRoomId, ()>,
|
||||||
|
) -> Result<Vec<OwnedEventId>> {
|
||||||
|
if incoming_pdu.state_key().is_some() {
|
||||||
|
debug!("Event is a state-event. Deriving new room state");
|
||||||
|
self.derive_new_state(incoming_pdu, room_version_rules, state_before, state_lock)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we calculate the set of extremities this room has after the incoming
|
||||||
|
// event has been applied. We start with the previous extremities
|
||||||
|
trace!("Calculating extremities");
|
||||||
|
let mut forward_extremities = forward_extremities
|
||||||
|
.into_iter()
|
||||||
|
.stream()
|
||||||
|
.ready_filter(|event_id| {
|
||||||
|
// Remove any that are referenced by this incoming event's prev_events
|
||||||
|
!incoming_pdu.prev_events().any(is_equal_to!(event_id))
|
||||||
|
})
|
||||||
|
.broad_filter_map(|event_id| async move {
|
||||||
|
// Only keep those extremities were not referenced yet
|
||||||
|
self.services
|
||||||
|
.pdu_metadata
|
||||||
|
.is_event_referenced(&incoming_pdu.room_id_or_hash(), &event_id)
|
||||||
|
.await
|
||||||
|
.eq(&false)
|
||||||
|
.then_some(event_id)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.await;
|
||||||
|
forward_extremities.push(incoming_pdu.event_id().to_owned());
|
||||||
|
debug!(
|
||||||
|
"Retained {} extremities checked against {} prev_events",
|
||||||
|
forward_extremities.len(),
|
||||||
|
incoming_pdu.prev_events().count()
|
||||||
|
);
|
||||||
|
assert!(!forward_extremities.is_empty(), "resolved extremities cannot be empty");
|
||||||
|
Ok(forward_extremities)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Derives a new room state by adding the incoming PDU to the state before
|
||||||
|
/// it to create the state at, which then becomes the current room state.
|
||||||
|
///
|
||||||
|
/// The caller MUST ensure forward extremities are set appropriately,
|
||||||
|
/// including this incoming pdu, either before or after calling this
|
||||||
|
/// function. Failing to do so will result in an inconsistent current state
|
||||||
|
/// cache, which may affect event authentication.
|
||||||
|
async fn derive_new_state(
|
||||||
|
&self,
|
||||||
|
incoming_pdu: &PduEvent,
|
||||||
|
room_version_rules: &RoomVersionRules,
|
||||||
|
state_before: HashMap<u64, OwnedEventId>,
|
||||||
|
state_lock: &Guard<OwnedRoomId, ()>,
|
||||||
|
) -> Result {
|
||||||
|
let room_id = incoming_pdu.room_id_or_hash();
|
||||||
|
// We also add state after incoming event to the fork states
|
||||||
|
let mut state_at_incoming_event = state_before;
|
||||||
|
let shortstatekey = self
|
||||||
|
.services
|
||||||
|
.short
|
||||||
|
.get_or_create_shortstatekey(
|
||||||
|
&incoming_pdu.kind().to_string().into(),
|
||||||
|
incoming_pdu.state_key().unwrap(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let event_id = incoming_pdu.event_id();
|
||||||
|
state_at_incoming_event.insert(shortstatekey, event_id.to_owned());
|
||||||
|
|
||||||
|
debug!("Resolving new room state");
|
||||||
|
let new_room_state = self
|
||||||
|
.services
|
||||||
|
.event_handler
|
||||||
|
.resolve_state(&room_id, room_version_rules, state_at_incoming_event)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
debug!("Forcing new room state");
|
||||||
|
let HashSetCompressStateEvent { shortstatehash, added, removed } = self
|
||||||
|
.services
|
||||||
|
.state_compressor
|
||||||
|
.save_state(&room_id, new_room_state)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
self.services
|
||||||
|
.state
|
||||||
|
.force_state(&room_id, shortstatehash, added, removed, state_lock)
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Populates the unsigned data of a PDU
|
/// Populates the unsigned data of a PDU
|
||||||
async fn populate_unsigned(&self, pdu: &PduEvent, pdu_json: &mut CanonicalJsonObject) {
|
async fn populate_unsigned(&self, pdu: &PduEvent, pdu_json: &mut CanonicalJsonObject) {
|
||||||
let Some(state_key) = pdu.state_key() else {
|
let Some(state_key) = pdu.state_key() else {
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ pub async fn get_remote_pdu(&self, room_id: &RoomId, event_id: &EventId) -> Resu
|
|||||||
| Ok(value) => {
|
| Ok(value) => {
|
||||||
self.services
|
self.services
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(&backfill_server, room_id, event_id, value, false)
|
.handle_incoming_pdu(&backfill_server, room_id, event_id, value, true)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
debug!("Successfully backfilled {event_id} from {backfill_server}");
|
debug!("Successfully backfilled {event_id} from {backfill_server}");
|
||||||
@@ -166,8 +166,11 @@ pub async fn get_remote_pdu(&self, room_id: &RoomId, event_id: &EventId) -> Resu
|
|||||||
/// Backfills a single PDU.
|
/// Backfills a single PDU.
|
||||||
#[tracing::instrument(skip(self, pdu), level = "debug")]
|
#[tracing::instrument(skip(self, pdu), level = "debug")]
|
||||||
pub async fn backfill_pdu(&self, origin: &ServerName, pdu: Box<RawJsonValue>) -> Result<()> {
|
pub async fn backfill_pdu(&self, origin: &ServerName, pdu: Box<RawJsonValue>) -> Result<()> {
|
||||||
let (room_id, event_id, value) =
|
let (room_id, event_id, value) = self
|
||||||
self.services.event_handler.parse_incoming_pdu(&pdu).await?;
|
.services
|
||||||
|
.event_handler
|
||||||
|
.parse_incoming_pdu(&pdu, None)
|
||||||
|
.await?;
|
||||||
|
|
||||||
// Lock so we cannot backfill the same pdu twice at the same time
|
// Lock so we cannot backfill the same pdu twice at the same time
|
||||||
let mutex_lock = self
|
let mutex_lock = self
|
||||||
@@ -185,7 +188,7 @@ pub async fn backfill_pdu(&self, origin: &ServerName, pdu: Box<RawJsonValue>) ->
|
|||||||
|
|
||||||
self.services
|
self.services
|
||||||
.event_handler
|
.event_handler
|
||||||
.handle_incoming_pdu(origin, &room_id, &event_id, value, false)
|
.handle_incoming_pdu(origin, &room_id, &event_id, value, true)
|
||||||
.boxed()
|
.boxed()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@
|
|||||||
|
|
||||||
use super::RoomMutexGuard;
|
use super::RoomMutexGuard;
|
||||||
|
|
||||||
pub fn pdu_fits(owned_obj: &mut CanonicalJsonObject) -> bool {
|
/// Ensures the given PDU fits inside the size limits for a PDU.
|
||||||
|
#[must_use]
|
||||||
|
pub fn pdu_fits(owned_obj: &CanonicalJsonObject) -> bool {
|
||||||
// room IDs, event IDs, senders, types, and state keys must all be <= 255 bytes
|
// room IDs, event IDs, senders, types, and state keys must all be <= 255 bytes
|
||||||
if let Some(CanonicalJsonValue::String(room_id)) = owned_obj.get("room_id") {
|
if let Some(CanonicalJsonValue::String(room_id)) = owned_obj.get("room_id") {
|
||||||
if room_id.len() > 255 {
|
if room_id.len() > 255 {
|
||||||
@@ -306,12 +308,27 @@ pub async fn create_hash_and_sign_event(
|
|||||||
| _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))),
|
| _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Evil hack because only the PDU JSON gets signed
|
||||||
|
pdu.hashes = serde_json::from_value(serde_json::to_value(
|
||||||
|
pdu_json
|
||||||
|
.get("hashes")
|
||||||
|
.expect("must have hashes after signing")
|
||||||
|
.clone(),
|
||||||
|
)?)?;
|
||||||
|
pdu.signatures = serde_json::from_value(serde_json::to_value(
|
||||||
|
pdu_json
|
||||||
|
.get("signatures")
|
||||||
|
.expect("must have signatures after signing")
|
||||||
|
.clone(),
|
||||||
|
)?)?;
|
||||||
|
|
||||||
// Generate event id
|
// Generate event id
|
||||||
pdu.event_id = gen_event_id(&pdu_json, &room_version_rules)?;
|
pdu.event_id = gen_event_id(&pdu_json, &room_version_rules)?;
|
||||||
pdu_json
|
pdu_json
|
||||||
.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
|
.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
|
||||||
// Verify that the *full* PDU isn't over 64KiB.
|
// Verify that the *full* PDU isn't over 64KiB.
|
||||||
if !pdu_fits(&mut pdu_json.clone()) {
|
if !pdu_fits(&pdu_json.clone()) {
|
||||||
// feckin huge PDU mate
|
// feckin huge PDU mate
|
||||||
return Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)")));
|
return Err!(Request(TooLarge("Message/PDU is too long (exceeds 65535 bytes)")));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,6 +77,7 @@ struct Services {
|
|||||||
short: Dep<rooms::short::Service>,
|
short: Dep<rooms::short::Service>,
|
||||||
state: Dep<rooms::state::Service>,
|
state: Dep<rooms::state::Service>,
|
||||||
state_accessor: Dep<rooms::state_accessor::Service>,
|
state_accessor: Dep<rooms::state_accessor::Service>,
|
||||||
|
state_compressor: Dep<rooms::state_compressor::Service>,
|
||||||
state_cache: Dep<rooms::state_cache::Service>,
|
state_cache: Dep<rooms::state_cache::Service>,
|
||||||
sync: Dep<sync::Service>,
|
sync: Dep<sync::Service>,
|
||||||
threads: Dep<rooms::threads::Service>,
|
threads: Dep<rooms::threads::Service>,
|
||||||
@@ -110,6 +111,8 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
|||||||
state: args.depend::<rooms::state::Service>("rooms::state"),
|
state: args.depend::<rooms::state::Service>("rooms::state"),
|
||||||
state_accessor: args
|
state_accessor: args
|
||||||
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
|
||||||
|
state_compressor: args
|
||||||
|
.depend::<rooms::state_compressor::Service>("rooms::state_compressor"),
|
||||||
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
|
||||||
sync: args.depend::<sync::Service>("sync"),
|
sync: args.depend::<sync::Service>("sync"),
|
||||||
threads: args.depend::<rooms::threads::Service>("rooms::threads"),
|
threads: args.depend::<rooms::threads::Service>("rooms::threads"),
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) ->
|
|||||||
self.create_device(
|
self.create_device(
|
||||||
user_id,
|
user_id,
|
||||||
&request.device_id,
|
&request.device_id,
|
||||||
"",
|
|
||||||
None,
|
None,
|
||||||
request.initial_device_display_name.clone(),
|
request.initial_device_display_name.clone(),
|
||||||
None,
|
None,
|
||||||
|
|||||||
+38
-29
@@ -11,20 +11,44 @@
|
|||||||
use futures::{Stream, StreamExt};
|
use futures::{Stream, StreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
|
DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId,
|
||||||
api::client::device::Device, events::AnyToDeviceEvent, serde::Raw, uint,
|
api::client::device::Device, assign, events::AnyToDeviceEvent, serde::Raw, uint,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use crate::users::increment;
|
use crate::users::increment;
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct DeviceToken {
|
||||||
|
token: String,
|
||||||
|
max_age: Option<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceToken {
|
||||||
|
const DEVICE_ID_LENGTH: usize = 10;
|
||||||
|
|
||||||
|
pub fn new(token: String) -> Self { Self { token, max_age: None } }
|
||||||
|
|
||||||
|
pub fn new_random() -> Self { Self::new(utils::random_string(Self::DEVICE_ID_LENGTH)) }
|
||||||
|
|
||||||
|
pub fn with_max_age(self, max_age: Duration) -> Self {
|
||||||
|
assign!(self, { max_age: Some(max_age) })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn into_token(self) -> String { self.token }
|
||||||
|
}
|
||||||
|
|
||||||
impl super::Service {
|
impl super::Service {
|
||||||
/// Adds a new device to a user.
|
/// Adds a new device to a user.
|
||||||
|
///
|
||||||
|
/// If no `token` is provided, the device will not be able to be logged
|
||||||
|
/// into.
|
||||||
pub async fn create_device(
|
pub async fn create_device(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_id: &DeviceId,
|
device_id: &DeviceId,
|
||||||
token: &str,
|
token: Option<DeviceToken>,
|
||||||
token_max_age: Option<Duration>,
|
|
||||||
initial_device_display_name: Option<String>,
|
initial_device_display_name: Option<String>,
|
||||||
client_ip: Option<String>,
|
client_ip: Option<String>,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
@@ -38,8 +62,12 @@ pub async fn create_device(
|
|||||||
|
|
||||||
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
||||||
self.db.userdeviceid_metadata.put(key, Json(device));
|
self.db.userdeviceid_metadata.put(key, Json(device));
|
||||||
self.set_token(user_id, device_id, token, token_max_age)
|
|
||||||
.await
|
if let Some(token) = token {
|
||||||
|
self.set_token(user_id, device_id, token).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a device from a user.
|
/// Removes a device from a user.
|
||||||
@@ -97,31 +125,12 @@ pub async fn get_token(&self, user_id: &UserId, device_id: &DeviceId) -> Result<
|
|||||||
self.db.userdeviceid_token.qry(&key).await.deserialized()
|
self.db.userdeviceid_token.qry(&key).await.deserialized()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate a unique access token that doesn't collide with existing tokens
|
|
||||||
pub async fn generate_unique_token(&self) -> String {
|
|
||||||
loop {
|
|
||||||
let token = utils::random_string(32);
|
|
||||||
|
|
||||||
// Check for collision with existing appservice and user tokens
|
|
||||||
let (appservice, usr) = tokio::join!(
|
|
||||||
self.services.appservice.find_from_token(&token),
|
|
||||||
self.db.token_userdeviceid.get(&token)
|
|
||||||
);
|
|
||||||
if appservice.is_ok() || usr.is_ok() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replaces the access token of one device.
|
/// Replaces the access token of one device.
|
||||||
pub async fn set_token(
|
pub async fn set_token(
|
||||||
&self,
|
&self,
|
||||||
user_id: &UserId,
|
user_id: &UserId,
|
||||||
device_id: &DeviceId,
|
device_id: &DeviceId,
|
||||||
token: &str,
|
DeviceToken { token, max_age }: DeviceToken,
|
||||||
token_max_age: Option<Duration>,
|
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let key = (user_id, device_id);
|
let key = (user_id, device_id);
|
||||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||||
@@ -136,7 +145,7 @@ pub async fn set_token(
|
|||||||
if self
|
if self
|
||||||
.services
|
.services
|
||||||
.appservice
|
.appservice
|
||||||
.find_from_token(token)
|
.find_from_token(&token)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
@@ -153,10 +162,10 @@ pub async fn set_token(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assign token to user device combination
|
// Assign token to user device combination
|
||||||
self.db.userdeviceid_token.put_raw(key, token);
|
self.db.userdeviceid_token.put_raw(key, &token);
|
||||||
self.db.token_userdeviceid.raw_put(token, key);
|
self.db.token_userdeviceid.raw_put(&token, key);
|
||||||
|
|
||||||
if let Some(max_age) = token_max_age {
|
if let Some(max_age) = max_age {
|
||||||
let expires = SystemTime::now()
|
let expires = SystemTime::now()
|
||||||
.duration_since(SystemTime::UNIX_EPOCH)
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
.expect("system time should not be before the epoch")
|
.expect("system time should not be before the epoch")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
utils::{self},
|
utils::{self},
|
||||||
};
|
};
|
||||||
use database::Map;
|
use database::Map;
|
||||||
|
pub use device::DeviceToken;
|
||||||
pub use profile::ProfileFieldChange;
|
pub use profile::ProfileFieldChange;
|
||||||
use ruma::{UserId, api::error::ErrorKind, encryption::CrossSigningKey, serde::Raw};
|
use ruma::{UserId, api::error::ErrorKind, encryption::CrossSigningKey, serde::Raw};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ async fn get_account_deeplink(
|
|||||||
));
|
));
|
||||||
};
|
};
|
||||||
|
|
||||||
format!("device/{device_id}/delete")
|
format!("device/{device_id}/remove")
|
||||||
},
|
},
|
||||||
| AccountManagementAction::DeviceView => {
|
| AccountManagementAction::DeviceView => {
|
||||||
let Some(device_id) = query.device_id else {
|
let Some(device_id) = query.device_id else {
|
||||||
|
|||||||
@@ -6,9 +6,12 @@
|
|||||||
response::Redirect,
|
response::Redirect,
|
||||||
routing::on,
|
routing::on,
|
||||||
};
|
};
|
||||||
use conduwuit_service::{oauth::grant::AuthorizationCodeResponse, oidc::SessionCompletionStatus};
|
use conduwuit_service::{
|
||||||
|
oauth::grant::AuthorizationCodeResponse,
|
||||||
|
oidc::{ClaimedLocalUser, SessionCompletionStatus},
|
||||||
|
};
|
||||||
use futures::FutureExt;
|
use futures::FutureExt;
|
||||||
use ruma::{OwnedServerName, UserId};
|
use ruma::OwnedServerName;
|
||||||
use serde::{Deserialize, de::IgnoredAny};
|
use serde::{Deserialize, de::IgnoredAny};
|
||||||
use tower_sessions::Session;
|
use tower_sessions::Session;
|
||||||
|
|
||||||
@@ -99,65 +102,60 @@ async fn route_complete(
|
|||||||
state: OidcSessionState::Authorized { claims: Box::new(claims.clone()) },
|
state: OidcSessionState::Authorized { claims: Box::new(claims.clone()) },
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.expect("Should be able to serialize oidc session");
|
.expect("should be able to serialize oidc session");
|
||||||
|
|
||||||
services.oidc.complete_session(&claims, None).await
|
services.oidc.complete_session(&claims, None).await
|
||||||
},
|
},
|
||||||
| OidcSessionState::Authorized { claims } => {
|
| OidcSessionState::Authorized { claims } => {
|
||||||
let supplied_user_id = if let Some(form) = form {
|
let supplied_user_id = if let Some(form) = form {
|
||||||
if let Ok(user_id) = UserId::parse(format!(
|
match services
|
||||||
"@{}:{}",
|
.oidc
|
||||||
form.username,
|
.identify_claimed_local_user(&form.username)
|
||||||
services.globals.server_name()
|
.await
|
||||||
)) && services.users.status(&user_id).await.is_active()
|
|
||||||
{
|
{
|
||||||
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
| Ok(ClaimedLocalUser::Existing(user_id)) => {
|
||||||
|
let user_card =
|
||||||
|
UserCard::for_local_user(&services, user_id.clone()).await;
|
||||||
|
|
||||||
if let Some(password) = form.password {
|
if let Some(password) = form.password {
|
||||||
if services
|
if services
|
||||||
.users
|
.users
|
||||||
.check_password(&user_id, &password)
|
.check_password(&user_id, &password)
|
||||||
.await
|
.await
|
||||||
.is_ok()
|
.is_ok()
|
||||||
{
|
{
|
||||||
Some(user_id)
|
Some(user_id)
|
||||||
|
} else {
|
||||||
|
return response!(OidcComplete::new(
|
||||||
|
context,
|
||||||
|
OidcCompleteBody::PasswordPrompt {
|
||||||
|
username: form.username,
|
||||||
|
user_card,
|
||||||
|
password_error: true
|
||||||
|
}
|
||||||
|
));
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return response!(OidcComplete::new(
|
return response!(OidcComplete::new(
|
||||||
context,
|
context,
|
||||||
OidcCompleteBody::PasswordPrompt {
|
OidcCompleteBody::PasswordPrompt {
|
||||||
username: form.username,
|
username: form.username,
|
||||||
user_card,
|
user_card,
|
||||||
password_error: true
|
password_error: false,
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else {
|
},
|
||||||
|
| Ok(ClaimedLocalUser::New(user_id)) => Some(user_id),
|
||||||
|
| Err(err) => {
|
||||||
return response!(OidcComplete::new(
|
return response!(OidcComplete::new(
|
||||||
context,
|
context,
|
||||||
OidcCompleteBody::PasswordPrompt {
|
OidcCompleteBody::UsernamePrompt {
|
||||||
username: form.username,
|
server_name: services.globals.server_name().to_owned(),
|
||||||
user_card,
|
username_error: Some(err.message()),
|
||||||
password_error: false,
|
|
||||||
}
|
}
|
||||||
));
|
));
|
||||||
}
|
},
|
||||||
} else {
|
|
||||||
match services
|
|
||||||
.users
|
|
||||||
.determine_registration_user_id(Some(form.username), None, None)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
| Ok(user_id) => Some(user_id),
|
|
||||||
| Err(err) => {
|
|
||||||
return response!(OidcComplete::new(
|
|
||||||
context,
|
|
||||||
OidcCompleteBody::UsernamePrompt {
|
|
||||||
server_name: services.globals.server_name().to_owned(),
|
|
||||||
username_error: Some(err.message()),
|
|
||||||
}
|
|
||||||
));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|||||||
Reference in New Issue
Block a user