mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-18 04:26:15 +00:00
Compare commits
25
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39a1c1cf33 | ||
|
|
eff454218c | ||
|
|
e2eb980b5e | ||
|
|
8945cc9e10 | ||
|
|
78adeccda1 | ||
|
|
8195373d81 | ||
|
|
1ac05838dd | ||
|
|
7ecd2aa5e2 | ||
|
|
69a3145983 | ||
|
|
73b7553b1e | ||
|
|
232ec3f620 | ||
|
|
e52bbeaf94 | ||
|
|
ad1c194fb0 | ||
|
|
5e2dcd8e79 | ||
|
|
24c6474bd1 | ||
|
|
642d05048b | ||
|
|
446ae93ad7 | ||
|
|
9ca0774b2d | ||
|
|
ce494eb0c0 | ||
|
|
1f5e178c3f | ||
|
|
ddd050d402 | ||
|
|
6ad3e679bc | ||
|
|
65812bb246 | ||
|
|
9f3a7994c7 | ||
|
|
baef3289fe |
@@ -71,7 +71,7 @@ runs:
|
||||
|
||||
- name: Install timelord-cli and git-warp-time
|
||||
if: steps.check-binaries.outputs.need-install == 'true'
|
||||
uses: https://github.com/taiki-e/install-action@50414676f9f5d50a65992c6dd2ed02641263226c # v2
|
||||
uses: https://github.com/taiki-e/install-action@43aecc8d72668fbcfe75c31400bc4f890f1c5853 # v2
|
||||
with:
|
||||
tool: git-warp-time,timelord-cli@3.0.1
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
path: binaries
|
||||
merge-multiple: true
|
||||
- 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:
|
||||
draft: true
|
||||
files: binaries/*
|
||||
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
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@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: 📦 Setup Node.js
|
||||
uses: https://github.com/actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||
uses: https://github.com/actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
|
||||
with:
|
||||
node-version: "22"
|
||||
|
||||
|
||||
@@ -218,7 +218,7 @@ jobs:
|
||||
path: binaries
|
||||
merge-multiple: true
|
||||
- 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:
|
||||
draft: true
|
||||
files: binaries/*
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:43.252.1@sha256:121eb04ef758537019fb58f587aa53c99e5fda4e703993ce2ca01bd4b3926bd4
|
||||
image: ghcr.io/renovatebot/renovate:43.260.2@sha256:60d830108d9ff6408d11a55d5f110ee0976bed78a6b82b08211e3ddb72f72952
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
# 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
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ ### Pre-commit Checks
|
||||
|
||||
|
||||
```bash
|
||||
# Install prek using cargo-binstall
|
||||
# Install prek using cargo-binstall https://github.com/cargo-bins/cargo-binstall
|
||||
cargo binstall prek
|
||||
|
||||
# Install git hooks to run checks automatically
|
||||
@@ -155,7 +155,7 @@ ### Creating pull requests
|
||||
|
||||
Before submitting a pull request, please ensure:
|
||||
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
|
||||
4. Tests are added for new functionality
|
||||
5. Documentation is updated if needed
|
||||
|
||||
Generated
+155
-252
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
||||
# See also `rust-toolchain.toml`
|
||||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
version = "26.6.1"
|
||||
version = "26.6.2"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
@@ -344,7 +344,7 @@ version = "1.1.1"
|
||||
[workspace.dependencies.ruma]
|
||||
# version = "0.14.1"
|
||||
git = "https://github.com/ruma/ruma.git"
|
||||
rev = "9b6a2e7323649af926e4b5363d87239ee4247f4a"
|
||||
rev = "7f925c183b748e649bd2fa24eb1f3304159a73a4"
|
||||
features = [
|
||||
"appservice-api-c",
|
||||
"client-api",
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
Added support for the OAuth2 device authorization flow. Contributed by @ginger
|
||||
@@ -0,0 +1 @@
|
||||
Appservices are now properly able to create devices for E2EE.
|
||||
@@ -1 +0,0 @@
|
||||
Fixed `global.oauth.compatibility_mode` being required, despite being ignored, when the `[global.oauth.oidc]` config section is provided.
|
||||
@@ -1 +0,0 @@
|
||||
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.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed newly created rooms failing to sync properly in clients using legacy sync.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed newly joined rooms failing to sync their full state (including the room name) to clients using legacy sync.
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Updated an out-of-date statement about Oracle Linux release cadences.
|
||||
@@ -0,0 +1 @@
|
||||
Fixed high CPU usage when multiple clients from the same account were connected at once. Each sync woke the account's other sync loops, causing them to wake each other in a loop.
|
||||
@@ -0,0 +1 @@
|
||||
Fix joining restricted rooms over federation failing with signature verification error.
|
||||
@@ -15,8 +15,8 @@ ## Overview
|
||||
|
||||
:::warning Oracle Linux support
|
||||
Due to upstream limitations, Terra is only usable on Oracle Linux if you use [upstream EPEL](https://docs.fedoraproject.org/en-US/epel/getting-started/)
|
||||
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, as of the time of writing, 10.2 has been
|
||||
available for over a month through other ELs, but Oracle has not yet released corresponding updates—so you may encounter further compatibility issues with EPEL.
|
||||
rather than Oracle's rebuilds of EPEL. Oracle tends to lag behind on new EL versions, both major and minor—for example, Oracle's release of 10.2 lagged
|
||||
approximately 1.5 months behind other ELs—so you may encounter further compatibility issues with EPEL.
|
||||
**For this reason, it is recommended that you use another EL distribution if at all possible.**
|
||||
:::
|
||||
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
"message": "Welcome to Continuwuity! Important announcements about the project will appear here."
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 16,
|
||||
"mention_room": true,
|
||||
"date": "2026-07-10",
|
||||
"message": "[Continuwuity 26.6.1](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v26.6.1) has just been released! This release contains a bunch of bugfixes for bugs found initially after 26.6.0's release - please read the changelog before updating!"
|
||||
"date": "2026-07-13",
|
||||
"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": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1781566179,
|
||||
"narHash": "sha256-Tqv8I586fYzWpEW/Smq/JqESFa3DVVzVWsnAMtvhy/I=",
|
||||
"lastModified": 1783840254,
|
||||
"narHash": "sha256-XjyvZk0f3YiinVHmkGOotmLBAzvK+LwEJYj2QqJ5pn8=",
|
||||
"owner": "rustsec",
|
||||
"repo": "advisory-db",
|
||||
"rev": "74e084413d979d52d2f93b1d93b1ab7b9ee648f5",
|
||||
"rev": "6e3286f4efa8c142fb33e5ea4342c8db6693cf34",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -18,11 +18,11 @@
|
||||
},
|
||||
"crane": {
|
||||
"locked": {
|
||||
"lastModified": 1780532242,
|
||||
"narHash": "sha256-D+BsdpxmtUwtqGoY0IXPhHgTlmqgcZKCEo1oMyn7ep0=",
|
||||
"lastModified": 1783203018,
|
||||
"narHash": "sha256-G6R9IT/xwFuu+CYBWDUAok6AdC4ERC4ZfPPFtEpxnZE=",
|
||||
"owner": "ipetkov",
|
||||
"repo": "crane",
|
||||
"rev": "59a82a1222dd3b2080b5cc52a1a2e8d5f1b77f37",
|
||||
"rev": "80db5bdc391be8a1794f6d8a2d56e3a84ebcede2",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -39,11 +39,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1781527054,
|
||||
"narHash": "sha256-1fX9ev2Fh5QoKQ41G9dYutjo5j/jywu6tZse5Eb1Ck4=",
|
||||
"lastModified": 1783844668,
|
||||
"narHash": "sha256-3MOpw4y3reoErRLvFBDz0t9aG6FWXUj7leKvUns5eQA=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "8c2e51dffefc040a21975da7abf6f252c8c9b783",
|
||||
"rev": "4e49ef62eedaa5c149d62b63b3f53844e1cc45d7",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -74,11 +74,11 @@
|
||||
"nixpkgs-lib": "nixpkgs-lib"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1778716662,
|
||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
||||
"lastModified": 1782949081,
|
||||
"narHash": "sha256-vp6Y/Grm98ESt6ceOkWiHWyZRDV3J1RID4w+6NWK9yA=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "flake-parts",
|
||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
||||
"rev": "17c9d6cdfc60c64f4ee8d306f9bc0b4ccb51481e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -89,11 +89,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1781074563,
|
||||
"narHash": "sha256-md8WlXOlfnIeHeOScMTTHFyf2d6iaTwPl2apR5EQ3P4=",
|
||||
"lastModified": 1783776592,
|
||||
"narHash": "sha256-UgCQzxeWI75XM8G+hPrPh+MKzEPjG3SpAj7dtqSbksA=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9ae611a455b90cf061d8f332b977e387bda8e1ca",
|
||||
"rev": "e7a3ca8092b61ff85b6a45bf863ea2b2d6a661b3",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -105,11 +105,11 @@
|
||||
},
|
||||
"nixpkgs-lib": {
|
||||
"locked": {
|
||||
"lastModified": 1777168982,
|
||||
"narHash": "sha256-GOkGPcboWE9BmGCRMLX3worL4EMnsnG8MyKmXNeYuhQ=",
|
||||
"lastModified": 1782614948,
|
||||
"narHash": "sha256-ePjCwr1sNm9NYUqywL7QfK3JnlS015msC+eBu2zKlp8=",
|
||||
"owner": "nix-community",
|
||||
"repo": "nixpkgs.lib",
|
||||
"rev": "f5901329dade4a6ea039af1433fb087bd9c1fe14",
|
||||
"rev": "db3f255737b94216eb71cce308e2912cf6bc2d7c",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -132,11 +132,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1781453968,
|
||||
"narHash": "sha256-+V3nK4pCngbmgyVGXY6Kkrlevp4ocPkJJLf2aqwkDNA=",
|
||||
"lastModified": 1783779020,
|
||||
"narHash": "sha256-vpm418WZa9l1KUl6HRs+Ga+SIfE+D2/sV4olwTlilYc=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "cc272809a173c2c11d0e479d639c811c1eacf049",
|
||||
"rev": "5be5e89cf0145d73a85c805726821b4adfc3af48",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
Generated
+48
-48
@@ -510,14 +510,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/core": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.2.0.tgz",
|
||||
"integrity": "sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.1.tgz",
|
||||
"integrity": "sha512-ANMDxuaPsNMdDC1m4vfvhlDmJweMwkE5XitTwrq2rWHx5jM+dlm4MmHt2PP6t0uejfR77SuhrhJ0zEijIF/uhA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/primitive": "4.2.0",
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/primitive": "4.3.1",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-html": "^9.0.5"
|
||||
@@ -527,13 +527,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-javascript": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.2.0.tgz",
|
||||
"integrity": "sha512-fjETeq1k5ffyXqRgS6+3hpvqseLalp1kjNfRbXpUgWR8FpZ1CmQfiNHovc5lncYjt/Vg5JK/WJEmLahjwMa0og==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.3.1.tgz",
|
||||
"integrity": "sha512-JBItcnPuYq7jVJdZo/vMj94r+szT7XEjHFX+mvFDGSEIbVAXAGyHAHzhbWzpGOwYidCZrErJLLgn2PVeiokHnQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"oniguruma-to-es": "^4.3.6"
|
||||
},
|
||||
@@ -542,13 +542,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/engine-oniguruma": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.2.0.tgz",
|
||||
"integrity": "sha512-hTorK1dffPkpbMUk6Z+828PgRo7d07HbnizoP0hNPFjhxMHctj0Px/qoHeGMYafc6ju+u9iMldN4JbVzNQM++g==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.3.1.tgz",
|
||||
"integrity": "sha512-OXyNMzg0pews+msMj4cHeqT4xiYKKvbnn6VbdAXxfoFl3SSx4fJTc8FadECuc5/H9p3BzhNAoAUXKwAu9rWYhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2"
|
||||
},
|
||||
"engines": {
|
||||
@@ -556,26 +556,26 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/langs": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.2.0.tgz",
|
||||
"integrity": "sha512-bwrVRlJ0wUhZxAbVdvBbv2TTC9yLsh4C/IO5Ofz0T8MQntgDvyVnkbjw9vi50r1kx7RCIJdnJnjZAwmAsXFLZQ==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.3.1.tgz",
|
||||
"integrity": "sha512-m0l9nsDqgBHvbZbk7A0/kXz/impK3uB/c6rAn6Gpg/uPtdZRQ+alsN/17MU5thb68XTj/4DxkZAotrM0GGSpDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0"
|
||||
"@shikijs/types": "4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/primitive": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.2.0.tgz",
|
||||
"integrity": "sha512-NOq+DtUkVBJtZMVXL5A0vI0Xk8nvDYaXetFHSJFlOqjDZIVhIPRYFdGkSoElDqNuegikcc3A76SNUa8dTqtAYA==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.3.1.tgz",
|
||||
"integrity": "sha512-CXQRQOYy1leqQ8ceTeJdmXv/bsUY++6QyLpXJ94LZAAYj5X2SKRdc5ipguv4NPyGVKItB2PPwUpRNe0Sjh5S1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
@@ -584,16 +584,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/rehype": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.2.0.tgz",
|
||||
"integrity": "sha512-ST3EWye/dwF1gWskczJNBnwFtDzEQ9ceytXZtyc/GfwR5V0qJrkoSGZO55O3SAKDDsXkTDcsfwd9pVe7ROlAHg==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/rehype/-/rehype-4.3.1.tgz",
|
||||
"integrity": "sha512-oshrlfUF3VPUJfnp5K1lLwsS/SRBKrIxONpdWebSKZXdBE3UsZnxgqpvRUA8UsofS7vmjFOCAHIT71ECbmOxTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@types/hast": "^3.0.4",
|
||||
"hast-util-to-string": "^3.0.1",
|
||||
"shiki": "4.2.0",
|
||||
"shiki": "4.3.1",
|
||||
"unified": "^11.0.5",
|
||||
"unist-util-visit": "^5.1.0"
|
||||
},
|
||||
@@ -602,22 +602,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/themes": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.2.0.tgz",
|
||||
"integrity": "sha512-RX8IHYeLv8Cu2W6ruc3RxUqWn0IYCqSrMBzi/uRGAmfyDNOnNO5BF/Px7o97n4XTpmFTo5GbRaazuOWj+2ak2w==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.3.1.tgz",
|
||||
"integrity": "sha512-dgpoJ4WqNi2yTmizQHBJ5zcX6j2lE6icN/0yt4l1kkf16jrY/pwPLoTb1ETsWMz0OBLf9ZNvwmxft+cH+N9qSA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/types": "4.2.0"
|
||||
"@shikijs/types": "4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
},
|
||||
"node_modules/@shikijs/types": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.2.0.tgz",
|
||||
"integrity": "sha512-VT/MKtlpOhEPZloSH3Pb9WCZEBDoQVMa9jedp5UAwmJOar1DVc9DRODAxmYPW9M93IK4ryuqRejFfmlvlVDemw==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.3.1.tgz",
|
||||
"integrity": "sha512-CHFxE0jztBIZRHH6gxXE7DXUCFXjReEGxZ/j0rfSLGKZuwp2xBYycEP14875DSa9KLL/6700oxIq6oO6ef9K2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -684,9 +684,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hast": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz",
|
||||
"integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz",
|
||||
"integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1076,9 +1076,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||
"integrity": "sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==",
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.3.tgz",
|
||||
"integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -3589,18 +3589,18 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shiki": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.2.0.tgz",
|
||||
"integrity": "sha512-hjNax6o/ylDy9lefQEaSDtzaT3iVNtZ3WmpQnbuQNoG4xvnSKf2kSKbihZVO4JRG1TTMejs7CmNRYlWgAL66pQ==",
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/shiki/-/shiki-4.3.1.tgz",
|
||||
"integrity": "sha512-oR+qDVi2OjX1tmDpyv+3KviX01KzO6Af+0NNnKnsp9491UEGz2YpxTuJboS/6VhYpTdqzmuJBuiTlrAWWJAssw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@shikijs/core": "4.2.0",
|
||||
"@shikijs/engine-javascript": "4.2.0",
|
||||
"@shikijs/engine-oniguruma": "4.2.0",
|
||||
"@shikijs/langs": "4.2.0",
|
||||
"@shikijs/themes": "4.2.0",
|
||||
"@shikijs/types": "4.2.0",
|
||||
"@shikijs/core": "4.3.1",
|
||||
"@shikijs/engine-javascript": "4.3.1",
|
||||
"@shikijs/engine-oniguruma": "4.3.1",
|
||||
"@shikijs/langs": "4.3.1",
|
||||
"@shikijs/themes": "4.3.1",
|
||||
"@shikijs/types": "4.3.1",
|
||||
"@shikijs/vscode-textmate": "^10.0.2",
|
||||
"@types/hast": "^3.0.4"
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
};
|
||||
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};
|
||||
|
||||
pub(crate) mod register;
|
||||
|
||||
@@ -20,9 +20,12 @@
|
||||
assign,
|
||||
};
|
||||
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;
|
||||
|
||||
/// # `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());
|
||||
|
||||
// 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
|
||||
services
|
||||
@@ -127,8 +130,7 @@ pub(crate) async fn register_route(
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&new_token,
|
||||
None,
|
||||
Some(new_token.clone()),
|
||||
body.initial_device_display_name.clone(),
|
||||
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");
|
||||
|
||||
Ok(assign!(register::v3::Response::new(user_id), {
|
||||
access_token: token,
|
||||
access_token: token.map(DeviceToken::into_token),
|
||||
device_id: device,
|
||||
refresh_token: None,
|
||||
expires_in: None,
|
||||
|
||||
@@ -89,14 +89,7 @@ pub(crate) async fn update_device_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.create_device(
|
||||
sender_user,
|
||||
&device_id,
|
||||
&appservice.registration.as_token,
|
||||
None,
|
||||
None,
|
||||
Some(client.to_string()),
|
||||
)
|
||||
.create_device(sender_user, &device_id, None, None, Some(client.to_string()))
|
||||
.await?;
|
||||
|
||||
return Ok(update_device::v3::Response::new());
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
use axum::{Form, Json, extract::State, response::IntoResponse};
|
||||
use http::StatusCode;
|
||||
use service::oauth::grant::DeviceCodeRequest;
|
||||
|
||||
pub(crate) async fn device_authorization_route(
|
||||
State(services): State<crate::State>,
|
||||
Form(request): Form<DeviceCodeRequest>,
|
||||
) -> impl IntoResponse {
|
||||
match services.oauth.request_device_code(request).await {
|
||||
| Ok(response) => Ok(Json(response)),
|
||||
| Err(err) => Err((StatusCode::BAD_REQUEST, Json(err))),
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@
|
||||
use serde_json::json;
|
||||
pub(crate) use server_metadata::*;
|
||||
|
||||
mod device;
|
||||
mod register_client;
|
||||
mod server_metadata;
|
||||
mod token;
|
||||
@@ -21,7 +20,6 @@
|
||||
const CLIENT_REGISTER_PATH: &str = "client/register";
|
||||
const TOKEN_REVOKE_PATH: &str = "client/revoke";
|
||||
const TOKEN_PATH: &str = "grant/token";
|
||||
const DEVICE_AUTHORIZATION_PATH: &str = "device";
|
||||
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
|
||||
|
||||
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
|
||||
@@ -55,5 +53,4 @@ fn oauth_router() -> Router<crate::State> {
|
||||
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
|
||||
.route(concat!("/", TOKEN_PATH), post(token::token_route))
|
||||
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
|
||||
.route(concat!("/", DEVICE_AUTHORIZATION_PATH), post(device::device_authorization_route))
|
||||
}
|
||||
|
||||
@@ -24,5 +24,5 @@ pub(crate) async fn register_client_route(
|
||||
.await
|
||||
.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())
|
||||
}
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
use crate::{
|
||||
Ruma,
|
||||
client::oauth::{
|
||||
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, DEVICE_AUTHORIZATION_PATH,
|
||||
JWKS_URI_PATH, TOKEN_PATH, TOKEN_REVOKE_PATH,
|
||||
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
|
||||
TOKEN_REVOKE_PATH,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -49,8 +49,7 @@ pub(crate) async fn authorization_server_metadata(services: &Services) -> Value
|
||||
],
|
||||
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
|
||||
"code_challenge_methods_supported": ["S256"],
|
||||
"device_authorization_endpoint": endpoint_base.join(DEVICE_AUTHORIZATION_PATH).unwrap(),
|
||||
"grant_types_supported": ["authorization_code", "refresh_token", "urn:ietf:params:oauth:grant-type:device_code"],
|
||||
"grant_types_supported": ["authorization_code", "refresh_token"],
|
||||
"issuer": services.config.get_client_domain(),
|
||||
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
|
||||
"prompt_values_supported": ["create"],
|
||||
|
||||
@@ -29,8 +29,9 @@
|
||||
},
|
||||
assign,
|
||||
};
|
||||
use service::users::DeviceToken;
|
||||
|
||||
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
|
||||
use super::DEVICE_ID_LENGTH;
|
||||
use crate::Ruma;
|
||||
|
||||
/// # `GET /_matrix/client/v3/login`
|
||||
@@ -196,8 +197,8 @@ pub(crate) async fn login_route(
|
||||
.clone()
|
||||
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
|
||||
|
||||
// Generate a new token for the device (ensuring no collisions)
|
||||
let token = services.users.generate_unique_token().await;
|
||||
// Generate a new token for the device
|
||||
let token = DeviceToken::new_random();
|
||||
|
||||
// Determine if device_id was provided and exists in the db for this user
|
||||
let device_exists = if body.device_id.is_some() {
|
||||
@@ -213,7 +214,7 @@ pub(crate) async fn login_route(
|
||||
if device_exists {
|
||||
services
|
||||
.users
|
||||
.set_token(&user_id, &device_id, &token, None)
|
||||
.set_token(&user_id, &device_id, token.clone())
|
||||
.await?;
|
||||
} else {
|
||||
services
|
||||
@@ -221,8 +222,7 @@ pub(crate) async fn login_route(
|
||||
.create_device(
|
||||
&user_id,
|
||||
&device_id,
|
||||
&token,
|
||||
None,
|
||||
Some(token.clone()),
|
||||
body.initial_device_display_name.clone(),
|
||||
Some(client.to_string()),
|
||||
)
|
||||
@@ -241,7 +241,7 @@ pub(crate) async fn login_route(
|
||||
info!("{user_id} logged in");
|
||||
|
||||
#[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,
|
||||
expires_in: None,
|
||||
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)
|
||||
.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);
|
||||
|
||||
Ok(get_login_token::v1::Response::new(
|
||||
|
||||
@@ -269,13 +269,22 @@ async fn build_state_and_timeline(
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (state_events, notification_counts, joined_since_last_sync) = try_join3(
|
||||
build_state_events(services, sync_context, room_id, shortstatehashes, &timeline),
|
||||
let (notification_counts, joined_since_last_sync) = try_join(
|
||||
build_notification_counts(services, sync_context, room_id, &timeline),
|
||||
check_joined_since_last_sync(services, shortstatehashes, sync_context),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let state_events = build_state_events(
|
||||
services,
|
||||
sync_context,
|
||||
room_id,
|
||||
shortstatehashes,
|
||||
&timeline,
|
||||
joined_since_last_sync,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// the timeline should always include at least one PDU if the syncing user
|
||||
// joined since the last sync, that being the syncing user's join event. if
|
||||
// it's empty something is wrong.
|
||||
@@ -459,6 +468,7 @@ async fn build_state_events(
|
||||
room_id: &RoomId,
|
||||
shortstatehashes: ShortStateHashes,
|
||||
timeline: &TimelinePdus,
|
||||
joined_since_last_sync: bool,
|
||||
) -> Result<Vec<PduEvent>> {
|
||||
let SyncContext {
|
||||
syncing_user,
|
||||
@@ -488,9 +498,10 @@ async fn build_state_events(
|
||||
/*
|
||||
if `last_sync_end_count` is Some (meaning this is an incremental sync), and `last_sync_end_shortstatehash`
|
||||
is Some (meaning the syncing user didn't just join this room for the first time ever), and `full_state` is false,
|
||||
then use `build_state_incremental`.
|
||||
and the user didn't just join the room since the last sync, then use `build_state_incremental`.
|
||||
*/
|
||||
| (Some(_), Some(last_sync_end_shortstatehash)) if !full_state =>
|
||||
| (Some(_), Some(last_sync_end_shortstatehash))
|
||||
if !full_state && !joined_since_last_sync =>
|
||||
build_state_incremental(
|
||||
services,
|
||||
syncing_user,
|
||||
|
||||
@@ -15,10 +15,9 @@
|
||||
#[derive(Deserialize)]
|
||||
pub(crate) struct AuthQueryParams {
|
||||
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>,
|
||||
#[serde(rename = "org.matrix.msc3202.device_id")]
|
||||
pub(super) legacy_device_id: Option<String>,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
// The device_id can be provided via `device_id` or
|
||||
// `org.matrix.msc3202.device_id` query parameter.
|
||||
let sender_device =
|
||||
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as \
|
||||
this device."
|
||||
)));
|
||||
}
|
||||
let sender_device = if let Some(device_id) = query
|
||||
.device_id
|
||||
.or(query.legacy_device_id)
|
||||
.as_deref()
|
||||
.map(Into::into)
|
||||
{
|
||||
// Verify the device exists for this user
|
||||
if services
|
||||
.users
|
||||
.get_device_metadata(&sender_user, device_id)
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
return Err!(Request(Forbidden(
|
||||
"Device does not exist for user or appservice cannot masquerade as this \
|
||||
device."
|
||||
)));
|
||||
}
|
||||
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Some(device_id.to_owned())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ClientIdentity::Appservice {
|
||||
sender_user,
|
||||
|
||||
+32
-1
@@ -91,7 +91,7 @@ pub(crate) async fn validate_any_membership_event(
|
||||
expected_room_id: OwnedRoomId,
|
||||
expected_event_id: OwnedEventId,
|
||||
) -> Result<(CanonicalJsonObject, MembershipState, OwnedUserId, OwnedUserId)> {
|
||||
let (template_room_id, template_event_id, pdu) = services
|
||||
let (template_room_id, template_event_id, mut pdu) = services
|
||||
.rooms
|
||||
.event_handler
|
||||
.parse_incoming_pdu(body, Some(room_version_rules))
|
||||
@@ -109,6 +109,37 @@ pub(crate) async fn validate_any_membership_event(
|
||||
))));
|
||||
}
|
||||
|
||||
// Only `join` events carry `join_authorised_via_users_server`; co-sign
|
||||
// restricted joins so verification passes. Authorisation is enforced in
|
||||
// create_join_event.
|
||||
let membership_is_join = pdu
|
||||
.get("content")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|c| c.get("membership"))
|
||||
.and_then(|v| v.as_str())
|
||||
.is_some_and(|m| m == "join");
|
||||
|
||||
let authorising_user = pdu
|
||||
.get("content")
|
||||
.and_then(|v| v.as_object())
|
||||
.and_then(|c| c.get("join_authorised_via_users_server"))
|
||||
.and_then(|v| v.as_str())
|
||||
.map(UserId::parse)
|
||||
.and_then(Result::ok);
|
||||
|
||||
if room_version_rules.authorization.restricted_join_rule
|
||||
&& membership_is_join
|
||||
&& let Some(authorising_user) = authorising_user
|
||||
&& services.globals.user_is_local(&authorising_user)
|
||||
{
|
||||
services
|
||||
.server_keys
|
||||
.hash_and_sign_event(&mut pdu, room_version_rules)
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam("Failed to sign restricted join event: {e}")))
|
||||
})?;
|
||||
}
|
||||
|
||||
services
|
||||
.server_keys
|
||||
.verify_event(&pdu, room_version_rules)
|
||||
|
||||
@@ -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 tokio::time::{Duration, sleep};
|
||||
|
||||
@@ -7,10 +9,16 @@
|
||||
pub(super) const SIGNAL: &str = "SIGUSR2";
|
||||
|
||||
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) {
|
||||
#[cfg(feature = "console")]
|
||||
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
|
||||
tokio::task::yield_now().await;
|
||||
self.console.start().await;
|
||||
|
||||
@@ -132,8 +132,6 @@ pub enum ApplicationType {
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum GrantType {
|
||||
AuthorizationCode,
|
||||
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
|
||||
DeviceCode,
|
||||
RefreshToken,
|
||||
}
|
||||
|
||||
|
||||
+17
-62
@@ -13,7 +13,6 @@
|
||||
use url::Url;
|
||||
|
||||
use super::client_metadata::ResponseType;
|
||||
use crate::oauth::client_metadata::GrantType;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct AuthorizationCodeQuery {
|
||||
@@ -30,33 +29,6 @@ pub struct AuthorizationCodeQuery {
|
||||
pub prompt: Option<Prompt>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct AuthorizationCodeResponse {
|
||||
pub state: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DeviceCodeRequest {
|
||||
pub client_id: String,
|
||||
pub scope: RawScopes,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
pub struct DeviceCodeResponse {
|
||||
pub device_code: String,
|
||||
pub user_code: String,
|
||||
pub verification_uri: Url,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub verification_uri_complete: Option<Url>,
|
||||
pub expires_in: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct DeviceCodeVerifyQuery {
|
||||
pub user_code: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[non_exhaustive]
|
||||
@@ -154,29 +126,19 @@ pub struct OAuthError {
|
||||
|
||||
impl OAuthError {
|
||||
#[must_use]
|
||||
pub fn new(error: ErrorCode, error_description: String) -> Self {
|
||||
pub const fn invalid_request(error_description: &'static str) -> Self {
|
||||
Self {
|
||||
error,
|
||||
error_description: Cow::Owned(error_description),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn new_static(error: ErrorCode, error_description: &'static str) -> Self {
|
||||
Self {
|
||||
error,
|
||||
error: ErrorCode::InvalidRequest,
|
||||
error_description: Cow::Borrowed(error_description),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn invalid_request(error_description: &'static str) -> Self {
|
||||
Self::new_static(ErrorCode::InvalidRequest, error_description)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub const fn invalid_grant(error_description: &'static str) -> Self {
|
||||
Self::new_static(ErrorCode::InvalidGrant, error_description)
|
||||
Self {
|
||||
error: ErrorCode::InvalidGrant,
|
||||
error_description: Cow::Borrowed(error_description),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,43 +157,36 @@ pub enum ErrorCode {
|
||||
AccessDenied,
|
||||
InvalidScope,
|
||||
InvalidGrant,
|
||||
InvalidClient,
|
||||
InvalidClientMetadata,
|
||||
AuthorizationPending,
|
||||
ExpiredToken,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct TokenRequest {
|
||||
pub client_id: String,
|
||||
#[serde(flatten)]
|
||||
pub request: TokenRequestType,
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct AuthorizationCodeResponse {
|
||||
pub state: String,
|
||||
pub code: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "grant_type", rename_all = "snake_case")]
|
||||
pub enum TokenRequestType {
|
||||
pub enum TokenRequest {
|
||||
AuthorizationCode {
|
||||
code: String,
|
||||
redirect_uri: Url,
|
||||
client_id: String,
|
||||
code_verifier: String,
|
||||
},
|
||||
#[serde(rename = "urn:ietf:params:oauth:grant-type:device_code")]
|
||||
DeviceCode {
|
||||
device_code: String,
|
||||
},
|
||||
RefreshToken {
|
||||
client_id: String,
|
||||
refresh_token: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl TokenRequestType {
|
||||
impl TokenRequest {
|
||||
#[must_use]
|
||||
pub fn grant_type(&self) -> GrantType {
|
||||
pub fn client_id(&self) -> &str {
|
||||
match self {
|
||||
| Self::AuthorizationCode { .. } => GrantType::AuthorizationCode,
|
||||
| Self::DeviceCode { .. } => GrantType::DeviceCode,
|
||||
| Self::RefreshToken { .. } => GrantType::RefreshToken,
|
||||
| Self::AuthorizationCode { client_id, .. }
|
||||
| Self::RefreshToken { client_id, .. } => client_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
-235
@@ -12,22 +12,20 @@
|
||||
use database::{Deserialized, Json, Map};
|
||||
use itertools::Itertools;
|
||||
use lru_cache::LruCache;
|
||||
use rand::distr::{Distribution, slice::Choose};
|
||||
use ruma::{DeviceId, OwnedDeviceId, OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
Dep, config,
|
||||
Dep,
|
||||
oauth::{
|
||||
client_metadata::{ApplicationType, ClientMetadata, ResponseType},
|
||||
grant::{
|
||||
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod,
|
||||
DeviceCodeRequest, DeviceCodeResponse, ErrorCode, OAuthError, ResponseMode, Scope,
|
||||
TokenRequest, TokenRequestType, TokenResponse, TokenType,
|
||||
AuthorizationCodeQuery, AuthorizationCodeResponse, CodeChallengeMethod, ErrorCode,
|
||||
OAuthError, ResponseMode, Scope, TokenRequest, TokenResponse, TokenType,
|
||||
},
|
||||
},
|
||||
users,
|
||||
users::{self, DeviceToken},
|
||||
};
|
||||
|
||||
pub mod client_metadata;
|
||||
@@ -37,8 +35,7 @@ pub struct Service {
|
||||
services: Services,
|
||||
db: Data,
|
||||
tickets: Mutex<HashMap<String, HashMap<OAuthTicket, SystemTime>>>,
|
||||
pending_auth_code_grants: tokio::sync::Mutex<LruCache<String, PendingAuthCodeGrant>>,
|
||||
pending_device_code_grants: tokio::sync::Mutex<LruCache<String, PendingDeviceCodeGrant>>,
|
||||
pending_code_grants: tokio::sync::Mutex<LruCache<String, PendingCodeGrant>>,
|
||||
}
|
||||
|
||||
struct Data {
|
||||
@@ -49,7 +46,6 @@ struct Data {
|
||||
|
||||
struct Services {
|
||||
users: Dep<users::Service>,
|
||||
config: Dep<config::Service>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
@@ -66,7 +62,7 @@ struct RefreshTokenInfo {
|
||||
device_id: OwnedDeviceId,
|
||||
}
|
||||
|
||||
struct PendingAuthCodeGrant {
|
||||
struct PendingCodeGrant {
|
||||
authorizing_user: OwnedUserId,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
client_name: Option<String>,
|
||||
@@ -76,8 +72,12 @@ struct PendingAuthCodeGrant {
|
||||
requested_at: SystemTime,
|
||||
}
|
||||
|
||||
impl PendingAuthCodeGrant {
|
||||
impl PendingCodeGrant {
|
||||
const MAX_AGE: Duration = Duration::from_mins(1);
|
||||
const RANDOM_CODE_LENGTH: usize = 32;
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn generate_code() -> String { utils::random_string(Self::RANDOM_CODE_LENGTH) }
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
||||
@@ -90,43 +90,6 @@ pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
struct PendingDeviceCodeGrant {
|
||||
state: DeviceCodeGrantState,
|
||||
requested_scopes: BTreeSet<Scope>,
|
||||
client_name: Option<String>,
|
||||
client_id: String,
|
||||
requested_at: SystemTime,
|
||||
}
|
||||
|
||||
enum DeviceCodeGrantState {
|
||||
Unverified {
|
||||
user_code: String,
|
||||
},
|
||||
Verified {
|
||||
authorizing_user: OwnedUserId,
|
||||
},
|
||||
}
|
||||
|
||||
impl PendingDeviceCodeGrant {
|
||||
const MAX_AGE: Duration = Duration::from_mins(1);
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn is_valid_for(&self, client_id: &str) -> bool {
|
||||
let now = SystemTime::now();
|
||||
|
||||
self.client_id == client_id
|
||||
&& now
|
||||
.duration_since(self.requested_at)
|
||||
.is_ok_and(|age| age < Self::MAX_AGE)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DeviceCodeGrantInfo {
|
||||
pub device_code: String,
|
||||
pub client_metadata: ClientMetadata,
|
||||
pub requested_scopes: BTreeSet<Scope>,
|
||||
}
|
||||
|
||||
/// A time-limited grant for a client to perform some sensitive action.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub enum OAuthTicket {
|
||||
@@ -149,7 +112,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
Ok(Arc::new(Self {
|
||||
services: Services {
|
||||
users: args.depend::<users::Service>("users"),
|
||||
config: args.depend::<config::Service>("config"),
|
||||
},
|
||||
db: Data {
|
||||
clientid_clientmetadata: args.db["clientid_clientmetadata"].clone(),
|
||||
@@ -157,11 +119,8 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
refreshtoken_refreshtokeninfo: args.db["refreshtoken_refreshtokeninfo"].clone(),
|
||||
},
|
||||
tickets: Mutex::default(),
|
||||
pending_auth_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||
Self::MAX_PENDING_GRANTS,
|
||||
)),
|
||||
pending_device_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||
Self::MAX_PENDING_GRANTS,
|
||||
pending_code_grants: tokio::sync::Mutex::new(LruCache::new(
|
||||
Self::MAX_PENDING_CODE_GRANTS,
|
||||
)),
|
||||
}))
|
||||
}
|
||||
@@ -174,21 +133,11 @@ impl Service {
|
||||
// Maximum number of pending code grants which will be held in memory at once,
|
||||
// to prevent unbounded memory use if someone decides to repeatedly reload the
|
||||
// grant page.
|
||||
const MAX_PENDING_GRANTS: usize = 100;
|
||||
const MAX_PENDING_CODE_GRANTS: usize = 100;
|
||||
const RANDOM_TOKEN_LENGTH: usize = 32;
|
||||
const USER_CODE_CHARACTERS: &[char] = &['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
|
||||
const USER_CODE_LENGTH: usize = 6;
|
||||
|
||||
fn generate_token() -> String { utils::random_string(Self::RANDOM_TOKEN_LENGTH) }
|
||||
|
||||
fn generate_user_code() -> String {
|
||||
Choose::new(Self::USER_CODE_CHARACTERS)
|
||||
.unwrap()
|
||||
.sample_iter(&mut rand::rng())
|
||||
.take(Self::USER_CODE_LENGTH)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn register_client(&self, metadata: &ClientMetadata) -> Result<String, OAuthError> {
|
||||
metadata.validate().map_err(|error| OAuthError {
|
||||
error: ErrorCode::InvalidClientMetadata,
|
||||
@@ -282,14 +231,14 @@ pub async fn request_authorization_code(
|
||||
| ResponseMode::Query => '?',
|
||||
};
|
||||
|
||||
let code = Self::generate_token();
|
||||
let code = PendingCodeGrant::generate_code();
|
||||
|
||||
info!(
|
||||
client_id = &query.client_id,
|
||||
client_name = &client_metadata.client_name,
|
||||
?requested_scopes,
|
||||
?authorizing_user,
|
||||
"Issuing OAuth authorization code"
|
||||
"Issuing oauth authorization code"
|
||||
);
|
||||
|
||||
let redirect_uri = format!(
|
||||
@@ -303,7 +252,7 @@ pub async fn request_authorization_code(
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let pending_grant = PendingAuthCodeGrant {
|
||||
let pending_grant = PendingCodeGrant {
|
||||
authorizing_user,
|
||||
requested_scopes,
|
||||
client_name: client_metadata.client_name,
|
||||
@@ -313,7 +262,7 @@ pub async fn request_authorization_code(
|
||||
requested_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.pending_auth_code_grants
|
||||
self.pending_code_grants
|
||||
.lock()
|
||||
.await
|
||||
.insert(code, pending_grant);
|
||||
@@ -321,129 +270,15 @@ pub async fn request_authorization_code(
|
||||
Ok(redirect_uri)
|
||||
}
|
||||
|
||||
pub async fn request_device_code(
|
||||
&self,
|
||||
query: DeviceCodeRequest,
|
||||
) -> Result<DeviceCodeResponse, OAuthError> {
|
||||
let Some(client_metadata) = self.get_client_metadata(&query.client_id).await else {
|
||||
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
|
||||
};
|
||||
|
||||
let requested_scopes = query
|
||||
.scope
|
||||
.to_scopes()
|
||||
.map_err(|err| OAuthError::new(ErrorCode::InvalidGrant, err))?;
|
||||
|
||||
let device_code = Self::generate_token();
|
||||
let user_code = Self::generate_user_code();
|
||||
|
||||
let verification_uri = self
|
||||
.services
|
||||
.config
|
||||
.get_client_domain()
|
||||
.join(&format!("{}/oauth2/grant/device_code", conduwuit::ROUTE_PREFIX))
|
||||
.unwrap();
|
||||
|
||||
let mut verification_uri_complete = verification_uri.clone();
|
||||
verification_uri_complete
|
||||
.query_pairs_mut()
|
||||
.append_pair("user_code", &user_code);
|
||||
|
||||
info!(
|
||||
client_id = &query.client_id,
|
||||
client_name = &client_metadata.client_name,
|
||||
?requested_scopes,
|
||||
"Issuing OAuth device code"
|
||||
);
|
||||
|
||||
let pending_grant = PendingDeviceCodeGrant {
|
||||
state: DeviceCodeGrantState::Unverified { user_code: user_code.clone() },
|
||||
requested_scopes,
|
||||
client_name: client_metadata.client_name,
|
||||
client_id: query.client_id,
|
||||
requested_at: SystemTime::now(),
|
||||
};
|
||||
|
||||
self.pending_device_code_grants
|
||||
.lock()
|
||||
.await
|
||||
.insert(device_code.clone(), pending_grant);
|
||||
|
||||
Ok(DeviceCodeResponse {
|
||||
device_code,
|
||||
user_code,
|
||||
verification_uri,
|
||||
verification_uri_complete: Some(verification_uri_complete),
|
||||
expires_in: PendingDeviceCodeGrant::MAX_AGE.as_secs(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn grant_info_for_user_code(
|
||||
&self,
|
||||
supplied_user_code: &str,
|
||||
) -> Option<DeviceCodeGrantInfo> {
|
||||
let pending_grants = self.pending_device_code_grants.lock().await;
|
||||
|
||||
let (device_code, grant) = pending_grants
|
||||
.iter()
|
||||
.find(|(_, grant)| {
|
||||
matches!(&grant.state, DeviceCodeGrantState::Unverified { user_code } if user_code == supplied_user_code)
|
||||
})?;
|
||||
|
||||
let client_metadata = self
|
||||
.get_client_metadata(&grant.client_id)
|
||||
.await
|
||||
.expect("client should exist");
|
||||
|
||||
Some(DeviceCodeGrantInfo {
|
||||
device_code: device_code.clone(),
|
||||
client_metadata,
|
||||
requested_scopes: grant.requested_scopes.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn validate_device_code(
|
||||
&self,
|
||||
authorizing_user: OwnedUserId,
|
||||
device_code: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut pending_grants = self.pending_device_code_grants.lock().await;
|
||||
|
||||
let Some(pending_grant) = pending_grants.get_mut(device_code) else {
|
||||
return Err("Invalid device code".to_owned());
|
||||
};
|
||||
|
||||
match &mut pending_grant.state {
|
||||
| state @ DeviceCodeGrantState::Unverified { .. } => {
|
||||
*state = DeviceCodeGrantState::Verified { authorizing_user };
|
||||
|
||||
Ok(())
|
||||
},
|
||||
| DeviceCodeGrantState::Verified {
|
||||
authorizing_user: previous_authorizing_user,
|
||||
} =>
|
||||
if *previous_authorizing_user == authorizing_user {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("Device code is already verified".to_owned())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse, OAuthError> {
|
||||
let TokenRequest { client_id, request } = request;
|
||||
|
||||
let Some(client_metadata) = self.get_client_metadata(&client_id).await else {
|
||||
return Err(OAuthError::new_static(ErrorCode::InvalidClient, "Invalid client ID"));
|
||||
};
|
||||
|
||||
if !client_metadata.grant_types.contains(&request.grant_type()) {
|
||||
return Err(OAuthError::invalid_grant("Client cannot request this grant type"));
|
||||
}
|
||||
|
||||
match request {
|
||||
| TokenRequestType::AuthorizationCode { code, redirect_uri, code_verifier } => {
|
||||
let mut pending_grants = self.pending_auth_code_grants.lock().await;
|
||||
| TokenRequest::AuthorizationCode {
|
||||
code,
|
||||
redirect_uri,
|
||||
client_id,
|
||||
code_verifier,
|
||||
} => {
|
||||
let mut pending_grants = self.pending_code_grants.lock().await;
|
||||
|
||||
let Some(pending_grant) = pending_grants
|
||||
.remove(&code)
|
||||
@@ -470,39 +305,7 @@ pub async fn issue_token(&self, request: TokenRequest) -> Result<TokenResponse,
|
||||
)
|
||||
.await
|
||||
},
|
||||
| TokenRequestType::DeviceCode { device_code } => {
|
||||
let mut pending_grants = self.pending_device_code_grants.lock().await;
|
||||
|
||||
let Some(pending_grant) = pending_grants
|
||||
.remove(&device_code)
|
||||
.filter(|grant| grant.is_valid_for(&client_id))
|
||||
else {
|
||||
return Err(OAuthError::new_static(
|
||||
ErrorCode::ExpiredToken,
|
||||
"Invalid device code",
|
||||
));
|
||||
};
|
||||
|
||||
match &pending_grant.state {
|
||||
| DeviceCodeGrantState::Unverified { .. } => {
|
||||
pending_grants.insert(device_code, pending_grant);
|
||||
|
||||
Err(OAuthError::new_static(
|
||||
ErrorCode::AuthorizationPending,
|
||||
"Authorization is pending",
|
||||
))
|
||||
},
|
||||
| DeviceCodeGrantState::Verified { authorizing_user } =>
|
||||
self.create_session(
|
||||
authorizing_user.to_owned(),
|
||||
pending_grant.requested_scopes,
|
||||
pending_grant.client_name,
|
||||
client_id,
|
||||
)
|
||||
.await,
|
||||
}
|
||||
},
|
||||
| TokenRequestType::RefreshToken { refresh_token } =>
|
||||
| TokenRequest::RefreshToken { client_id, refresh_token } =>
|
||||
self.refresh_session(client_id, refresh_token).await,
|
||||
}
|
||||
}
|
||||
@@ -540,7 +343,7 @@ async fn create_session(
|
||||
client_name: Option<String>,
|
||||
client_id: String,
|
||||
) -> 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 device_id = requested_scopes
|
||||
@@ -561,10 +364,11 @@ async fn create_session(
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
return Err(OAuthError::new_static(
|
||||
ErrorCode::InvalidScope,
|
||||
"A device with the supplied ID already exists for this user",
|
||||
));
|
||||
return Err(OAuthError {
|
||||
error: ErrorCode::InvalidScope,
|
||||
error_description: "A device with the supplied ID already exists for this user"
|
||||
.into(),
|
||||
});
|
||||
}
|
||||
|
||||
self.services
|
||||
@@ -572,8 +376,7 @@ async fn create_session(
|
||||
.create_device(
|
||||
&authorizing_user,
|
||||
device_id,
|
||||
&access_token,
|
||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
||||
Some(access_token.clone()),
|
||||
client_name,
|
||||
None,
|
||||
)
|
||||
@@ -609,7 +412,7 @@ async fn create_session(
|
||||
);
|
||||
|
||||
Ok(TokenResponse {
|
||||
access_token,
|
||||
access_token: access_token.into_token(),
|
||||
token_type: TokenType::Bearer,
|
||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
||||
scope: requested_scopes.iter().join(" "),
|
||||
@@ -645,7 +448,7 @@ async fn refresh_session(
|
||||
|
||||
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 scope = session_info.scopes.iter().join(" ");
|
||||
session_info
|
||||
@@ -657,8 +460,7 @@ async fn refresh_session(
|
||||
.set_token(
|
||||
&refresh_token_info.user_id,
|
||||
&refresh_token_info.device_id,
|
||||
&new_access_token,
|
||||
Some(Self::ACCESS_TOKEN_MAX_AGE),
|
||||
new_access_token.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("should be able to set token");
|
||||
@@ -675,7 +477,7 @@ async fn refresh_session(
|
||||
.raw_put(&new_refresh_token, Json(refresh_token_info));
|
||||
|
||||
Ok(TokenResponse {
|
||||
access_token: new_access_token,
|
||||
access_token: new_access_token.into_token(),
|
||||
token_type: TokenType::Bearer,
|
||||
expires_in: Self::ACCESS_TOKEN_MAX_AGE.as_secs(),
|
||||
scope,
|
||||
|
||||
@@ -6,8 +6,9 @@
|
||||
};
|
||||
use futures::future::ready;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, EventId, OwnedEventId, ServerName, api::error::ErrorKind,
|
||||
canonical_json::redact, events::StateEventType, room_version_rules::RoomVersionRules,
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, ServerName,
|
||||
api::error::ErrorKind, canonical_json::redact, events::StateEventType,
|
||||
room_version_rules::RoomVersionRules,
|
||||
};
|
||||
|
||||
use crate::rooms::{
|
||||
@@ -42,11 +43,27 @@ pub fn pdu_format_check_1(
|
||||
return Err!(Request(BadJson("PDU has too many auth events")));
|
||||
}
|
||||
|
||||
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")));
|
||||
// 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")?;
|
||||
|
||||
@@ -41,7 +41,6 @@ pub async fn set_dehydrated_device(&self, user_id: &UserId, request: Request) ->
|
||||
self.create_device(
|
||||
user_id,
|
||||
&request.device_id,
|
||||
"",
|
||||
None,
|
||||
request.initial_device_display_name.clone(),
|
||||
None,
|
||||
|
||||
+38
-31
@@ -11,20 +11,44 @@
|
||||
use futures::{Stream, StreamExt};
|
||||
use ruma::{
|
||||
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 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 {
|
||||
/// 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(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
token: &str,
|
||||
token_max_age: Option<Duration>,
|
||||
token: Option<DeviceToken>,
|
||||
initial_device_display_name: Option<String>,
|
||||
client_ip: Option<String>,
|
||||
) -> Result<()> {
|
||||
@@ -38,8 +62,12 @@ pub async fn create_device(
|
||||
|
||||
increment(&self.db.userid_devicelistversion, user_id.as_bytes());
|
||||
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.
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub async fn set_token(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
token: &str,
|
||||
token_max_age: Option<Duration>,
|
||||
DeviceToken { token, max_age }: DeviceToken,
|
||||
) -> Result<()> {
|
||||
let key = (user_id, device_id);
|
||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
@@ -136,7 +145,7 @@ pub async fn set_token(
|
||||
if self
|
||||
.services
|
||||
.appservice
|
||||
.find_from_token(token)
|
||||
.find_from_token(&token)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
@@ -153,10 +162,10 @@ pub async fn set_token(
|
||||
}
|
||||
|
||||
// Assign token to user device combination
|
||||
self.db.userdeviceid_token.put_raw(key, token);
|
||||
self.db.token_userdeviceid.raw_put(token, key);
|
||||
self.db.userdeviceid_token.put_raw(key, &token);
|
||||
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()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.expect("system time should not be before the epoch")
|
||||
@@ -244,8 +253,6 @@ pub async fn remove_to_device_events<Until>(
|
||||
self.db.todeviceid_events.del(key);
|
||||
})
|
||||
.await;
|
||||
|
||||
self.services.sync.wake(user_id).await;
|
||||
}
|
||||
|
||||
/// Updates device metadata and increments the device list version.
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
utils::{self},
|
||||
};
|
||||
use database::Map;
|
||||
pub use device::DeviceToken;
|
||||
pub use profile::ProfileFieldChange;
|
||||
use ruma::{UserId, api::error::ErrorKind, encryption::CrossSigningKey, serde::Raw};
|
||||
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 => {
|
||||
let Some(device_id) = query.device_id else {
|
||||
|
||||
@@ -65,17 +65,17 @@ pub(super) async fn for_local_user(services: &Services, user_id: &UserId) -> Sel
|
||||
}
|
||||
|
||||
pub(super) fn for_device(
|
||||
client_metadata: Option<&ClientMetadata>,
|
||||
oauth_metadata: Option<&ClientMetadata>,
|
||||
display_name: Option<&str>,
|
||||
) -> Self {
|
||||
let avatar_src = client_metadata
|
||||
let avatar_src = oauth_metadata
|
||||
.and_then(|metadata| metadata.logo_uri.as_ref())
|
||||
.map(|uri| uri.as_str().to_owned());
|
||||
|
||||
let avatar_type = if let Some(avatar_src) = avatar_src {
|
||||
AvatarType::Image(avatar_src)
|
||||
} else if let Some(initial) = display_name.and_then(|name| name.chars().next()) {
|
||||
if client_metadata.is_some() {
|
||||
if oauth_metadata.is_some() {
|
||||
AvatarType::Initial(initial)
|
||||
} else {
|
||||
AvatarType::Initial('❖')
|
||||
@@ -86,20 +86,6 @@ pub(super) fn for_device(
|
||||
|
||||
Self { avatar_type }
|
||||
}
|
||||
|
||||
pub(super) fn for_client(client_metadata: &ClientMetadata) -> Self {
|
||||
let avatar_type = if let Some(logo) = &client_metadata.logo_uri {
|
||||
AvatarType::Image(logo.to_string())
|
||||
} else if let Some(name) = &client_metadata.client_name
|
||||
&& let Some(char) = name.chars().next()
|
||||
{
|
||||
AvatarType::Initial(char)
|
||||
} else {
|
||||
AvatarType::Initial('?')
|
||||
};
|
||||
|
||||
Self { avatar_type }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
|
||||
+36
-108
@@ -4,12 +4,9 @@
|
||||
response::Redirect,
|
||||
routing::on,
|
||||
};
|
||||
use conduwuit_service::oauth::{
|
||||
client_metadata::ClientMetadata,
|
||||
grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery, Prompt},
|
||||
};
|
||||
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, Prompt};
|
||||
use ruma::OwnedUserId;
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::{
|
||||
ROUTE_PREFIX, WebError,
|
||||
@@ -17,7 +14,7 @@
|
||||
pages::{
|
||||
GET_POST, Result, TemplateContext,
|
||||
account::register::{RegisterQuery, RequestedRegistrationFlow},
|
||||
components::{Avatar, ClientScopes, UserCard},
|
||||
components::{Avatar, AvatarType, ClientScopes},
|
||||
},
|
||||
response,
|
||||
session::{LoginIntent, LoginQuery, LoginTarget, User},
|
||||
@@ -25,9 +22,7 @@
|
||||
};
|
||||
|
||||
pub(crate) fn build() -> Router<crate::State> {
|
||||
Router::new()
|
||||
.route("/authorization_code", on(GET_POST, route_authorization_code))
|
||||
.route("/device_code", on(GET_POST, route_device_code))
|
||||
Router::new().route("/authorization_code", on(GET_POST, route_authorization_code))
|
||||
}
|
||||
|
||||
template! {
|
||||
@@ -35,9 +30,12 @@ struct Grant use "grant.html.j2" {
|
||||
logout_query: String,
|
||||
user_id: OwnedUserId,
|
||||
user_avatar: Avatar,
|
||||
client_metadata: ClientMetadata,
|
||||
scopes: ClientScopes,
|
||||
device_code: Option<String>
|
||||
client_uri: Url,
|
||||
client_name: String,
|
||||
client_avatar: Avatar,
|
||||
policy_uri: Option<Url>,
|
||||
tos_uri: Option<Url>,
|
||||
scopes: ClientScopes
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,6 +102,27 @@ async fn route_authorization_code(
|
||||
|
||||
let scopes = query.scope.to_scopes().map_err(WebError::BadRequest)?;
|
||||
|
||||
let client_name = if let Some(name) = &client.client_name {
|
||||
name
|
||||
} else {
|
||||
"Unknown application"
|
||||
}
|
||||
.to_owned();
|
||||
|
||||
let client_avatar = {
|
||||
let avatar_type = if let Some(logo) = &client.logo_uri {
|
||||
AvatarType::Image(logo.to_string())
|
||||
} else if let Some(name) = &client.client_name
|
||||
&& let Some(char) = name.chars().next()
|
||||
{
|
||||
AvatarType::Initial(char)
|
||||
} else {
|
||||
AvatarType::Initial('?')
|
||||
};
|
||||
|
||||
Avatar { avatar_type }
|
||||
};
|
||||
|
||||
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
|
||||
|
||||
response!(Grant::new(
|
||||
@@ -116,102 +135,11 @@ async fn route_authorization_code(
|
||||
.unwrap(),
|
||||
user_id,
|
||||
user_avatar,
|
||||
client,
|
||||
client.client_uri.clone(),
|
||||
client_name,
|
||||
client_avatar,
|
||||
client.policy_uri.clone(),
|
||||
client.tos_uri.clone(),
|
||||
ClientScopes { scopes },
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(tag = "stage", rename_all = "snake_case")]
|
||||
enum DeviceCodeForm {
|
||||
Lookup {
|
||||
user_code: String,
|
||||
},
|
||||
Confirm {
|
||||
device_code: String,
|
||||
},
|
||||
}
|
||||
|
||||
template! {
|
||||
struct DeviceCodeGrant use "device_code_grant.html.j2" {
|
||||
user_card: UserCard,
|
||||
body: DeviceCodeGrantBody
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DeviceCodeGrantBody {
|
||||
Lookup {
|
||||
user_code_error: bool,
|
||||
},
|
||||
Success,
|
||||
}
|
||||
|
||||
async fn route_device_code(
|
||||
State(services): State<crate::State>,
|
||||
Extension(context): Extension<TemplateContext>,
|
||||
user: User<true>,
|
||||
Expect(Query(query)): Expect<Query<DeviceCodeVerifyQuery>>,
|
||||
PostForm(form): PostForm<DeviceCodeForm>,
|
||||
) -> Result {
|
||||
let user_id = if let Some(user) = user.into_session() {
|
||||
user.user_id
|
||||
} else {
|
||||
let next = LoginTarget::DeviceCode(query.clone());
|
||||
|
||||
let uri = format!(
|
||||
"{}/account/login?{}",
|
||||
ROUTE_PREFIX,
|
||||
serde_urlencoded::to_string(LoginQuery { next: Some(next), ..Default::default() })
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
return response!(Redirect::to(&uri));
|
||||
};
|
||||
|
||||
let user_card = UserCard::for_local_user(&services, user_id.clone()).await;
|
||||
|
||||
match (form, query.user_code.clone()) {
|
||||
| (None, Some(user_code)) | (Some(DeviceCodeForm::Lookup { user_code }), _) => {
|
||||
let Some(grant_info) = services.oauth.grant_info_for_user_code(&user_code).await
|
||||
else {
|
||||
return response!(DeviceCodeGrant::new(
|
||||
context,
|
||||
user_card,
|
||||
DeviceCodeGrantBody::Lookup { user_code_error: true }
|
||||
));
|
||||
};
|
||||
|
||||
let user_avatar = Avatar::for_local_user(&services, &user_id).await;
|
||||
|
||||
response!(Grant::new(
|
||||
context,
|
||||
serde_urlencoded::to_string(LoginQuery {
|
||||
next: Some(LoginTarget::DeviceCode(query)),
|
||||
intent: Some(LoginIntent::SwitchAccounts),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap(),
|
||||
user_id,
|
||||
user_avatar,
|
||||
grant_info.client_metadata,
|
||||
ClientScopes { scopes: grant_info.requested_scopes },
|
||||
Some(grant_info.device_code),
|
||||
))
|
||||
},
|
||||
| (Some(DeviceCodeForm::Confirm { device_code }), _) => {
|
||||
services
|
||||
.oauth
|
||||
.validate_device_code(user_id, &device_code)
|
||||
.await
|
||||
.map_err(WebError::BadRequest)?;
|
||||
|
||||
response!(DeviceCodeGrant::new(context, user_card, DeviceCodeGrantBody::Success))
|
||||
},
|
||||
| (None, None) =>
|
||||
response!(DeviceCodeGrant::new(context, user_card, DeviceCodeGrantBody::Lookup {
|
||||
user_code_error: false
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,17 +20,3 @@
|
||||
font-style: italic;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.user-code {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 200%;
|
||||
padding: 0.2em;
|
||||
width: 6em !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
{% extends "_layout.html.j2" %}
|
||||
|
||||
{%- block head -%}
|
||||
<link rel="stylesheet" href="{{ crate::ROUTE_PREFIX }}/resources/grant.css">
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block title -%}
|
||||
Authorize client
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
<div class="panel narrow">
|
||||
<h1>Authorize device</h1>
|
||||
{{ user_card }}
|
||||
{% match body %}
|
||||
{% when DeviceCodeGrantBody::Lookup { user_code_error } %}
|
||||
<p>
|
||||
Enter the code displayed on your device, or in the app you're signing into.
|
||||
</p>
|
||||
<p>
|
||||
<em class="negative">Never enter a code given to you by someone else.</em>
|
||||
Your homeserver administrator will never ask you to enter a code on this page.
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<p>
|
||||
<div class="user-code">
|
||||
<label for="user_code">Validation code</label>
|
||||
<input
|
||||
type="text"
|
||||
id="user_code"
|
||||
name="user_code"
|
||||
maxlength="6"
|
||||
spellcheck="false"
|
||||
pattern="\d{6}"
|
||||
title="Enter the six-digit validation code"
|
||||
>
|
||||
</div>
|
||||
{% if user_code_error %}
|
||||
<small class="error">Invalid code</small>
|
||||
{% endif %}
|
||||
</p>
|
||||
<input type="hidden" name="stage" value="lookup">
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
{% when DeviceCodeGrantBody::Success %}
|
||||
<p>Success! 🎉 You can close this tab and return to your device.</p>
|
||||
{% endmatch %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -9,7 +9,6 @@ Authorize client
|
||||
{%- endblock -%}
|
||||
|
||||
{%- block content -%}
|
||||
{% let client_name = client_metadata.client_name.as_deref().unwrap_or("Unknown application") %}
|
||||
<div class="panel narrow">
|
||||
<h1>Authorize {{ client_name }}</h1>
|
||||
<div class="avatars">
|
||||
@@ -19,17 +18,17 @@ Authorize client
|
||||
<div class="separator" aria-hidden="true">
|
||||
⇄
|
||||
</div>
|
||||
{{ Avatar::for_client(client_metadata) }}
|
||||
{{ client_avatar }}
|
||||
</div>
|
||||
<div class="identity">
|
||||
Signed in as <code>{{ user_id }}</code>. <a href="{{ crate::ROUTE_PREFIX }}/account/logout?{{ logout_query }}">Switch accounts</a>
|
||||
</div>
|
||||
<p>
|
||||
<b>{{ client_name }}</b> (<a href="{{ client_metadata.client_uri }}">{{ client_metadata.client_uri.domain().unwrap() }}</a>) would like
|
||||
<b>{{ client_name }}</b> (<a href="{{ client_uri }}">{{ client_uri.domain().unwrap() }}</a>) would like
|
||||
your permission to:
|
||||
{{ scopes }}
|
||||
</p>
|
||||
{% match (&client_metadata.policy_uri, &client_metadata.tos_uri) %}
|
||||
{% match (&policy_uri, &tos_uri) %}
|
||||
{% when (Some(policy_uri), Some(tos_uri)) %}
|
||||
<p>
|
||||
{{ client_name }}'s <a href="{{ policy_uri }}">policies</a>
|
||||
@@ -50,10 +49,6 @@ Authorize client
|
||||
{% endmatch %}
|
||||
|
||||
<form method="post">
|
||||
<input type="hidden" name="stage" value="confirm">
|
||||
{% if let Some(device_code) = device_code %}
|
||||
<input type="hidden" name="device_code" value="{{ device_code }}">
|
||||
{% endif %}
|
||||
<button type="submit">Continue</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
http::request::Parts,
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
};
|
||||
use conduwuit_service::oauth::grant::{AuthorizationCodeQuery, DeviceCodeVerifyQuery};
|
||||
use conduwuit_service::oauth::grant::AuthorizationCodeQuery;
|
||||
use ruma::{OwnedUserId, UserId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tower_sessions::Session;
|
||||
@@ -38,7 +38,6 @@ pub(crate) enum LoginTarget {
|
||||
ChangeEmail,
|
||||
CrossSigningReset,
|
||||
Deactivate,
|
||||
DeviceCode(DeviceCodeVerifyQuery),
|
||||
DeviceInfo(DevicePath),
|
||||
RemoveDevice(DevicePath),
|
||||
}
|
||||
@@ -60,9 +59,6 @@ pub(crate) fn target_path(&self) -> String {
|
||||
| Self::ChangeEmail => "account/email/change/".into(),
|
||||
| Self::CrossSigningReset => "account/cross_signing_reset".into(),
|
||||
| Self::Deactivate => "account/deactivate".into(),
|
||||
| Self::DeviceCode(code) =>
|
||||
format!("oauth2/grant/device_code?{}", serde_urlencoded::to_string(code).unwrap())
|
||||
.into(),
|
||||
| Self::DeviceInfo(path) => format!("account/device/{}/", path.device).into(),
|
||||
| Self::RemoveDevice(path) => format!("account/device/{}/remove", path.device).into(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user