Compare commits

..

21 Commits

Author SHA1 Message Date
Ginger eecc472258 fix(ci): Use the version from rust-toolchain.toml when restoring caches 2025-11-21 11:07:44 -05:00
Ginger 4a9bea5764 fix(docs): Fix git repository icon name 2025-11-21 10:46:15 -05:00
Ginger 08fd87c7de chore: Kick forgejo 2025-11-21 15:19:39 +00:00
Ginger ac6d639660 fix(docs): Remove unused multilingual option in mdBook config 2025-11-21 15:19:39 +00:00
Ginger 0958660eb5 ci: Do not run release-image workflow on pull requests 2025-11-21 14:52:41 +00:00
Renovate Bot 57c3290f02 chore(deps): update rust-patch-updates 2025-11-21 14:25:55 +00:00
nex 6794ea565f fix(tests): Fix new syntax error in debug.rc
fixes #1180
2025-11-18 18:46:35 +00:00
Lilith 38080275d4 build(Cargo.toml): updated the versions automatically 2025-11-17 12:51:49 +00:00
Lilith 1138218878 build(Cargo.toml): resolve the fact that we have v0.5.0-rc.8.1 despite 0.5.0-rc.8 in toml 2025-11-17 12:51:49 +00:00
Renovate Bot c0f1d8eab6 chore(deps): update ghcr.io/renovatebot/renovate docker tag to v42 2025-11-17 00:33:11 +00:00
nex 192f78887a chore: Add rc.8.1 release announcement 2025-11-16 22:08:09 +00:00
timedout def8816c02 style: Fix clippy failures from 9e73146 2025-11-16 19:55:28 +00:00
timedout 9e73146b19 fix: Restore continuwuity's remembering capabilities 2025-11-14 14:57:49 +00:00
Renovate Bot 19d792e4eb chore(deps): update rust crate ctor to 0.6.0 2025-11-14 14:03:57 +00:00
Renovate Bot 2a977f019f chore(deps): update pre-commit hook crate-ci/typos to v1.39.2 2025-11-14 05:01:41 +00:00
ginger 76ea4dfa29 Update README.md 2025-11-13 19:55:12 +00:00
Renovate Bot 2ec771c84d chore(deps): update rust crate bytesize to v2.2.0 2025-11-13 05:03:54 +00:00
timedout 9375e81974 fix(1163): Resolve algorithm misinterpretations 2025-11-13 03:33:47 +00:00
Renovate Bot f22f35d27b chore(deps): update rust crate syn to v2.0.110 2025-11-12 05:03:14 +00:00
Renovate Bot d5c7d80709 chore(deps): update dependency cargo-bins/cargo-binstall to v1.15.11 2025-11-11 23:11:19 +00:00
Jade Ellis 1899d8bb00 ci: Mirror to Docker Hub 2025-11-11 23:11:09 +00:00
48 changed files with 313 additions and 2035 deletions
+76 -68
View File
@@ -17,7 +17,7 @@ inputs:
required: false
default: ''
rust-version:
description: 'Rust version to install (e.g. nightly). Defaults to 1.87.0'
description: 'Rust version to install (e.g. nightly). Defaults to the version specified in rust-toolchain.toml'
required: false
default: ''
sccache-cache-limit:
@@ -59,9 +59,20 @@ runs:
mkdir -p "${{ github.workspace }}/target"
mkdir -p "${{ github.workspace }}/.rustup"
- name: Start cache restore group
- name: Start registry/toolchain restore group
shell: bash
run: echo "::group::📦 Restoring caches (registry, toolchain, build artifacts)"
run: echo "::group::📦 Restoring registry and toolchain caches"
- name: Cache toolchain binaries
id: toolchain-cache
uses: actions/cache@v4
with:
path: |
.cargo/bin
.rustup/toolchains
.rustup/update-hashes
# Shared toolchain cache across all Rust versions
key: continuwuity-toolchain-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}
- name: Cache Cargo registry and git
id: registry-cache
@@ -77,58 +88,13 @@ runs:
restore-keys: |
continuwuity-cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-
- name: Cache toolchain binaries
id: toolchain-cache
uses: actions/cache@v4
with:
path: |
.cargo/bin
.rustup/toolchains
.rustup/update-hashes
# Shared toolchain cache across all Rust versions
key: continuwuity-toolchain-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Cache dependencies
id: deps-cache
uses: actions/cache@v4
with:
path: |
target/**/.fingerprint
target/**/deps
target/**/*.d
target/**/.cargo-lock
target/**/CACHEDIR.TAG
target/**/.rustc_info.json
/timelord/
# Dependencies cache - based on Cargo.lock, survives source code changes
key: >-
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
restore-keys: |
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
- name: Cache incremental compilation
id: incremental-cache
uses: actions/cache@v4
with:
path: |
target/**/incremental
# Incremental cache - based on source code changes
key: >-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
restore-keys: |
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ inputs.rust-version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
- name: End cache restore group
- name: End registry/toolchain restore group
shell: bash
run: echo "::endgroup::"
- name: Setup Rust toolchain
shell: bash
id: rust-setup
run: |
# Install rustup if not already cached
if ! command -v rustup &> /dev/null; then
@@ -156,8 +122,68 @@ runs:
echo "::group::📦 Setting up Rust from rust-toolchain.toml"
rustup show
fi
RUST_VERSION=$(rustc --version | cut -d' ' -f2)
echo "version=$RUST_VERSION" >> $GITHUB_OUTPUT
echo "::endgroup::"
- name: Install Rust components
if: inputs.rust-components != ''
shell: bash
run: |
echo "📦 Installing components: ${{ inputs.rust-components }}"
rustup component add ${{ inputs.rust-components }}
- name: Install Rust target
if: inputs.rust-target != ''
shell: bash
run: |
echo "📦 Installing target: ${{ inputs.rust-target }}"
rustup target add ${{ inputs.rust-target }}
- name: Start build cache restore group
shell: bash
run: echo "::group::📦 Restoring build cache"
- name: Setup sccache
uses: https://git.tomfos.tr/tom/sccache-action@v1
- name: Cache dependencies
id: deps-cache
uses: actions/cache@v4
with:
path: |
target/**/.fingerprint
target/**/deps
target/**/*.d
target/**/.cargo-lock
target/**/CACHEDIR.TAG
target/**/.rustc_info.json
/timelord/
# Dependencies cache - based on Cargo.lock, survives source code changes
key: >-
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}
restore-keys: |
continuwuity-deps-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
- name: Cache incremental compilation
id: incremental-cache
uses: actions/cache@v4
with:
path: |
target/**/incremental
# Incremental cache - based on source code changes
key: >-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-${{ hashFiles('**/*.rs', '**/Cargo.toml') }}
restore-keys: |
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-${{ hashFiles('rust-toolchain.toml', '**/Cargo.lock') }}-
continuwuity-incremental-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-${{ steps.rust-setup.outputs.version }}${{ inputs.cache-key-suffix && format('-{0}', inputs.cache-key-suffix) || '' }}-
- name: End build cache restore group
shell: bash
run: echo "::endgroup::"
- name: Configure PATH and install tools
shell: bash
env:
@@ -211,27 +237,9 @@ runs:
echo "CARGO_INCREMENTAL_GC_THRESHOLD=5" >> $GITHUB_ENV
fi
- name: Install Rust components
if: inputs.rust-components != ''
shell: bash
run: |
echo "📦 Installing components: ${{ inputs.rust-components }}"
rustup component add ${{ inputs.rust-components }}
- name: Install Rust target
if: inputs.rust-target != ''
shell: bash
run: |
echo "📦 Installing target: ${{ inputs.rust-target }}"
rustup target add ${{ inputs.rust-target }}
- name: Output version and summary
id: rust-setup
shell: bash
run: |
RUST_VERSION=$(rustc --version | cut -d' ' -f2)
echo "version=$RUST_VERSION" >> $GITHUB_OUTPUT
echo "📋 Setup complete:"
echo " Rust: $(rustc --version)"
echo " Cargo: $(cargo --version)"
+7
View File
@@ -46,6 +46,9 @@ creds:
- registry: ghcr.io
user: "{{env \"GH_PACKAGES_USER\"}}"
pass: "{{env \"GH_PACKAGES_TOKEN\"}}"
- registry: docker.io
user: "{{env \"DOCKER_MIRROR_USER\"}}"
pass: "{{env \"DOCKER_MIRROR_TOKEN\"}}"
# Global defaults
defaults:
@@ -67,3 +70,7 @@ sync:
target: ghcr.io/continuwuity/continuwuity
type: repository
<<: *tags-main
- source: *source
target: docker.io/jadedblueeyes/continuwuity
type: repository
<<: *tags-main
+2
View File
@@ -34,6 +34,8 @@ jobs:
N7574_GIT_TOKEN: ${{ secrets.N7574_GIT_TOKEN }}
GH_PACKAGES_USER: ${{ vars.GH_PACKAGES_USER }}
GH_PACKAGES_TOKEN: ${{ secrets.GH_PACKAGES_TOKEN }}
DOCKER_MIRROR_USER: ${{ vars.DOCKER_MIRROR_USER }}
DOCKER_MIRROR_TOKEN: ${{ secrets.DOCKER_MIRROR_TOKEN }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
-9
View File
@@ -3,15 +3,6 @@ concurrency:
group: "release-image-${{ github.ref }}"
on:
pull_request:
paths-ignore:
- "*.md"
- "**/*.md"
- ".gitlab-ci.yml"
- ".gitignore"
- "renovate.json"
- "pkg/**"
- "docs/**"
push:
branches:
- main
+1 -1
View File
@@ -43,7 +43,7 @@ jobs:
name: Renovate
runs-on: ubuntu-latest
container:
image: ghcr.io/renovatebot/renovate:41.146.4@sha256:bb70194b7405faf10a6f279b60caa10403a440ba37d158c5a4ef0ae7b67a0f92
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
options: --tmpfs /tmp:exec
steps:
- name: Checkout
-3
View File
@@ -7,6 +7,3 @@ f419c64aca300a338096b4e0db4c73ace54f23d0
5998a0d883d31b866f7c8c46433a8857eae51a89
# trailing whitespace and newlines
46c193e74b2ce86c48ce802333a0aabce37fd6e9
# Formatting PRs
fd972f114293ea1be9633b750a703edd661e970d
+1 -1
View File
@@ -23,7 +23,7 @@ repos:
- id: check-added-large-files
- repo: https://github.com/crate-ci/typos
rev: v1.39.0
rev: v1.39.2
hooks:
- id: typos
- id: typos
Generated
+173 -280
View File
@@ -17,6 +17,19 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "ahash"
version = "0.8.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
dependencies = [
"cfg-if",
"getrandom 0.3.4",
"once_cell",
"version_check",
"zerocopy",
]
[[package]]
name = "aho-corasick"
version = "1.1.4"
@@ -26,6 +39,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "aligned"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923"
dependencies = [
"as-slice",
]
[[package]]
name = "aligned-vec"
version = "0.6.4"
@@ -50,15 +72,6 @@ dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
@@ -156,12 +169,6 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236"
[[package]]
name = "arrayref"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
[[package]]
name = "arrayvec"
version = "0.7.6"
@@ -171,6 +178,15 @@ dependencies = [
"serde",
]
[[package]]
name = "as-slice"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "516b6b4f0e40d50dcda9365d53964ec74560ad4284da2e7fc97122cd83174516"
dependencies = [
"stable_deref_trait",
]
[[package]]
name = "as_variant"
version = "1.3.0"
@@ -320,6 +336,26 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "av-scenechange"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f321d77c20e19b92c39e7471cf986812cbb46659d2af674adc4331ef3f18394"
dependencies = [
"aligned",
"anyhow",
"arg_enum_proc_macro",
"arrayvec",
"log",
"num-rational",
"num-traits",
"pastey",
"rayon",
"thiserror 2.0.17",
"v_frame",
"y4m",
]
[[package]]
name = "av1-grain"
version = "0.2.4"
@@ -499,9 +535,9 @@ dependencies = [
[[package]]
name = "axum-server"
version = "0.7.2"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab"
checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
dependencies = [
"arc-swap",
"bytes",
@@ -614,9 +650,12 @@ checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
[[package]]
name = "bitstream-io"
version = "2.6.0"
version = "4.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
dependencies = [
"core2",
]
[[package]]
name = "blake2"
@@ -627,17 +666,6 @@ dependencies = [
"digest",
]
[[package]]
name = "blake2b_simd"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]]
name = "block-buffer"
version = "0.10.4"
@@ -677,12 +705,6 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "built"
version = "0.7.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56ed6191a7e78c36abdb16ab65341eefd73d64d303fffccdbb00d51e4205967b"
[[package]]
name = "built"
version = "0.8.0"
@@ -721,9 +743,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "bytesize"
version = "2.1.0"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f"
checksum = "c99fa31e08a43eaa5913ef68d7e01c37a2bdce6ed648168239ad33b7d30a9cd8"
[[package]]
name = "bzip2-sys"
@@ -766,16 +788,6 @@ dependencies = [
"nom",
]
[[package]]
name = "cfg-expr"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
dependencies = [
"smallvec 1.15.1",
"target-lexicon",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -803,9 +815,7 @@ version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"num-traits",
"windows-link 0.2.1",
]
[[package]]
@@ -821,9 +831,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.51"
version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
dependencies = [
"clap_builder",
"clap_derive",
@@ -840,9 +850,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.51"
version = "4.5.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
dependencies = [
"anstream",
"anstyle",
@@ -930,7 +940,7 @@ dependencies = [
[[package]]
name = "conduwuit"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"conduwuit_admin",
@@ -962,7 +972,7 @@ dependencies = [
[[package]]
name = "conduwuit_admin"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"conduwuit_api",
@@ -984,7 +994,7 @@ dependencies = [
[[package]]
name = "conduwuit_api"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"async-trait",
"axum 0.7.9",
@@ -994,7 +1004,6 @@ dependencies = [
"bytes",
"conduwuit_core",
"conduwuit_service",
"conduwuit_web",
"const-str",
"ctor",
"futures",
@@ -1005,8 +1014,6 @@ dependencies = [
"ipaddress",
"itertools 0.14.0",
"log",
"oxide-auth",
"percent-encoding",
"rand 0.8.5",
"reqwest",
"ruma",
@@ -1020,14 +1027,14 @@ dependencies = [
[[package]]
name = "conduwuit_build_metadata"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"built 0.8.0",
"built",
]
[[package]]
name = "conduwuit_core"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"argon2",
"arrayvec",
@@ -1088,7 +1095,7 @@ dependencies = [
[[package]]
name = "conduwuit_database"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"async-channel",
"conduwuit_core",
@@ -1107,7 +1114,7 @@ dependencies = [
[[package]]
name = "conduwuit_macros"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"itertools 0.14.0",
"proc-macro2",
@@ -1117,7 +1124,7 @@ dependencies = [
[[package]]
name = "conduwuit_router"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"axum 0.7.9",
"axum-client-ip",
@@ -1152,7 +1159,7 @@ dependencies = [
[[package]]
name = "conduwuit_service"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"async-trait",
"base64 0.22.1",
@@ -1173,8 +1180,6 @@ dependencies = [
"log",
"loole",
"lru-cache",
"once_cell",
"oxide-auth",
"rand 0.8.5",
"recaptcha-verify",
"regex",
@@ -1195,22 +1200,16 @@ dependencies = [
[[package]]
name = "conduwuit_web"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"askama",
"async-trait",
"axum 0.7.9",
"conduwuit_build_metadata",
"conduwuit_core",
"conduwuit_service",
"futures",
"oxide-auth",
"percent-encoding",
"rand 0.8.5",
"serde",
"thiserror 2.0.17",
"tracing",
"url",
]
[[package]]
@@ -1274,12 +1273,6 @@ dependencies = [
"typewit",
]
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "convert_case"
version = "0.7.1"
@@ -1314,6 +1307,15 @@ version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "core2"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505"
dependencies = [
"memchr",
]
[[package]]
name = "core_affinity"
version = "0.8.1"
@@ -1476,9 +1478,9 @@ dependencies = [
[[package]]
name = "ctor"
version = "0.5.0"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
checksum = "3ffc71fcdcdb40d6f087edddf7f8f1f8f79e6cf922f555a9ee8779752d4819bd"
dependencies = [
"ctor-proc-macro",
"dtor",
@@ -1486,9 +1488,9 @@ dependencies = [
[[package]]
name = "ctor-proc-macro"
version = "0.0.6"
version = "0.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
[[package]]
name = "curve25519-dalek"
@@ -1685,6 +1687,24 @@ dependencies = [
"serde",
]
[[package]]
name = "encoding_rs"
version = "0.8.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
dependencies = [
"cfg-if",
]
[[package]]
name = "encoding_rs_io"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
dependencies = [
"encoding_rs",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
@@ -1730,7 +1750,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -1755,9 +1775,9 @@ dependencies = [
[[package]]
name = "exr"
version = "1.73.0"
version = "1.74.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
dependencies = [
"bit_field",
"half",
@@ -2036,9 +2056,9 @@ dependencies = [
[[package]]
name = "gif"
version = "0.13.3"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162"
dependencies = [
"color_quant",
"weezl",
@@ -2385,36 +2405,12 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2 0.5.10",
"socket2 0.6.1",
"tokio",
"tower-service",
"tracing",
]
[[package]]
name = "iana-time-zone"
version = "0.1.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.0.0"
@@ -2524,9 +2520,9 @@ dependencies = [
[[package]]
name = "image"
version = "0.25.8"
version = "0.25.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
dependencies = [
"bytemuck",
"byteorder-lite",
@@ -2542,8 +2538,8 @@ dependencies = [
"rayon",
"rgb",
"tiff",
"zune-core",
"zune-jpeg",
"zune-core 0.5.0",
"zune-jpeg 0.5.5",
]
[[package]]
@@ -3014,9 +3010,9 @@ dependencies = [
[[package]]
name = "minicbor-serde"
version = "0.6.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "546cc904f35809921fa57016a84c97e68d9d27c012e87b9dadc28c233705f783"
checksum = "80047f75e28e3b38f6ab2ec3c2c7669f6b411fa6f8424e1a90a3fd784b19a3f4"
dependencies = [
"minicbor",
"serde",
@@ -3368,27 +3364,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "oxide-auth"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c136ac668d12ba0b5b8ce159b95c7600fda826dc599a1e1916f8461c0d16a84"
dependencies = [
"base64 0.21.7",
"chrono",
"hmac",
"once_cell",
"rand 0.8.5",
"rmp-serde",
"rust-argon2",
"serde",
"serde_derive",
"serde_json",
"sha2",
"subtle",
"url",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -3437,6 +3412,12 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pastey"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
[[package]]
name = "pear"
version = "0.2.9"
@@ -3701,7 +3682,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
dependencies = [
"anyhow",
"itertools 0.12.1",
"itertools 0.14.0",
"proc-macro2",
"quote",
"syn",
@@ -3780,7 +3761,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls",
"socket2 0.5.10",
"socket2 0.6.1",
"thiserror 2.0.17",
"tokio",
"tracing",
@@ -3817,7 +3798,7 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2 0.5.10",
"socket2 0.6.1",
"tracing",
"windows-sys 0.52.0",
]
@@ -3898,19 +3879,21 @@ dependencies = [
[[package]]
name = "rav1e"
version = "0.7.1"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
dependencies = [
"aligned-vec",
"arbitrary",
"arg_enum_proc_macro",
"arrayvec",
"av-scenechange",
"av1-grain",
"bitstream-io",
"built 0.7.7",
"built",
"cfg-if",
"interpolate_name",
"itertools 0.12.1",
"itertools 0.14.0",
"libc",
"libfuzzer-sys",
"log",
@@ -3919,23 +3902,21 @@ dependencies = [
"noop_proc_macro",
"num-derive",
"num-traits",
"once_cell",
"paste",
"profiling",
"rand 0.8.5",
"rand_chacha 0.3.1",
"rand 0.9.2",
"rand_chacha 0.9.0",
"simd_helpers",
"system-deps",
"thiserror 1.0.69",
"thiserror 2.0.17",
"v_frame",
"wasm-bindgen",
]
[[package]]
name = "ravif"
version = "0.11.20"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b"
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
dependencies = [
"avif-serialize",
"imgref",
@@ -4087,28 +4068,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rmp"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "228ed7c16fa39782c3b3468e974aec2795e9089153cd08ee2e9aefb3613334c4"
dependencies = [
"byteorder",
"num-traits",
"paste",
]
[[package]]
name = "rmp-serde"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52e599a477cf9840e92f2cde9a7189e67b42c57532749bf90aea6ec10facd4db"
dependencies = [
"byteorder",
"rmp",
"serde",
]
[[package]]
name = "roff"
version = "0.2.2"
@@ -4311,17 +4270,6 @@ dependencies = [
"thiserror 2.0.17",
]
[[package]]
name = "rust-argon2"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d9848531d60c9cbbcf9d166c885316c24bc0e2a9d3eba0956bb6cbbd79bc6e8"
dependencies = [
"base64 0.21.7",
"blake2b_simd",
"constant_time_eq",
]
[[package]]
name = "rust-librocksdb-sys"
version = "0.39.0+10.5.1"
@@ -4389,7 +4337,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys",
"windows-sys 0.52.0",
"windows-sys 0.61.2",
]
[[package]]
@@ -4695,18 +4643,20 @@ dependencies = [
[[package]]
name = "serde-saphyr"
version = "0.0.7"
version = "0.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd76af9505b2498740576f95f60b3b4e2c469b5b677a8d2dd1d2da18b58193de"
checksum = "c0916ccf524f1ccec1b3c02193c9e3d2e167aee9b6b294829dce6f4411332155"
dependencies = [
"ahash",
"base64 0.22.1",
"encoding_rs_io",
"nohash-hasher",
"num-traits",
"ryu",
"saphyr-parser",
"serde",
"serde_json",
"smallvec 2.0.0-alpha.11",
"smallvec 2.0.0-alpha.12",
]
[[package]]
@@ -4930,9 +4880,9 @@ dependencies = [
[[package]]
name = "smallvec"
version = "2.0.0-alpha.11"
version = "2.0.0-alpha.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570"
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
[[package]]
name = "socket2"
@@ -5024,9 +4974,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "2.0.109"
version = "2.0.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
dependencies = [
"proc-macro2",
"quote",
@@ -5053,31 +5003,12 @@ dependencies = [
"syn",
]
[[package]]
name = "system-deps"
version = "6.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml 0.8.23",
"version-compare",
]
[[package]]
name = "tagptr"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417"
[[package]]
name = "target-lexicon"
version = "0.12.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]]
name = "tendril"
version = "0.4.3"
@@ -5175,7 +5106,7 @@ dependencies = [
"half",
"quick-error",
"weezl",
"zune-jpeg",
"zune-jpeg 0.4.21",
]
[[package]]
@@ -5800,12 +5731,6 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version-compare"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "852e951cb7832cb45cb1169900d19760cfa39b82bc0ea9c0e5a14ae88411c98b"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -5989,41 +5914,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link 0.1.3",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.1.3"
@@ -6036,24 +5926,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-strings"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
dependencies = [
"windows-link 0.1.3",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -6346,7 +6218,7 @@ dependencies = [
[[package]]
name = "xtask"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"clap",
"serde",
@@ -6355,7 +6227,7 @@ dependencies = [
[[package]]
name = "xtask-generate-commands"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
dependencies = [
"clap-markdown",
"clap_builder",
@@ -6364,6 +6236,12 @@ dependencies = [
"conduwuit_admin",
]
[[package]]
name = "y4m"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a5a4b21e1a62b67a2970e6831bc091d7b87e119e7f9791aef9702e3bef04448"
[[package]]
name = "yansi"
version = "1.0.1"
@@ -6508,6 +6386,12 @@ version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a"
[[package]]
name = "zune-core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "111f7d9820f05fd715df3144e254d6fc02ee4088b0644c0ffd0efc9e6d9d2773"
[[package]]
name = "zune-inflate"
version = "0.2.54"
@@ -6523,5 +6407,14 @@ version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
dependencies = [
"zune-core",
"zune-core 0.4.12",
]
[[package]]
name = "zune-jpeg"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fb7703e32e9a07fb3f757360338b3a567a5054f21b5f52a666752e333d58e"
dependencies = [
"zune-core 0.5.0",
]
+3 -15
View File
@@ -21,7 +21,7 @@ license = "Apache-2.0"
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.0-rc.8"
version = "0.5.0-rc.8.1"
[workspace.metadata.crane]
name = "conduwuit"
@@ -48,7 +48,7 @@ features = ["ffi", "std", "union"]
version = "0.7.0"
[workspace.dependencies.ctor]
version = "0.5.0"
version = "0.6.0"
[workspace.dependencies.cargo_toml]
version = "0.22"
@@ -103,9 +103,6 @@ features = [
"matched-path",
"tokio",
"tracing",
"query",
# Needed for debug_handler.
#"macros",
]
[workspace.dependencies.axum-extra]
@@ -170,7 +167,7 @@ features = ["raw_value"]
# Used for appservice registration files
[workspace.dependencies.serde-saphyr]
version = "0.0.7"
version = "0.0.8"
# Used to load forbidden room/user regex from config
[workspace.dependencies.serde_regex]
@@ -372,7 +369,6 @@ features = [
"unstable-msc2666",
"unstable-msc2867",
"unstable-msc2870",
"unstable-msc2965",
"unstable-msc3026",
"unstable-msc3061",
"unstable-msc3245",
@@ -561,14 +557,6 @@ features = ["sync", "tls-rustls", "rustls-provider"]
[workspace.dependencies.resolv-conf]
version = "0.7.5"
[workspace.dependencies.oxide-auth]
version = "0.6.1"
[workspace.dependencies.once_cell]
version = "1.21.3"
[workspace.dependencies.percent-encoding]
version = "2.3.1"
#
# Patches
#
+1 -1
View File
@@ -11,7 +11,7 @@ ## A community-driven [Matrix](https://matrix.org/) homeserver in Rust
<!-- ANCHOR_END: catchphrase -->
[continuwuity] is a Matrix homeserver written in Rust.
It's a community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
It's the official community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
<!-- ANCHOR: body -->
+1 -2
View File
@@ -4,7 +4,6 @@ description = "continuwuity is a community continuation of the conduwuit Matrix
language = "en"
authors = ["The continuwuity Community"]
text-direction = "ltr"
multilingual = false
src = "docs"
[build]
@@ -18,7 +17,7 @@ edition = "2024"
[output.html]
edit-url-template = "https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/{path}"
git-repository-url = "https://forgejo.ellis.link/continuwuation/continuwuity"
git-repository-icon = "fa-git-alt"
git-repository-icon = "fab-git-alt"
[output.html.search]
limit-results = 15
+1 -20
View File
@@ -23,8 +23,7 @@
# See the docs for reverse proxying and delegation:
# https://continuwuity.org/deploying/generic.html#setting-up-the-reverse-proxy
#
# Also see the `[global.auth]` and `[global.well_known]` config sections
# at the very bottom.
# Also see the `[global.well_known]` config section at the very bottom.
#
# Examples of delegation:
# - https://puppygock.gay/.well-known/matrix/server
@@ -1755,24 +1754,6 @@
#
#dual_protocol = false
[global.auth]
# Use this homeserver as the OIDC authentication reference. It will
# advertise itself as the OIDC authentication issuer to new clients,
# and use the internal user database to answer on the advertised
# endpoints. Note that the legacy Matrix authentication still will be
# reachable.
# Unset by default.
#
#enable_oidc_login =
# Whether this homeserver should provide users with an account management
# interface. Only used if `enable_oidc_login` is set. Note that the
# endpoint is unimplemented at the moment.
# Unset by default.
#
#enable_oidc_account_management =
[global.well_known]
# The server URL that the client well-known file will serve. This should
+1 -1
View File
@@ -48,7 +48,7 @@ EOF
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.15.10
ENV BINSTALL_VERSION=1.15.11
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+1 -1
View File
@@ -18,7 +18,7 @@ RUN --mount=type=cache,target=/etc/apk/cache apk add \
# Developer tool versions
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
ENV BINSTALL_VERSION=1.15.10
ENV BINSTALL_VERSION=1.15.11
# renovate: datasource=github-releases depName=psastras/sbom-rs
ENV CARGO_SBOM_VERSION=0.9.1
# renovate: datasource=crate depName=lddtree
+2
View File
@@ -17,3 +17,5 @@ ## systemd unit file
```
{{#include ../../pkg/conduwuit.service}}
```
</details>
+4
View File
@@ -8,6 +8,10 @@
{
"id": 3,
"message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner."
},
{
"id": 5,
"message": "It's a bird! It's a plane! No, it's 0.5.0-rc.8.1!\n\nThis is a minor bugfix update to the rc8 which backports some important fixes from the latest main branch. If you still haven't updated to rc8, you should skip to main. Otherwise, you should upgrade to this bugfix release as soon as possible.\n\nBugfixes backported to this version:\n\n- Resolved several issues with state resolution v2.1 (room version 12)\n- Fixed issues with the `restricted` and `knock_restricted` join rules that would sometimes incorrectly disallow a valid join\n- Fixed the automatic support contact listing being a no-op\n- Fixed upgrading pre-v12 rooms to v12 rooms\n- Fixed policy servers sending the incorrect JSON objects (resulted in false positives)\n- Fixed debug build panic during MSC4133 migration\n\nIt is recommended, if you can and are comfortable with doing so, following updates to the main branch - we're in the run up to the full 0.5.0 release, and more and more bugfixes and new features are being pushed constantly. Please don't forget to join [#announcements:continuwuity.org](https://matrix.to/#/#announcements:continuwuity.org) to receive this news faster and be alerted to other important updates!"
}
]
}
-3
View File
@@ -94,9 +94,6 @@ sha1.workspace = true
tokio.workspace = true
tracing.workspace = true
ctor.workspace = true
oxide-auth.workspace = true
conduwuit-web.workspace = true
percent-encoding.workspace = true
[lints]
workspace = true
+1 -3
View File
@@ -75,9 +75,7 @@ pub(crate) async fn upload_keys_route(
}
if deser_device_keys.device_id != sender_device {
return Err!(Request(Unknown(
"Device ID in keys uploaded ({}) does not match your own device ID ({})",
deser_device_keys.device_id,
sender_device,
"Device ID in keys uploaded does not match your own device ID"
)));
}
-2
View File
@@ -14,7 +14,6 @@
pub(super) mod media_legacy;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod oidc;
pub(super) mod openid;
pub(super) mod presence;
pub(super) mod profile;
@@ -59,7 +58,6 @@
pub(super) use membership::*;
pub use membership::{join_room_by_id_helper, leave_all_rooms, leave_room, remote_leave_room};
pub(super) use message::*;
pub(super) use oidc::*;
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
-137
View File
@@ -1,137 +0,0 @@
use axum::extract::{Query, State};
use conduwuit::{Result, err, utils::ReadyExt};
use conduwuit_web::oidc::{
AuthorizationQuery, OidcRequest, OidcResponse, oidc_consent_form, oidc_login_form,
};
use oxide_auth::{
endpoint::{OwnerConsent, Solicitation},
frontends::simple::endpoint::FnSolicitor,
};
use percent_encoding::percent_decode_str;
use ruma::UserId;
use service::oidc::{SCOPE_PREFIX_API, SCOPE_PREFIX_DEVICE};
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/authorize`
///
/// Authenticate a user and device, and solicit the user's consent.
///
/// Redirects to the login page if no token or token not belonging to any user.
/// [super::login::oidc_login] takes it up at the same point, so it's either
/// the client has a token, or the user does user password. Then the user gets
/// access to stage two, [authorize_consent].
pub(crate) async fn authorize(
State(services): State<crate::State>,
Query(query): Query<AuthorizationQuery>,
oauth: OidcRequest,
) -> Result<OidcResponse> {
tracing::trace!("processing OAuth request: {query:#?}");
// Enforce MSC2964's restrictions on OAuth2 flow.
let Ok(scope) = percent_decode_str(&query.scope).decode_utf8() else {
return Err(err!(Request(Unknown("the scope could not be percent-decoded"))));
};
if !scope.contains(&format!("{SCOPE_PREFIX_API}*")) {
return Err(err!(Request(Unknown("the scope does not include the client API"))));
}
if !scope.contains(SCOPE_PREFIX_DEVICE) {
return Err(err!(Request(Unknown("the scope does not include a device ID"))));
}
if query.code_challenge_method != "S256" {
return Err(err!(Request(Unknown("unsupported code challenge method"))));
}
// Redirect to the login page if no token or token not known.
let hostname = services.config.server_name.host();
let Some(token) = oauth.authorization_header() else {
return Ok(oidc_login_form(hostname, &query));
};
tracing::debug!("submitting OIDC authorisation for token : {token:#?}");
// Get the user id from the token and add it to the query.
let (owner_id, _) = services.oidc.user_and_device_from_token(token).await?;
let mut query_with_user_id = query.clone();
query_with_user_id.username = Some(owner_id.localpart().to_owned());
services
.oidc
.endpoint()
.with_solicitor(oidc_consent_form(hostname, &query_with_user_id))
.authorization_flow()
.execute(oauth)
.map_err(|err| err!("authorization failed: {err:?}"))
}
/// Whether a user allows their device to access this homeserver's resources.
#[derive(serde::Deserialize)]
pub(crate) struct Allowance {
allow: Option<String>,
}
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/authorize?allow=[Option<String>]`
///
/// Authorize the device based on the owner's consent. If the owner allows
/// it to access their data, the client may request a token at the
/// [super::token::token] endpoint.
///
/// On the owner's consent, if their specific device is unregistered it will be
/// registered in their device list (not to be confused with the OIDC client
/// registration).
pub(crate) async fn authorize_consent(
Query(Allowance { allow }): Query<Allowance>,
State(services): State<crate::State>,
Query(query): Query<AuthorizationQuery>,
oauth: OidcRequest,
) -> Result<OidcResponse> {
tracing::debug!("processing owner's consent: {:?}", allow);
tracing::trace!("owner's consent request: {:#?}", query);
let Some(owner_id) = allow.clone() else {
return Err(err!(Request(Unknown("the owner did not consent to the client's access"))));
};
let server_name = services.globals.server_name();
let owner_id = UserId::parse_with_server_name(owner_id.clone(), server_name)
.map_err(|err| err!(Request(InvalidUsername("invalid username {owner_id:?}: {err}"))))?;
let Some(matrix_client) = services
.oidc
.client_from_client_id(&query.client_id)
.await?
else {
return Err(err!(Request(Unknown(
"no client has registered client_id {:?}",
query.client_id
))));
};
let scope = query.scope.parse().map_err(|err| {
err!(Request(Unknown("could not parse scope {:?}: {}", query.scope, err)))
})?;
let device_id = services.oidc.device_id_from_scope(&scope)?;
// Check that the device is registered in the owner devices list.
// Note that this is _not_ the OIDC client registration.
let device_is_registered_with_owner = services
.users
.all_device_ids(&owner_id)
.ready_any(|v| v == device_id)
.await;
if !device_is_registered_with_owner {
// TODO get the client's IP from the request.
let client_ip = None;
services
.oidc
.register_device(
&query.client_id,
(&owner_id, &device_id),
matrix_client.name.as_deref(),
client_ip,
)
.await?;
}
services
.oidc
.endpoint()
.with_solicitor(FnSolicitor(move |_: &mut _, _: Solicitation<'_>| match allow.clone() {
| None => OwnerConsent::Denied,
| Some(user_id) => OwnerConsent::Authorized(user_id),
}))
.authorization_flow()
.execute(oauth)
.map_err(|err| err!(Request(Unknown("consent request failed: {err:?}"))))
}
-93
View File
@@ -1,93 +0,0 @@
/// Manual implementation of [MSC2965]'s OIDC server discovery.
///
/// [MSC2965]: https://github.com/matrix-org/matrix-spec-proposals/pull/2965
use axum::extract::State;
use conduwuit::Result;
use ruma::{
api::client::{
discovery::get_authorization_server_metadata::msc2965::{
self, AccountManagementAction, AuthorizationServerMetadata, CodeChallengeMethod,
GrantType, Prompt, ResponseMode, ResponseType,
},
error::{
Error as ClientError, ErrorBody as ClientErrorBody, ErrorKind as ClientErrorKind,
},
},
serde::Raw,
};
use crate::{Ruma, RumaResponse, conduwuit::Error};
/// # `GET /_matrix/client/unstable/org.matrix.msc2965/auth_metadata`
///
/// If `globals.auth.enable_oidc_login` is set, advertise this homeserver's
/// OAuth2 endpoints. Otherwise, MSC2965 requires that the homeserver responds
/// with 404/M_UNRECOGNIZED.
pub(crate) async fn get_auth_metadata(
State(services): State<crate::State>,
_body: Ruma<msc2965::Request>,
) -> Result<RumaResponse<msc2965::Response>> {
let unrecognized_error = Err(Error::Ruma(ClientError::new(
http::StatusCode::NOT_FOUND,
ClientErrorBody::Standard {
kind: ClientErrorKind::Unrecognized,
message: "This homeserver has disabled OIDC authentication.".to_owned(),
},
)));
let Some(ref auth) = services.server.config.auth else {
return unrecognized_error;
};
if !auth.enable_oidc_login {
return unrecognized_error;
}
// Advertise this homeserver's access URL as the issuer URL.
// Unwrap all Url::parse() calls because the issuer URL is validated at startup.
let issuer = services.server.config.well_known.client.as_ref().unwrap();
let account_management_uri = auth.enable_oidc_account_management.then_some(
issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/account")
.unwrap(),
);
// Build up metadata with primitives from ruma::api::client::msc2965.
let metadata = AuthorizationServerMetadata {
issuer: issuer.clone(),
authorization_endpoint: issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/authorize")
.unwrap(),
device_authorization_endpoint: Some(
issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/device")
.unwrap(),
),
token_endpoint: issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/token")
.unwrap(),
registration_endpoint: Some(
issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/device/register")
.unwrap(),
),
revocation_endpoint: issuer
.join("/_matrix/client/unstable/org.matrix.msc2964/revoke")
.unwrap(),
response_types_supported: [ResponseType::Code].into(),
grant_types_supported: [GrantType::AuthorizationCode, GrantType::RefreshToken].into(),
response_modes_supported: [ResponseMode::Fragment, ResponseMode::Query].into(),
code_challenge_methods_supported: [CodeChallengeMethod::S256].into(),
account_management_uri,
account_management_actions_supported: [
AccountManagementAction::Profile,
AccountManagementAction::SessionView,
AccountManagementAction::SessionEnd,
]
.into(),
prompt_values_supported: match services.server.config.allow_registration {
| true => vec![Prompt::Create],
| false => vec![],
},
};
let metadata = Raw::new(&metadata).expect("authorization server metadata should serialize");
Ok(RumaResponse(msc2965::Response::new(metadata)))
}
-48
View File
@@ -1,48 +0,0 @@
use axum::extract::State;
use conduwuit::{Result, err, utils::hash::verify_password};
use conduwuit_web::oidc::{LoginError, LoginQuery, OidcRequest, OidcResponse, oidc_consent_form};
use ruma::user_id::UserId;
//#[axum::debug_handler]
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/login`
///
/// Display a login UI to the user and return an authorization code on success.
/// We presume that the OAuth2 query parameters are provided in the form.
/// With the code, the client may then access stage two,
/// [super::authorize::authorize_consent].
pub(crate) async fn oidc_login(
State(services): State<crate::State>,
request: OidcRequest,
) -> Result<OidcResponse> {
let query: LoginQuery = request.clone().try_into().map_err(|LoginError(err)| {
err!(Request(InvalidParam("Cannot process login form. {err}")))
})?;
tracing::trace!("processing login query {:#?}", query.clone());
// Only accept local usernames. Mostly to simplify things at first.
let user_id =
UserId::parse_with_server_name(query.username.clone(), &services.config.server_name)
.map_err(|e| err!(Request(InvalidUsername("Username is invalid: {e}"))))?;
if !services.users.exists(&user_id).await {
return Err(err!(Request(Unknown("unknown username"))));
}
let valid_hash = services.users.password_hash(&user_id).await?;
if valid_hash.is_empty() {
return Err(err!(Request(UserDeactivated("the user's hash was not found"))));
}
if verify_password(&query.password, &valid_hash).is_err() {
return Err(err!(Request(InvalidParam("password does not match"))));
}
// TODO check if user disabled, etc. See /src/api/client/session.rs
let hostname = services.config.server_name.host();
tracing::info!("logging in {user_id:?}");
services
.oidc
.endpoint()
.with_solicitor(oidc_consent_form(hostname, &query.into()))
.authorization_flow()
.execute(request)
.map_err(|err| err!(Request(Unknown("authorisation failed: {err:?}"))))
}
-27
View File
@@ -1,27 +0,0 @@
//! OIDC
//!
//! Stands for OpenID Connect, and is an authentication scheme relying on
//! OAuth2. The [MSC2964] Matrix Spec Proposal describes an authentication
//! process based on the OIDC flow, with restrictions. See the [sample flow] for
//! details on what's expected.
//!
//! This module implements the needed endpoints. It relies on the [oxide-auth]
//! crate, and the [`service::oidc`] and [`web::oidc`] modules.
//!
//! [MSC2964]: https://github.com/matrix-org/matrix-spec-proposals/pull/2964
//! [oxide-auth]: https://docs.rs/oxide-auth
//! [sample flow]: https://github.com/sandhose/matrix-spec-proposals/blob/msc/sandhose/oauth2-profile/proposals/2964-oauth2-profile.md#sample-flow
mod authorize;
mod discovery;
mod login;
mod register;
mod token;
pub(crate) use self::{
authorize::{authorize, authorize_consent},
discovery::get_auth_metadata,
login::oidc_login,
register::register_client,
token::token,
};
-139
View File
@@ -1,139 +0,0 @@
use axum::{Json, extract::State};
use conduwuit::{Result, err};
use conduwuit_service::oidc::normalize_redirect;
use oxide_auth::primitives::prelude::Client;
use reqwest::Url;
use ruma::{ClientSecret, DeviceId, identifiers_validation};
/// The required parameters to register a new client for OAuth2 application.
/// See the required metadata in OAuth2 authorization grant flow in [MSC2966].
///
/// [MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966
#[derive(serde::Deserialize, Clone, Debug)]
pub(crate) struct ClientQuery {
/// Human-readable name.
client_name: String,
/// A public page that tells more about the client. All other links must be
/// within.
client_uri: Url,
/// Redirect URIs declared by the client. At least one.
redirect_uris: Vec<Url>,
/// Must include the literal "code".
response_types: Vec<String>,
/// Must include the literals "authorization_code" and "refresh_token".
grant_types: Vec<String>,
/// How the client intends to authenticate its requests. Can be "none",
/// meaning that the client will negotiate its token with the
/// "authorization code" flow.
token_endpoint_auth_method: String,
/// Link to the logo.
logo_uri: Option<Url>,
/// Link to the client's policy.
policy_uri: Option<Url>,
/// Link to the terms of service.
tos_uri: Option<Url>,
/// Can be "native", implying localhost or reserved redirect pages.
/// Defaults to "web" if not present.
application_type: Option<String>,
}
/// A successful response that the client was registered.
#[derive(serde::Serialize, Debug)]
pub(crate) struct ClientResponse {
client_id: String,
/// If the client is private, the secret it authenticates itself with.
client_secret: Option<String>,
/// If there's a `client_secret`, its expiration date in seconds since
/// 1970-01-01T00:00. Some(0) means no expiration date.
client_secret_expires_at: Option<u32>,
client_name: String,
/// Points to the "about" page of the client.
client_uri: Url,
logo_uri: Option<Url>,
tos_uri: Option<Url>,
policy_uri: Option<Url>,
/// Registered redirect uris, which will be matched against when
/// authenticating. If a localhost address, must contain instances of
/// oxide-auth's `RegisteredUrl::IgnorePortOnLocalhost` to let
/// authorization flow through any port over localhost.
redirect_uris: Vec<Url>,
token_endpoint_auth_method: String,
response_types: Vec<String>,
grant_types: Vec<String>,
application_type: Option<String>,
}
/// # `GET /_matrix/client/unstable/org.matrix.msc2964/device/register`
///
/// Register a client, as specified in [MSC2966]. This client, "device" in OIDC
/// parlance, will have the right to submit [super::authorize::authorize]
/// requests.
///
/// [MSC2966]: https://github.com/matrix-org/matrix-spec-proposals/pull/2966
pub(crate) async fn register_client(
State(services): State<crate::State>,
Json(client): Json<ClientQuery>,
) -> Result<Json<ClientResponse>> {
tracing::trace!("processing OIDC device register request for client: {client:#?}");
if client.redirect_uris.is_empty() {
return Err(err!(Request(Unknown(
"the client's registration request should contain at least a redirect_uri"
))));
}
let mut redirect_uris = client.redirect_uris.clone();
let redirect_uri = redirect_uris.pop().expect("at least one redirect_uri");
let redirect_uri = normalize_redirect(redirect_uri);
let remaining_uris = redirect_uris.into_iter().map(normalize_redirect).collect();
let device_id = DeviceId::new();
// Only provide a default scope, we'll test the client's proposed scope for
// consent anyway.
let scope = "default".parse().unwrap();
// TODO check if the users service needs an update.
//services.users.update_device_metadata();
// If the client cannot authenticate itself at the token endpoint, then
// it's a public client. This is usually the case in Matrix.
let is_private = client.token_endpoint_auth_method != "none";
let client_secret = match is_private {
| true => {
let secret = ClientSecret::new();
identifiers_validation::client_secret::validate(secret.as_str())?;
Some(secret.to_string())
},
| false => None,
};
let registration = match is_private {
| true => &Client::confidential(
device_id.as_ref(),
redirect_uri,
scope,
client_secret.as_ref().unwrap().as_bytes(),
)
.with_additional_redirect_uris(remaining_uris),
| _ => &Client::public(device_id.as_ref(), redirect_uri, scope)
.with_additional_redirect_uris(remaining_uris),
};
tracing::trace!("registering OIDC device : {registration:#?}");
services
.oidc
.register_client(Some(client.client_name.clone()), registration);
let client_response = ClientResponse {
client_id: device_id.to_string(),
client_secret,
client_secret_expires_at: if is_private { Some(0) } else { None },
client_name: client.client_name.clone(),
client_uri: client.client_uri.clone(),
redirect_uris: client.redirect_uris.clone(),
logo_uri: client.logo_uri.clone(),
policy_uri: client.policy_uri.clone(),
tos_uri: client.tos_uri.clone(),
token_endpoint_auth_method: client.token_endpoint_auth_method.clone(),
response_types: client.response_types.clone(),
grant_types: client.grant_types.clone(),
application_type: client.application_type,
};
tracing::debug!("OIDC device registered : {client_response:#?}");
Ok(Json(client_response))
}
-35
View File
@@ -1,35 +0,0 @@
use axum::extract::State;
use conduwuit::{Result, err};
use conduwuit_web::oidc::{OidcRequest, OidcResponse};
use oxide_auth::endpoint::QueryParameter;
/// # `POST /_matrix/client/unstable/org.matrix.msc2964/token`
///
/// Depending on `grant_type`, either deliver a new token to a device, and store
/// it in the server's ring, or refresh the token.
pub(crate) async fn token(
State(services): State<crate::State>,
oauth: OidcRequest,
) -> Result<OidcResponse> {
tracing::trace!("processing OpenID token request {:#?}", oauth);
let Some(body) = oauth.body() else {
return Err(err!(Request(Unknown("OAuth request had an empty body"))));
};
let grant_type = body
.unique_value("grant_type")
.map(|value| value.to_string());
let endpoint = services.oidc.endpoint();
tracing::debug!("submitting OpenID token request for grant type {grant_type:?}");
match grant_type.as_deref() {
| Some("authorization_code") => endpoint
.access_token_flow()
.execute(oauth)
.map_err(|err| err!(Request(Unknown("token grant failed: {err:?}")))),
| Some("refresh_token") => endpoint
.refresh_flow()
.execute(oauth)
.map_err(|err| err!(Request(Unknown("token refresh failed: {err:?}")))),
| other => Err(err!(Request(Unknown("unsupported grant type: {other:?}")))),
}
}
+2 -12
View File
@@ -3,9 +3,7 @@
use futures::StreamExt;
use ruma::api::client::{
discovery::{
discover_homeserver::{
self, AuthenticationServerInfo, HomeserverInfo, SlidingSyncProxyInfo,
},
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
discover_support::{self, Contact},
},
error::ErrorKind,
@@ -28,16 +26,8 @@ pub(crate) async fn well_known_client(
Ok(discover_homeserver::Response {
homeserver: HomeserverInfo { base_url: client_url.clone() },
identity_server: None,
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url.clone() }),
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url }),
tile_server: None,
authentication: services.config.auth.as_ref().and_then(|auth| {
auth.enable_oidc_login
.then_some(AuthenticationServerInfo::new(
client_url.clone(),
auth.enable_oidc_account_management
.then_some(format!("{client_url}/account")),
))
}),
})
}
-15
View File
@@ -115,21 +115,6 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_protocols_route)
.route("/_matrix/client/unstable/thirdparty/protocols",
get(client::get_protocols_route_unstable))
// MSC2965: OAuth 2.0 Authorization Server Metadata discovery.
.route("/_matrix/client/unstable/org.matrix.msc2965/auth_metadata",
get(client::get_auth_metadata))
// MSC2964: Usage of OAuth 2.0 authorization code grant and refresh token grant.
.route("/_matrix/client/unstable/org.matrix.msc2964/authorize",
get(client::authorize))
.route("/_matrix/client/unstable/org.matrix.msc2964/authorize",
post(client::authorize_consent))
.route("/_matrix/client/unstable/org.matrix.msc2964/login",
post(client::oidc_login))
.route("/_matrix/client/unstable/org.matrix.msc2964/token",
post(client::token))
// MSC2966: Usage of OAuth 2.0 Dynamic Client Registration in Matrix.
.route("/_matrix/client/unstable/org.matrix.msc2964/device/register",
post(client::register_client))
.ruma_route(&client::send_message_event_route)
.ruma_route(&client::send_state_event_for_key_route)
.ruma_route(&client::get_state_events_route)
-9
View File
@@ -289,15 +289,6 @@ pub fn check(config: &Config) -> Result {
));
}
if let Some(auth) = &config.auth {
if auth.enable_oidc_login && config.well_known.client.is_none() {
return Err!(Config(
"auth.enable_oidc_login",
"OIDC authentication is enabled but the well-known client is not set."
));
}
}
Ok(())
}
+2 -25
View File
@@ -53,7 +53,7 @@
### For more information, see:
### https://continuwuity.org/configuration.html
"#,
ignore = "catchall auth well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
ignore = "catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure"
)]
pub struct Config {
/// The server_name is the pretty name of this server. It is used as a
@@ -62,8 +62,7 @@ pub struct Config {
/// See the docs for reverse proxying and delegation:
/// https://continuwuity.org/deploying/generic.html#setting-up-the-reverse-proxy
///
/// Also see the `[global.auth]` and `[global.well_known]` config sections
/// at the very bottom.
/// Also see the `[global.well_known]` config section at the very bottom.
///
/// Examples of delegation:
/// - https://puppygock.gay/.well-known/matrix/server
@@ -105,9 +104,6 @@ pub struct Config {
#[serde(default)]
pub tls: TlsConfig,
// external structure; separate section
pub auth: Option<AuthConfig>,
/// The UNIX socket continuwuity will listen on.
///
/// continuwuity cannot listen on both an IP address and a UNIX socket. If
@@ -2024,25 +2020,6 @@ pub struct TlsConfig {
pub dual_protocol: bool,
}
#[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)]
#[derive(Clone, Debug, Deserialize, Default)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.auth")]
pub struct AuthConfig {
/// Use this homeserver as the OIDC authentication reference. It will
/// advertise itself as the OIDC authentication issuer to new clients,
/// and use the internal user database to answer on the advertised
/// endpoints. Note that the legacy Matrix authentication still will be
/// reachable.
/// Unset by default.
pub enable_oidc_login: bool,
/// Whether this homeserver should provide users with an account management
/// interface. Only used if `enable_oidc_login` is set. Note that the
/// endpoint is unimplemented at the moment.
/// Unset by default.
pub enable_oidc_account_management: bool,
}
#[allow(rustdoc::broken_intra_doc_links, rustdoc::bare_urls)]
#[derive(Clone, Debug, Deserialize, Default)]
#[config_example_generator(filename = "conduwuit-example.toml", section = "global.well_known")]
+29 -28
View File
@@ -101,40 +101,40 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
debug!(version = ?stateres_version, "State resolution starting");
// Split non-conflicting and conflicting state
let (clean, conflicting) = separate(state_sets.into_iter());
let (unconflicted, conflicting) = separate(state_sets.into_iter());
debug!(count = clean.len(), "non-conflicting events");
trace!(map = ?clean, "non-conflicting events");
debug!(count = unconflicted.len(), "non-conflicting events");
trace!(map = ?unconflicted, "non-conflicting events");
if conflicting.is_empty() {
debug!("no conflicting state found");
return Ok(clean);
return Ok(unconflicted);
}
debug!(count = conflicting.len(), "conflicting events");
trace!(map = ?conflicting, "conflicting events");
let conflicted_state_subgraph: HashSet<_> = match stateres_version {
| StateResolutionVersion::V2_1 =>
calculate_conflicted_subgraph(&conflicting, event_fetch)
let (conflicted_state_subgraph, initial_state) =
if stateres_version == StateResolutionVersion::V2_1 {
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
.await
.ok_or_else(|| {
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
})?,
| _ => HashSet::new(),
};
debug!(count = conflicted_state_subgraph.len(), "conflicted subgraph");
trace!(set = ?conflicted_state_subgraph, "conflicted subgraph");
let conflicting_values = conflicting.into_values().flatten().stream();
})?;
debug!(count = csg.len(), "conflicted subgraph");
trace!(set = ?csg, "conflicted subgraph");
(csg, HashMap::new())
} else {
(HashSet::new(), unconflicted.clone())
};
// `all_conflicted` contains unique items
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in
// event_map}`
// Hydra: Also consider the conflicted state subgraph
let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets)
.chain(conflicting_values)
.chain(conflicted_state_subgraph.into_iter().stream())
.chain(conflicting.into_values().flatten().stream())
.broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id))
.chain(conflicted_state_subgraph.into_iter().stream())
.collect()
.await;
@@ -169,9 +169,8 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
// Sequentially auth check each control event.
let resolved_control = iterative_auth_check(
&room_version,
&stateres_version,
sorted_control_levels.iter().stream().map(AsRef::as_ref),
clean.clone(),
initial_state,
&event_fetch,
)
.await?;
@@ -201,7 +200,7 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
let power_levels_ty_sk = (StateEventType::RoomPowerLevels, StateKey::new());
let power_event = resolved_control.get(&power_levels_ty_sk);
debug!(event_id = ?power_event, "power event");
trace!(event_id = ?power_event, "power event");
let sorted_left_events =
mainline_sort(&events_to_resolve, power_event.cloned(), &event_fetch).await?;
@@ -210,15 +209,14 @@ pub async fn resolve<'a, Pdu, Sets, SetIter, Hasher, Fetch, FetchFut, Exists, Ex
let mut resolved_state = iterative_auth_check(
&room_version,
&stateres_version,
sorted_left_events.iter().stream().map(AsRef::as_ref),
resolved_control.clone(), // The control events are added to the final resolved state
resolved_control, // The control events are added to the final resolved state
&event_fetch,
)
.await?;
// Ensure unconflicting state is in the final state
resolved_state.extend(clean);
resolved_state.extend(unconflicted);
debug!("state resolution finished");
trace!( map = ?resolved_state, "final resolved state" );
@@ -598,7 +596,6 @@ async fn get_power_level_for_sender<E, F, Fut>(
#[tracing::instrument(level = "trace", skip_all)]
async fn iterative_auth_check<'a, E, F, Fut, S>(
room_version: &RoomVersion,
stateres_version: &StateResolutionVersion,
events_to_check: S,
unconflicted_state: StateMap<OwnedEventId>,
fetch_event: &F,
@@ -623,6 +620,10 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
.boxed()
.await?;
trace!(list = ?events_to_check, "events to check");
if events_to_check.is_empty() {
debug!("no events to check, returning unconflicted state");
return Ok(unconflicted_state);
}
let auth_event_ids: HashSet<OwnedEventId> = events_to_check
.iter()
@@ -643,10 +644,11 @@ async fn iterative_auth_check<'a, E, F, Fut, S>(
trace!(map = ?auth_events.keys().collect::<Vec<_>>(), "fetched auth events");
let auth_events = &auth_events;
let mut resolved_state = match stateres_version {
| StateResolutionVersion::V2_1 => StateMap::new(),
| _ => unconflicted_state,
};
// NOTE: in state resolution v2.1, auth checks should start with an empty state
// map. It is the caller's job to do this. Previously, this function would
// force an empty state map in this case, and this resulted in power events
// going missing from the resolved state as they'd be discarded here.
let mut resolved_state = unconflicted_state;
for event in events_to_check {
trace!(event_id = event.event_id().as_str(), "checking event");
let state_key = event
@@ -1034,7 +1036,6 @@ async fn test_event_sort() {
let resolved_power = super::iterative_auth_check(
&RoomVersion::V6,
&StateResolutionVersion::V2,
sorted_power_events.iter().map(AsRef::as_ref).stream(),
HashMap::new(), // unconflicted events
&fetcher,
+1 -1
View File
@@ -28,7 +28,7 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
/// use conduwuit_core::utils::debug::slice_truncated;
///
/// #[tracing::instrument(fields(foos = slice_truncated(foos, 42)))]
/// fn bar(foos: &[&str]);
/// fn bar(foos: &[&str]) {};
/// ```
pub fn slice_truncated<T: fmt::Debug>(
slice: &[T],
-8
View File
@@ -434,14 +434,6 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userroomid_notificationcount",
..descriptor::RANDOM
},
Descriptor {
name: "client_registrar",
..descriptor::RANDOM
},
Descriptor {
name: "deviceid_clientidmap",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userroomid_invitesender",
..descriptor::RANDOM_SMALL
-2
View File
@@ -125,8 +125,6 @@ ctor.workspace = true
[target.'cfg(all(unix, target_os = "linux"))'.dependencies]
sd-notify.workspace = true
sd-notify.optional = true
oxide-auth.workspace = true
once_cell.workspace = true
[lints]
workspace = true
-1
View File
@@ -19,7 +19,6 @@
pub mod key_backups;
pub mod media;
pub mod moderation;
pub mod oidc;
pub mod presence;
pub mod pusher;
pub mod resolver;
-361
View File
@@ -1,361 +0,0 @@
//! OIDC service.
//!
//! Provides the registrar, authorizer and issuer needed by [api::client::oidc].
//! The whole OIDC OAuth2 flow is taken care of by [oxide-auth].
//!
//! [oxide-auth]: https://docs.rs/oxide-auth
use std::{
borrow::Cow,
sync::{Arc, Mutex},
};
use async_trait::async_trait;
use conduwuit::{Result, err};
use conduwuit_core::utils;
use database::{Deserialized, Json, Map};
use oxide_auth::{
endpoint::{PreGrant, Scope},
frontends::simple::endpoint::{Generic, Vacant},
primitives::{
grant::Grant,
prelude::{
AuthMap, Authorizer, Client, ClientUrl, Issuer, RandomGenerator, Registrar, TokenMap,
},
registrar::{
Argon2, BoundClient, EncodedClient, RegisteredClient, RegisteredUrl, RegistrarError,
},
},
};
use ruma::{
MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId, api::client::device::Device,
};
use serde::{Deserialize, Serialize};
use url::Url;
use crate::{Dep, globals};
pub const SCOPE_PREFIX_DEVICE: &str = "urn:matrix:org.matrix.msc2967.client:device:";
pub const SCOPE_PREFIX_API: &str = "urn:matrix:org.matrix.msc2967.client:api:";
static PASSWORD_POLICY: std::sync::LazyLock<Argon2> = std::sync::LazyLock::new(Argon2::default);
/// A client app that connects to continuwuity via OIDC, as recorded in the
/// database.
#[derive(Clone, Serialize, Deserialize)]
pub struct OidcClient {
/// The name published by the app itself.
pub name: Option<String>,
/// A device id that we'll generate on OIDC registration.
pub device_id: Option<String>,
/// The device's coordinates recorded by oxide-auth.
pub client: EncodedClient,
}
struct Services {
globals: Dep<globals::Service>,
}
struct Data {
client_registrar: Arc<Map>,
deviceid_clientidmap: Arc<Map>,
userid_devicelistversion: Arc<Map>,
userdeviceid_metadata: Arc<Map>,
}
pub struct Service {
/// Authorization tokens are 16 byte random keys to a memory hash map.
///
/// Will be reinitialised on continuwuity's restart.
authorizer: Mutex<AuthMap<RandomGenerator>>,
/// Bearer tokens are also random generated but 256-bit tokens, since they
/// live longer.
///
/// We could also use a `TokenSigner::ephemeral` here to create signed
/// tokens which can be read and parsed by anyone, but not maliciously
/// created. However, they can not be revoked and thus don't offer even
/// longer lived refresh tokens.
///
/// Will be reinitialised on continuwuity's restart.
issuer: Mutex<TokenMap<RandomGenerator>>,
services: Services,
db: Data,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
// TODO implement authorizer and issuer inside database so that token
// requests survive server restarts.
Ok(Arc::new(Self {
authorizer: Mutex::new(AuthMap::new(RandomGenerator::new(16))),
issuer: Mutex::new(TokenMap::new(RandomGenerator::new(16))),
services: Services {
globals: args.depend::<globals::Service>("globals"),
},
db: Data {
client_registrar: args.db["client_registrar"].clone(),
deviceid_clientidmap: args.db["deviceid_clientidmap"].clone(),
userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
/// Register an OIDC client in the client_registrar for future
/// authentication flows.
pub fn register_client(&self, display_name: Option<String>, client: &Client) {
let client = client.clone().encode(&*PASSWORD_POLICY);
let client_id = client.client_id.clone();
let oidc_client = OidcClient {
name: display_name,
// Matrix clients have no device_id at registration time.
device_id: None,
client,
};
self.db.client_registrar.put(client_id, Json(oidc_client));
}
/// Register a device in the main continuwuity database. This should only
/// happen on successful authentication and consent, and will register the
/// client's device_id.
pub async fn register_device(
&self,
client_id: &str,
(user_id, device_id): (&OwnedUserId, &OwnedDeviceId),
display_name: Option<&str>,
client_ip: Option<String>,
) -> Result<()> {
let device_key = (user_id, device_id);
let device = Device {
device_id: device_id.into(),
display_name: display_name.map(ToOwned::to_owned),
last_seen_ip: client_ip,
last_seen_ts: Some(MilliSecondsSinceUnixEpoch::now()),
};
increment(&self.db.userid_devicelistversion, user_id.as_bytes()).await;
self.db.userdeviceid_metadata.put(device_key, Json(device));
let mut client: OidcClient = self
.db
.client_registrar
.get(client_id)
.await?
.deserialized()?;
client.device_id = Some(device_id.to_string());
self.db
.client_registrar
.put(client_id.to_owned(), Json(client));
self.db
.deviceid_clientidmap
.put(device_id, client_id.to_owned());
Ok(())
}
fn grant_from_token(&self, token: &str) -> Option<Grant> {
let issuer = self.issuer.lock().expect("lockable issuer");
issuer
.recover_token(token)
.expect("infallible recover_token implementation")
}
pub async fn client_from_client_id(&self, client_id: &str) -> Result<Option<OidcClient>> {
self.db
.client_registrar
.get(client_id)
.await?
.deserialized()
}
pub async fn client_from_device_id(
&self,
device_id: OwnedDeviceId,
) -> Result<Option<OidcClient>> {
let client_id: String = self
.db
.deviceid_clientidmap
.get(&device_id)
.await?
.deserialized()?;
self.db
.client_registrar
.get(&client_id)
.await?
.deserialized()
}
pub fn device_id_from_scope(&self, scope: &Scope) -> Result<OwnedDeviceId> {
let Some(device_id) = scope.iter().find(|s| s.starts_with(SCOPE_PREFIX_DEVICE)) else {
tracing::warn!("device_id not found in scope {scope:?}");
return Err(err!(Request(InvalidParam("something went wrong with the scope"))));
};
let device_id = device_id.replace(SCOPE_PREFIX_DEVICE, "");
Ok(device_id.into())
}
pub async fn user_and_device_from_token(
&self,
token: &str,
) -> Result<(OwnedUserId, OwnedDeviceId)> {
let Some(Grant { owner_id, client_id, .. }) = self.grant_from_token(token) else {
return Err(err!(Request(MissingToken("unknown token: {token:?}"))));
};
let server_name = self.services.globals.server_name();
let owner_id =
UserId::parse_with_server_name(owner_id.clone(), server_name).map_err(|err| {
err!(Request(InvalidUsername("invalid username {owner_id:?}: {err}")))
})?;
let client = self
.client_from_client_id(&client_id)
.await?
.expect("validated client_id");
let Some(device_id) = client.device_id else {
return Err(err!(Request(Unknown("this client has no device_id yet"))));
};
let device_id = OwnedDeviceId::from(device_id);
Ok((owner_id, device_id))
}
/// The oxide-auth carry-all endpoint.
pub fn endpoint(
&self,
) -> Generic<impl Registrar + '_, impl Authorizer + '_, impl Issuer + '_> {
Generic {
registrar: self,
authorizer: self.authorizer.lock().unwrap(),
issuer: self.issuer.lock().unwrap(),
// Solicitor configured later.
solicitor: Vacant,
// Scope configured later.
scopes: Vacant,
response: Vacant,
}
}
}
async fn increment(db: &Arc<Map>, key: &[u8]) {
let old = db.get(key).await;
let new = utils::increment(old.ok().as_deref());
db.insert(key, new);
}
/// Substitute "127.0.0.1" and "[::1]" for "localhost" to let oxide-auth compare
/// them ignoring their port.
pub fn normalize_redirect_hostname(url: &mut Url) {
let new_host = url.host_str().map(|h| {
h.replace("127.0.0.1", "localhost")
.replace("[::1]", "localhost")
});
url.set_host(new_host.as_deref())
.expect("replaceable redirect hostname");
}
/// If `url` is a localhost (either 'localhost', '127.0.0.1' or '[::1]'), wrap
/// it in an `IgnorePortOnLocalhost`, so that oxide-auth ignores the port when
/// comparing it with the registered ones.
#[must_use]
pub fn normalize_redirect(mut url: Url) -> RegisteredUrl {
normalize_redirect_hostname(&mut url);
match url.host_str() {
| Some("localhost") => RegisteredUrl::IgnorePortOnLocalhost(url.into()),
| _ => RegisteredUrl::Semantic(url),
}
}
/// Let this service act as an oxide-auth `Registrar`.
impl Registrar for Service {
fn bound_redirect<'a>(
&self,
bound: ClientUrl<'a>,
) -> Result<BoundClient<'a>, RegistrarError> {
let client_handle = self
.db
.client_registrar
.get_blocking(bound.client_id.as_ref())
.map_err(|_| RegistrarError::Unspecified)?;
let oidc_client: OidcClient = client_handle
.deserialized()
.map_err(|_| RegistrarError::Unspecified)?;
let client = oidc_client.client;
// Perform exact matching as motivated in the rfc, but substitute
// "127.0.0.1" and "[::1]" for "localhost" to let oxide-auth ignore
// their port.
let redirect_uri = bound.redirect_uri;
let normalized_uri = redirect_uri.clone().map(|u| normalize_redirect(u.to_url()));
let redirect_uri = match normalized_uri {
| None => client.redirect_uri,
| Some(url) => {
let original = std::iter::once(&client.redirect_uri);
let alternatives = client.additional_redirect_uris.iter();
if original
.chain(alternatives)
.any(|registered| *registered == url)
{
// If normalized_uri is Some(url), so is redirect_uri, so unwrap().
redirect_uri.unwrap().into_owned().into()
} else {
tracing::debug!(
"the request's redirect url didn't match any registered. bound: {:?}, \
in client {:#?}",
url,
client
);
return Err(RegistrarError::Unspecified);
}
},
};
Ok(BoundClient {
client_id: bound.client_id,
redirect_uri: Cow::Owned(redirect_uri),
})
}
fn negotiate(
&self,
bound: BoundClient<'_>,
_scope: Option<Scope>,
) -> Result<PreGrant, RegistrarError> {
let client_handle = self
.db
.client_registrar
.get_blocking(bound.client_id.as_ref())
.map_err(|_| RegistrarError::Unspecified)?;
let oidc_client: OidcClient = client_handle
.deserialized()
.map_err(|_| RegistrarError::Unspecified)?;
Ok(PreGrant {
client_id: bound.client_id.into_owned(),
redirect_uri: bound.redirect_uri.into_owned(),
// Always use the client's scope.
scope: oidc_client.client.default_scope,
})
}
fn check(&self, client_id: &str, passphrase: Option<&[u8]>) -> Result<(), RegistrarError> {
let client_handle = self
.db
.client_registrar
.get_blocking(client_id)
.map_err(|_| RegistrarError::Unspecified)?;
let oidc_client: OidcClient = client_handle
.deserialized()
.map_err(|_| RegistrarError::Unspecified)?;
let client = oidc_client.client;
RegisteredClient::new(&client, &*PASSWORD_POLICY).check_authentication(passphrase)
}
}
+1 -3
View File
@@ -11,7 +11,7 @@
account_data, admin, announcements, appservice, client, config, emergency, federation,
globals, key_backups,
manager::Manager,
media, moderation, oidc, presence, pusher, resolver, rooms, sending, server_keys, service,
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service},
sync, transaction_ids, uiaa, users,
};
@@ -39,7 +39,6 @@ pub struct Services {
pub users: Arc<users::Service>,
pub moderation: Arc<moderation::Service>,
pub announcements: Arc<announcements::Service>,
pub oidc: Arc<oidc::Service>,
manager: Mutex<Option<Arc<Manager>>>,
pub(crate) service: Arc<Map>,
@@ -108,7 +107,6 @@ macro_rules! build {
users: build!(users::Service),
moderation: build!(moderation::Service),
announcements: build!(announcements::Service),
oidc: build!(oidc::Service),
manager: Mutex::new(None),
service,
+3 -33
View File
@@ -29,7 +29,7 @@
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::{Dep, account_data, admin, appservice, globals, oidc, rooms};
use crate::{Dep, account_data, admin, appservice, globals, rooms};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserSuspension {
@@ -52,7 +52,6 @@ struct Services {
admin: Dep<admin::Service>,
appservice: Dep<appservice::Service>,
globals: Dep<globals::Service>,
oidc: Dep<oidc::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
state_cache: Dep<rooms::state_cache::Service>,
}
@@ -90,7 +89,6 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
account_data: args.depend::<account_data::Service>("account_data"),
admin: args.depend::<admin::Service>("admin"),
appservice: args.depend::<appservice::Service>("appservice"),
oidc: args.depend::<oidc::Service>("oidc"),
globals: args.depend::<globals::Service>("globals"),
state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
@@ -272,18 +270,7 @@ pub async fn count(&self) -> usize { self.db.userid_password.count().await }
/// Find out which user an access token belongs to.
pub async fn find_from_token(&self, token: &str) -> Result<(OwnedUserId, OwnedDeviceId)> {
if self
.services
.server
.config
.auth
.as_ref()
.is_some_and(|auth| auth.enable_oidc_login)
{
self.services.oidc.user_and_device_from_token(token).await
} else {
self.db.token_userdeviceid.get(token).await.deserialized()
}
self.db.token_userdeviceid.get(token).await.deserialized()
}
/// Returns an iterator over all users on this homeserver (offered for
@@ -549,24 +536,7 @@ pub async fn add_one_time_key(
// Only existing devices should be able to call this, but we shouldn't assert
// either...
let key = (user_id, device_id);
if self
.services
.server
.config
.auth
.as_ref()
.is_some_and(|auth| auth.enable_oidc_login)
{
if self
.services
.oidc
.client_from_device_id(device_id.into())
.await?
.is_none()
{
return Err!(Database(error!(?user_id, ?device_id, "Device has no metadata.")));
}
} else if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
return Err!(Database(error!(
?user_id,
?device_id,
-6
View File
@@ -22,8 +22,6 @@ crate-type = [
[dependencies]
conduwuit-build-metadata.workspace = true
conduwuit-service.workspace = true
conduwuit-core.workspace = true
async-trait.workspace = true
askama = "0.14.0"
@@ -32,10 +30,6 @@ futures.workspace = true
tracing.workspace = true
rand.workspace = true
thiserror.workspace = true
serde.workspace = true
url.workspace = true
percent-encoding.workspace = true
oxide-auth.workspace = true
[lints]
workspace = true
-1
View File
@@ -8,7 +8,6 @@
};
use conduwuit_build_metadata::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL, version_tag};
use conduwuit_service::state;
pub mod oidc;
pub fn build() -> Router<state::State> {
let router = Router::<state::State>::new();
-60
View File
@@ -1,60 +0,0 @@
use askama::Template;
use conduwuit_build_metadata::version_tag;
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
// Imports needed by askama templates.
use crate::{GIT_REMOTE_COMMIT_URL, GIT_REMOTE_WEB_URL};
mod authorize;
mod consent;
mod error;
mod login;
mod request;
mod response;
pub use authorize::AuthorizationQuery;
pub use consent::oidc_consent_form;
pub use error::OidcError;
pub use login::{LoginError, LoginQuery, oidc_login_form};
pub use request::OidcRequest;
pub use response::OidcResponse;
/// The parameters for the OIDC login page template.
#[derive(Template)]
#[template(path = "login.html.j2")]
pub(crate) struct LoginPageTemplate<'a> {
nonce: &'a str,
hostname: &'a str,
route: &'a str,
client_id: &'a str,
client_secret: Option<&'a str>,
redirect_uri: &'a str,
scope: &'a str,
state: &'a str,
code_challenge: &'a str,
code_challenge_method: &'a str,
response_type: &'a str,
response_mode: &'a str,
}
/// The parameters for the OIDC consent page template.
#[derive(Template)]
#[template(path = "consent.html.j2")]
pub(crate) struct ConsentPageTemplate<'a> {
nonce: &'a str,
hostname: &'a str,
route: &'a str,
user_id: &'a str,
client_id: &'a str,
client_secret: Option<&'a str>,
redirect_uri: &'a str,
scope: &'a str,
state: &'a str,
code_challenge: &'a str,
code_challenge_method: &'a str,
response_type: &'a str,
response_mode: &'a str,
}
pub(crate) fn encode(text: &str) -> String {
utf8_percent_encode(text, NON_ALPHANUMERIC).to_string()
}
-49
View File
@@ -1,49 +0,0 @@
use url::Url;
use super::LoginQuery;
/// The set of parameters required for an OIDC authorization request.
#[derive(serde::Deserialize, Debug, Clone)]
pub struct AuthorizationQuery {
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Url,
pub scope: String,
pub state: String,
pub code_challenge: String,
pub code_challenge_method: String,
pub response_type: String,
pub response_mode: Option<String>,
pub username: Option<String>,
}
impl From<LoginQuery> for AuthorizationQuery {
fn from(value: LoginQuery) -> Self {
let LoginQuery {
client_id,
client_secret,
redirect_uri,
scope,
state,
code_challenge,
code_challenge_method,
response_type,
response_mode,
username,
..
} = value;
Self {
client_id,
client_secret,
redirect_uri,
scope,
state,
code_challenge,
code_challenge_method,
response_type,
response_mode: Some(response_mode),
username: Some(username),
}
}
}
-55
View File
@@ -1,55 +0,0 @@
use askama::Template;
use axum::http::StatusCode;
use oxide_auth::frontends::simple::request::Body;
use super::{AuthorizationQuery, ConsentPageTemplate, OidcResponse, encode};
/// A web consent solicitor form for the OIDC authentication flow.
///
/// Asks the resource owner for their consent to let a client access their data
/// on this server.
#[must_use]
pub fn oidc_consent_form(hostname: &str, query: &AuthorizationQuery) -> OidcResponse {
// The target request route.
let route = "/_matrix/client/unstable/org.matrix.msc2964/authorize";
let nonce = rand::random::<u64>().to_string();
let body = Some(Body::Text(consent_page(hostname, query, route, &nonce)));
OidcResponse {
status: StatusCode::OK,
location: None,
www_authenticate: None,
body,
nonce: Some(nonce),
}
}
/// Render the html contents of the user consent page.
fn consent_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &str) -> String {
let response_mode = query
.response_mode
.as_deref()
.unwrap_or("fragment")
.to_owned();
let user_id = query
.username
.clone()
.expect("user_id in authorization query");
let template = ConsentPageTemplate {
nonce,
hostname,
route,
user_id: &encode(&user_id),
client_id: &encode(query.client_id.as_str()),
client_secret: query.client_secret.as_deref(),
redirect_uri: &encode(query.redirect_uri.as_str()),
scope: &encode(query.scope.as_str()),
state: &encode(query.state.as_str()),
code_challenge: &encode(query.code_challenge.as_str()),
code_challenge_method: &encode(query.code_challenge_method.as_str()),
response_type: &encode(query.response_type.as_str()),
response_mode: &encode(response_mode.as_str()),
};
template.render().expect("consent page render")
}
-78
View File
@@ -1,78 +0,0 @@
use axum::{
http::{StatusCode, header::InvalidHeaderValue},
response::{IntoResponse, Response},
};
use oxide_auth::frontends::{dev::OAuthError, simple::endpoint::Error};
use super::OidcRequest;
#[derive(Debug)]
/// The error type for Oxide Auth operations
pub enum OidcError {
/// Errors occurring in Endpoint operations
Endpoint(OAuthError),
/// Errors occurring in Endpoint operations
Header(InvalidHeaderValue),
/// Errors with the request encoding
Encoding,
/// Request body could not be parsed as a form
Form,
/// Request query was absent or could not be parsed
Query,
/// Request query was absent or could not be parsed
Body,
/// The Authorization header was invalid
Authorization,
/// General internal server error
InternalError(Option<String>),
}
impl std::fmt::Display for OidcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match *self {
| Self::Endpoint(ref e) => write!(f, "Endpoint, {e}"),
| Self::Header(ref e) => write!(f, "Couldn't set header, {e}"),
| Self::Encoding => write!(f, "Error decoding request"),
| Self::Form => write!(f, "Request is not a form"),
| Self::Query => write!(f, "No query present"),
| Self::Body => write!(f, "No body present"),
| Self::Authorization => write!(f, "Request has invalid Authorization headers"),
| Self::InternalError(None) => write!(f, "An internal server error occurred"),
| Self::InternalError(Some(ref e)) =>
write!(f, "An internal server error occurred: {e}"),
}
}
}
impl std::error::Error for OidcError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match *self {
| Self::Endpoint(ref e) => e.source(),
| Self::Header(ref e) => e.source(),
| _ => None,
}
}
}
impl IntoResponse for OidcError {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response()
}
}
impl From<Error<OidcRequest>> for OidcError {
fn from(e: Error<OidcRequest>) -> Self {
match e {
| Error::Web(e) => e,
| Error::OAuth(e) => e.into(),
}
}
}
impl From<OAuthError> for OidcError {
fn from(e: OAuthError) -> Self { Self::Endpoint(e) }
}
impl From<InvalidHeaderValue> for OidcError {
fn from(e: InvalidHeaderValue) -> Self { Self::Header(e) }
}
-126
View File
@@ -1,126 +0,0 @@
use std::{borrow::Cow, str::FromStr};
use askama::Template;
use axum::http::StatusCode;
use oxide_auth::{endpoint::QueryParameter, frontends::simple::request::Body};
use url::Url;
use super::{AuthorizationQuery, LoginPageTemplate, OidcRequest, OidcResponse};
/// The set of query parameters a client needs to get authorization.
#[derive(serde::Deserialize, Debug, Clone)]
pub struct LoginQuery {
pub username: String,
pub password: String,
pub client_id: String,
pub client_secret: Option<String>,
pub redirect_uri: Url,
pub scope: String,
pub state: String,
pub code_challenge: String,
pub code_challenge_method: String,
pub response_type: String,
pub response_mode: String,
}
#[derive(Debug)]
pub struct LoginError(pub String);
impl TryFrom<OidcRequest> for LoginQuery {
type Error = LoginError;
fn try_from(value: OidcRequest) -> Result<Self, LoginError> {
let body = value.body().expect("body in OidcRequest");
let Some(username) = body.unique_value("username") else {
return Err(LoginError("missing field: username".to_owned()));
};
let Some(password) = body.unique_value("password") else {
return Err(LoginError("missing field: password".to_owned()));
};
let Some(client_id) = body.unique_value("client_id") else {
return Err(LoginError("missing field: client_id".to_owned()));
};
let Some(redirect_uri) = body.unique_value("redirect_uri") else {
return Err(LoginError("missing field: redirect_uri".to_owned()));
};
let Some(scope) = body.unique_value("scope") else {
return Err(LoginError("missing field: scope".to_owned()));
};
let Some(state) = body.unique_value("state") else {
return Err(LoginError("missing field: state".to_owned()));
};
let Some(code_challenge) = body.unique_value("code_challenge") else {
return Err(LoginError("missing field: code_challenge".to_owned()));
};
let Some(code_challenge_method) = body.unique_value("code_challenge_method") else {
return Err(LoginError("missing field: code_challenge_method".to_owned()));
};
let Some(response_type) = body.unique_value("response_type") else {
return Err(LoginError("missing field: response_type".to_owned()));
};
let Ok(redirect_uri) = Url::from_str(&redirect_uri) else {
return Err(LoginError("invalid field: redirect_uri".to_owned()));
};
// response_mode is not strictly needed : it must be the literal "fragment"
// when over https. It's required by the spec but Fractal doesn't provide it.
let response_mode = body
.unique_value("response_mode")
.unwrap_or(Cow::Borrowed("fragment"));
let client_secret = body.unique_value("client_secret").map(|s| s.to_string());
Ok(Self {
username: username.to_string(),
password: password.to_string(),
client_id: client_id.to_string(),
client_secret,
redirect_uri,
scope: scope.to_string(),
state: state.to_string(),
code_challenge: code_challenge.to_string(),
code_challenge_method: code_challenge_method.to_string(),
response_type: response_type.to_string(),
response_mode: response_mode.to_string(),
})
}
}
/// A web login form for the OIDC authentication flow.
///
/// The returned `OidcResponse` handles CSP headers to allow that form.
#[must_use]
pub fn oidc_login_form(hostname: &str, query: &AuthorizationQuery) -> OidcResponse {
// The target request route.
let route = "/_matrix/client/unstable/org.matrix.msc2964/login";
let nonce = rand::random::<u64>().to_string();
let body = Some(Body::Text(login_page(hostname, query, route, &nonce)));
OidcResponse {
status: StatusCode::OK,
location: None,
www_authenticate: None,
body,
nonce: Some(nonce),
}
}
/// Render the html contents of the login page.
fn login_page(hostname: &str, query: &AuthorizationQuery, route: &str, nonce: &str) -> String {
let response_mode = query.response_mode.as_deref().unwrap_or("fragment");
let template = LoginPageTemplate {
nonce,
hostname,
route,
client_id: query.client_id.as_str(),
client_secret: query.client_secret.as_deref(),
redirect_uri: query.redirect_uri.as_str(),
scope: query.scope.as_str(),
state: query.state.as_str(),
code_challenge: query.code_challenge.as_str(),
code_challenge_method: query.code_challenge_method.as_str(),
response_type: query.response_type.as_str(),
response_mode,
};
template.render().expect("login template render")
}
-118
View File
@@ -1,118 +0,0 @@
use std::borrow::Cow;
use async_trait::async_trait;
use axum::{
extract::{Form, FromRequest, FromRequestParts, Query, Request},
http::header,
};
use oxide_auth::endpoint::{NormalizedParameter, QueryParameter, WebRequest};
use super::{OidcError, OidcResponse};
/// An OIDC authentication request.
///
/// Expected to receive GET and POST requests to the `authorize` endpoint, or
/// POST requests to the `login` endpoint.
///
/// Mostly adapted from the OAuthRequest struct in the [oxide-auth-axum] crate.
/// [oxide-auth-axum]: https://docs.rs/oxide-auth-axum
#[derive(Clone, Debug)]
pub struct OidcRequest {
pub(crate) auth: Option<String>,
pub(crate) query: Option<NormalizedParameter>,
pub(crate) body: Option<NormalizedParameter>,
}
impl OidcRequest {
/// Fetch the authorization header from the request
#[must_use]
pub fn authorization_header(&self) -> Option<&str> { self.auth.as_deref() }
/// Fetch the query for this request
#[must_use]
pub fn query(&self) -> Option<&NormalizedParameter> { self.query.as_ref() }
/// Fetch the query mutably
pub fn query_mut(&mut self) -> Option<&mut NormalizedParameter> { self.query.as_mut() }
/// Fetch the body of the request
#[must_use]
pub fn body(&self) -> Option<&NormalizedParameter> { self.body.as_ref() }
}
impl WebRequest for OidcRequest {
type Error = OidcError;
type Response = OidcResponse;
fn query(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
self.query
.as_ref()
.map(|q| {
let q: &dyn QueryParameter = q;
Cow::Borrowed(q)
})
.ok_or(OidcError::Query)
}
fn urlbody(&mut self) -> Result<Cow<'_, dyn QueryParameter + 'static>, Self::Error> {
self.body
.as_ref()
.map(|q| {
let q: &dyn QueryParameter = q;
Cow::Borrowed(q)
})
.ok_or(OidcError::Body)
}
fn authheader(&mut self) -> Result<Option<Cow<'_, str>>, Self::Error> {
Ok(self.auth.as_deref().map(Cow::Borrowed))
}
}
#[async_trait]
impl<S> FromRequest<S> for OidcRequest
where
S: Send + Sync,
{
type Rejection = OidcError;
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
let mut all_auth = req.headers().get_all(header::AUTHORIZATION).iter();
let optional = all_auth.next();
let auth = if all_auth.next().is_some() {
return Err(OidcError::Authorization);
} else {
optional.and_then(|hv| hv.to_str().ok().map(str::to_owned))
};
let (mut parts, body) = req.into_parts();
let query = Query::from_request_parts(&mut parts, state)
.await
.ok()
.map(|q: Query<NormalizedParameter>| q.0);
let req = Request::from_parts(parts, body);
let body = Form::from_request(req, state)
.await
.ok()
.map(|b: Form<NormalizedParameter>| b.0);
// If the query is empty and the body has a request, copy it over
// because login forms are POST requests but OAuth flow expects
// arguments in query.
let query = match query {
| None => body.clone(),
| Some(params) => {
//if params == NormalizedParameter::new() {
if params.unique_value("client_id").is_none() {
body.clone()
} else {
Some(params)
}
},
};
Ok(Self { auth, query, body })
}
}
-117
View File
@@ -1,117 +0,0 @@
use axum::{
body::Body,
http::{Response, StatusCode, header},
response::IntoResponse,
};
use oxide_auth::{
endpoint::{OwnerConsent, OwnerSolicitor, Solicitation, WebRequest, WebResponse},
frontends::simple::request::Body as OAuthRequestBody,
};
use url::Url;
use super::{LoginQuery, OidcError, OidcRequest, oidc_consent_form};
/// A Web response that can be processed by the OIDC authentication flow before
/// being sent over.
#[derive(Default, Clone, Debug)]
pub struct OidcResponse {
pub(crate) status: StatusCode,
pub(crate) location: Option<Url>,
pub(crate) www_authenticate: Option<String>,
pub(crate) body: Option<OAuthRequestBody>,
pub(crate) nonce: Option<String>,
}
impl IntoResponse for OidcResponse {
fn into_response(self) -> Response<Body> {
let csp_src = match self.nonce {
| Some(nonce) => &format!("default-src 'nonce-{nonce}';"),
| None => "default-src 'none';",
};
let csp_form_action =
"form-action 'self' http://localhost http://127.0.0.1 http://[::1];";
let content_csp = format!("{csp_src} {csp_form_action}");
let content_type = match self.body {
| Some(OAuthRequestBody::Json(_)) => "application/json",
| _ => "text/html",
};
let mut response = Response::builder()
.status(self.status)
.header(header::CONTENT_TYPE, content_type)
.header(header::CONTENT_SECURITY_POLICY, content_csp);
if let Some(location) = self.location {
response = response.header(header::LOCATION, location.as_str());
}
// Transform from OAuthRequestBody to String.
let body_content = self.body.map(|b| b.as_str().to_owned()).unwrap_or_default();
response.body(body_content.into()).unwrap()
}
}
/// OidcResponse uses [super::oidc_consent_form] to be turned into an owner
/// consent solicitation.
impl OwnerSolicitor<OidcRequest> for OidcResponse {
fn check_consent(
&mut self,
request: &mut OidcRequest,
_: Solicitation<'_>,
) -> OwnerConsent<<OidcRequest as WebRequest>::Response> {
// TODO find a way to pass the hostname to the template.
let hostname = "Continuwuity";
let query: LoginQuery = request
.clone()
.try_into()
.expect("login query from OidcRequest");
OwnerConsent::InProgress(oidc_consent_form(hostname, &query.into()))
}
}
impl WebResponse for OidcResponse {
type Error = OidcError;
fn ok(&mut self) -> Result<(), Self::Error> {
self.status = StatusCode::OK;
Ok(())
}
/// A response which will redirect the user-agent to which the response is
/// issued.
fn redirect(&mut self, url: Url) -> Result<(), Self::Error> {
self.status = StatusCode::FOUND;
self.location = Some(url);
Ok(())
}
/// Set the response status to 400.
fn client_error(&mut self) -> Result<(), Self::Error> {
self.status = StatusCode::BAD_REQUEST;
Ok(())
}
/// Set the response status to 401 and add a `WWW-Authenticate` header.
fn unauthorized(&mut self, header_value: &str) -> Result<(), Self::Error> {
self.status = StatusCode::UNAUTHORIZED;
self.www_authenticate = Some(header_value.to_owned());
Ok(())
}
/// A pure text response with no special media type set.
fn body_text(&mut self, text: &str) -> Result<(), Self::Error> {
self.body = Some(OAuthRequestBody::Text(text.to_owned()));
Ok(())
}
/// Json response data, with media type `aplication/json.
fn body_json(&mut self, data: &str) -> Result<(), Self::Error> {
self.body = Some(OAuthRequestBody::Json(data.to_owned()));
Ok(())
}
}
-16
View File
@@ -1,16 +0,0 @@
{% extends "_layout.html.j2" %}
{%- block content -%}
<div class="panel">
<h1 class-"project-name">{{ hostname }}</h1>
<p>
'{{ client_id }}' (at {{ redirect_uri }}) is requesting permission for '{{ scope }}'
</p>
<form method="post">
<input type="submit" value="Accept" formaction="{{ route }}?client_id={{ client_id }}{%- if let Some(secret)
= client_secret -%}&client_secret={{ secret }}{%- endif -%}&redirect_uri={{ redirect_uri }}&scope={{ scope
}}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method
}}&response_type={{ response_type }}&response_mode={{ response_mode }}&allow={{ user_id }}">
<input type="submit" value="Deny" formaction="{{ route }}?client_id={{ client_id }}{%- if let Some(secret) = client_secret -%}&client_secret={{ secret }}{%- endif -%}&redirect_uri={{ redirect_uri }}&scope={{scope }}&state={{ state }}&code_challenge={{ code_challenge }}&code_challenge_method={{ code_challenge_method }}&response_type={{ response_type }}&response_mode={{ response_mode }}&deny=true">
</form>
</div>
{%- endblock content -%}
-22
View File
@@ -1,22 +0,0 @@
{% extends "_layout.html.j2" %}
{%- block content -%}
<div class="panel">
<h1 class-"project-name">{{ hostname }}</h1>
<form action="{{ route }}" method="post">
<input type="text" name="username" placeholder="Username" required>
<input type="password" name="password" placeholder="Password" required>
<input type="hidden" name="client_id" value="{{ client_id }}">
{%- if let Some(secret) = client_secret -%}
<input type="hidden" name="client_secret" value="{{ secret }}">
{%- endif -%}
<input type="hidden" name="redirect_uri" value="{{ redirect_uri }}">
<input type="hidden" name="scope" value="{{ scope }}">
<input type="hidden" name="state" value="{{ state }}">
<input type="hidden" name="code_challenge" value="{{ code_challenge }}">
<input type="hidden" name="code_challenge_method" value="{{ code_challenge_method }}">
<input type="hidden" name="response_type" value="{{ response_type }}">
<input type="hidden" name="response_mode" value="{{ response_mode }}">
<button type="submit">Login</button>
</form>
</div>
{%- endblock content -%}