mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-05 15:41:37 +00:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96c069cd67 | |||
| 9a38395f0a | |||
| fdbb1c86b1 | |||
| 17f8ec21a3 | |||
| c46104ac0c | |||
| 900ba33a4e | |||
| f2e696ae3e | |||
| 1088aa5020 | |||
| 5c2a5de7d3 | |||
| 600b8cb366 | |||
| 3bbbfcdd46 | |||
| e9b387414f | |||
| 426b113c30 | |||
| ddae8af99f | |||
| 769db9b818 | |||
| 27d9d1d78d | |||
| 2e34ac9f59 | |||
| 95632103bc | |||
| d916bb9f21 | |||
| 4e50064740 | |||
| 56bd11013f | |||
| 8893bd1613 | |||
| e7b9446b5a | |||
| bc23071dd4 | |||
| b602be0921 | |||
| 0c333c9a05 | |||
| b9b3a466f4 | |||
| 3803f06392 | |||
| cadcbd7d49 | |||
| 144036f58b | |||
| 2afe656e12 | |||
| c89dfe38da | |||
| 1ce1254514 | |||
| 5bd1bedad0 | |||
| 68ca6eabe3 | |||
| e75f5cbbed | |||
| 97c692b052 | |||
| a586ea390c | |||
| dba528a5e0 | |||
| aafc93f6fb | |||
| cd0c3886fb | |||
| be4ccbc11b |
@@ -17,7 +17,7 @@ inputs:
|
||||
required: false
|
||||
default: ''
|
||||
rust-version:
|
||||
description: 'Rust version to install (e.g. nightly). Defaults to the version specified in rust-toolchain.toml'
|
||||
description: 'Rust version to install (e.g. nightly). Defaults to 1.87.0'
|
||||
required: false
|
||||
default: ''
|
||||
sccache-cache-limit:
|
||||
@@ -59,20 +59,9 @@ runs:
|
||||
mkdir -p "${{ github.workspace }}/target"
|
||||
mkdir -p "${{ github.workspace }}/.rustup"
|
||||
|
||||
- name: Start registry/toolchain restore group
|
||||
- name: Start cache restore group
|
||||
shell: bash
|
||||
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 }}
|
||||
run: echo "::group::📦 Restoring caches (registry, toolchain, build artifacts)"
|
||||
|
||||
- name: Cache Cargo registry and git
|
||||
id: registry-cache
|
||||
@@ -88,13 +77,58 @@ runs:
|
||||
restore-keys: |
|
||||
continuwuity-cargo-registry-${{ steps.runner-os.outputs.slug }}-${{ steps.runner-os.outputs.arch }}-
|
||||
|
||||
- name: End registry/toolchain restore group
|
||||
- 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
|
||||
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
|
||||
@@ -122,68 +156,8 @@ 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:
|
||||
@@ -237,9 +211,27 @@ runs:
|
||||
echo "CARGO_INCREMENTAL_GC_THRESHOLD=5" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Output version and summary
|
||||
- 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)"
|
||||
|
||||
@@ -46,9 +46,6 @@ 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:
|
||||
@@ -70,7 +67,3 @@ sync:
|
||||
target: ghcr.io/continuwuity/continuwuity
|
||||
type: repository
|
||||
<<: *tags-main
|
||||
- source: *source
|
||||
target: docker.io/jadedblueeyes/continuwuity
|
||||
type: repository
|
||||
<<: *tags-main
|
||||
|
||||
@@ -34,8 +34,6 @@ 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
|
||||
|
||||
@@ -3,6 +3,15 @@ concurrency:
|
||||
group: "release-image-${{ github.ref }}"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- "*.md"
|
||||
- "**/*.md"
|
||||
- ".gitlab-ci.yml"
|
||||
- ".gitignore"
|
||||
- "renovate.json"
|
||||
- "pkg/**"
|
||||
- "docs/**"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
name: Renovate
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/renovatebot/renovate:42.11.0@sha256:656c1e5b808279eac16c37b89562fb4c699e02fc7e219244f4a1fc2f0a7ce367
|
||||
image: ghcr.io/renovatebot/renovate:41.146.4@sha256:bb70194b7405faf10a6f279b60caa10403a440ba37d158c5a4ef0ae7b67a0f92
|
||||
options: --tmpfs /tmp:exec
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -7,3 +7,6 @@ f419c64aca300a338096b4e0db4c73ace54f23d0
|
||||
5998a0d883d31b866f7c8c46433a8857eae51a89
|
||||
# trailing whitespace and newlines
|
||||
46c193e74b2ce86c48ce802333a0aabce37fd6e9
|
||||
|
||||
# Formatting PRs
|
||||
fd972f114293ea1be9633b750a703edd661e970d
|
||||
|
||||
@@ -23,7 +23,7 @@ repos:
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/crate-ci/typos
|
||||
rev: v1.39.2
|
||||
rev: v1.39.0
|
||||
hooks:
|
||||
- id: typos
|
||||
- id: typos
|
||||
|
||||
Generated
+280
-173
@@ -17,19 +17,6 @@ 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"
|
||||
@@ -39,15 +26,6 @@ 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"
|
||||
@@ -72,6 +50,15 @@ 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"
|
||||
@@ -169,6 +156,12 @@ 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"
|
||||
@@ -178,15 +171,6 @@ 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"
|
||||
@@ -336,26 +320,6 @@ 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"
|
||||
@@ -535,9 +499,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "axum-server"
|
||||
version = "0.7.3"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1ab4a3ec9ea8a657c72d99a03a824af695bd0fb5ec639ccbd9cd3543b41a5f9"
|
||||
checksum = "495c05f60d6df0093e8fb6e74aa5846a0ad06abaf96d76166283720bf740f8ab"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"bytes",
|
||||
@@ -650,12 +614,9 @@ checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||
|
||||
[[package]]
|
||||
name = "bitstream-io"
|
||||
version = "4.9.0"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757"
|
||||
dependencies = [
|
||||
"core2",
|
||||
]
|
||||
checksum = "6099cdc01846bc367c4e7dd630dc5966dccf36b652fae7a74e17b640411a91b2"
|
||||
|
||||
[[package]]
|
||||
name = "blake2"
|
||||
@@ -666,6 +627,17 @@ 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"
|
||||
@@ -705,6 +677,12 @@ 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"
|
||||
@@ -743,9 +721,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "bytesize"
|
||||
version = "2.2.0"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c99fa31e08a43eaa5913ef68d7e01c37a2bdce6ed648168239ad33b7d30a9cd8"
|
||||
checksum = "f5c434ae3cf0089ca203e9019ebe529c47ff45cefe8af7c85ecb734ef541822f"
|
||||
|
||||
[[package]]
|
||||
name = "bzip2-sys"
|
||||
@@ -788,6 +766,16 @@ 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"
|
||||
@@ -815,7 +803,9 @@ 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]]
|
||||
@@ -831,9 +821,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.52"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "aa8120877db0e5c011242f96806ce3c94e0737ab8108532a76a3300a01db2ab8"
|
||||
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -850,9 +840,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.52"
|
||||
version = "4.5.51"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02576b399397b659c26064fbc92a75fede9d18ffd5f80ca1cd74ddab167016e1"
|
||||
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -940,7 +930,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_admin",
|
||||
@@ -972,7 +962,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
@@ -994,7 +984,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum 0.7.9",
|
||||
@@ -1004,6 +994,7 @@ dependencies = [
|
||||
"bytes",
|
||||
"conduwuit_core",
|
||||
"conduwuit_service",
|
||||
"conduwuit_web",
|
||||
"const-str",
|
||||
"ctor",
|
||||
"futures",
|
||||
@@ -1014,6 +1005,8 @@ dependencies = [
|
||||
"ipaddress",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"oxide-auth",
|
||||
"percent-encoding",
|
||||
"rand 0.8.5",
|
||||
"reqwest",
|
||||
"ruma",
|
||||
@@ -1027,14 +1020,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_build_metadata"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"built",
|
||||
"built 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_core"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
@@ -1095,7 +1088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_database"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"conduwuit_core",
|
||||
@@ -1114,7 +1107,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_macros"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
@@ -1124,7 +1117,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"axum 0.7.9",
|
||||
"axum-client-ip",
|
||||
@@ -1159,7 +1152,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_service"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
@@ -1180,6 +1173,8 @@ dependencies = [
|
||||
"log",
|
||||
"loole",
|
||||
"lru-cache",
|
||||
"once_cell",
|
||||
"oxide-auth",
|
||||
"rand 0.8.5",
|
||||
"recaptcha-verify",
|
||||
"regex",
|
||||
@@ -1200,16 +1195,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_web"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
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]]
|
||||
@@ -1273,6 +1274,12 @@ 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"
|
||||
@@ -1307,15 +1314,6 @@ 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"
|
||||
@@ -1478,9 +1476,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor"
|
||||
version = "0.6.1"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ffc71fcdcdb40d6f087edddf7f8f1f8f79e6cf922f555a9ee8779752d4819bd"
|
||||
checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb"
|
||||
dependencies = [
|
||||
"ctor-proc-macro",
|
||||
"dtor",
|
||||
@@ -1488,9 +1486,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ctor-proc-macro"
|
||||
version = "0.0.7"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52560adf09603e58c9a7ee1fe1dcb95a16927b17c127f0ac02d6e768a0e25bc1"
|
||||
checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2"
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
@@ -1687,24 +1685,6 @@ 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"
|
||||
@@ -1750,7 +1730,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1775,9 +1755,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "exr"
|
||||
version = "1.74.0"
|
||||
version = "1.73.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4300e043a56aa2cb633c01af81ca8f699a321879a7854d3896a0ba89056363be"
|
||||
checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0"
|
||||
dependencies = [
|
||||
"bit_field",
|
||||
"half",
|
||||
@@ -2056,9 +2036,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.0"
|
||||
version = "0.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f954a9e9159ec994f73a30a12b96a702dde78f5547bcb561174597924f7d4162"
|
||||
checksum = "4ae047235e33e2829703574b54fdec96bfbad892062d97fed2f76022287de61b"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
@@ -2405,12 +2385,36 @@ dependencies = [
|
||||
"libc",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"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"
|
||||
@@ -2520,9 +2524,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.9"
|
||||
version = "0.25.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a"
|
||||
checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
@@ -2538,8 +2542,8 @@ dependencies = [
|
||||
"rayon",
|
||||
"rgb",
|
||||
"tiff",
|
||||
"zune-core 0.5.0",
|
||||
"zune-jpeg 0.5.5",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3010,9 +3014,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "minicbor-serde"
|
||||
version = "0.6.2"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80047f75e28e3b38f6ab2ec3c2c7669f6b411fa6f8424e1a90a3fd784b19a3f4"
|
||||
checksum = "546cc904f35809921fa57016a84c97e68d9d27c012e87b9dadc28c233705f783"
|
||||
dependencies = [
|
||||
"minicbor",
|
||||
"serde",
|
||||
@@ -3364,6 +3368,27 @@ 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"
|
||||
@@ -3412,12 +3437,6 @@ 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"
|
||||
@@ -3682,7 +3701,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9120690fafc389a67ba3803df527d0ec9cbbc9cc45e4cc20b332996dfb672425"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"itertools 0.14.0",
|
||||
"itertools 0.12.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
@@ -3761,7 +3780,7 @@ dependencies = [
|
||||
"quinn-udp",
|
||||
"rustc-hash",
|
||||
"rustls",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"thiserror 2.0.17",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -3798,7 +3817,7 @@ dependencies = [
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
"once_cell",
|
||||
"socket2 0.6.1",
|
||||
"socket2 0.5.10",
|
||||
"tracing",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
@@ -3879,21 +3898,19 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
version = "0.8.1"
|
||||
version = "0.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43b6dd56e85d9483277cde964fd1bdb0428de4fec5ebba7540995639a21cb32b"
|
||||
checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9"
|
||||
dependencies = [
|
||||
"aligned-vec",
|
||||
"arbitrary",
|
||||
"arg_enum_proc_macro",
|
||||
"arrayvec",
|
||||
"av-scenechange",
|
||||
"av1-grain",
|
||||
"bitstream-io",
|
||||
"built",
|
||||
"built 0.7.7",
|
||||
"cfg-if",
|
||||
"interpolate_name",
|
||||
"itertools 0.14.0",
|
||||
"itertools 0.12.1",
|
||||
"libc",
|
||||
"libfuzzer-sys",
|
||||
"log",
|
||||
@@ -3902,21 +3919,23 @@ dependencies = [
|
||||
"noop_proc_macro",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"paste",
|
||||
"profiling",
|
||||
"rand 0.9.2",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"simd_helpers",
|
||||
"thiserror 2.0.17",
|
||||
"system-deps",
|
||||
"thiserror 1.0.69",
|
||||
"v_frame",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ravif"
|
||||
version = "0.12.0"
|
||||
version = "0.11.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef69c1990ceef18a116855938e74793a5f7496ee907562bd0857b6ac734ab285"
|
||||
checksum = "5825c26fddd16ab9f515930d49028a630efec172e903483c94796cfe31893e6b"
|
||||
dependencies = [
|
||||
"avif-serialize",
|
||||
"imgref",
|
||||
@@ -4068,6 +4087,28 @@ 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"
|
||||
@@ -4270,6 +4311,17 @@ 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"
|
||||
@@ -4337,7 +4389,7 @@ dependencies = [
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4643,20 +4695,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde-saphyr"
|
||||
version = "0.0.8"
|
||||
version = "0.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0916ccf524f1ccec1b3c02193c9e3d2e167aee9b6b294829dce6f4411332155"
|
||||
checksum = "fd76af9505b2498740576f95f60b3b4e2c469b5b677a8d2dd1d2da18b58193de"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"base64 0.22.1",
|
||||
"encoding_rs_io",
|
||||
"nohash-hasher",
|
||||
"num-traits",
|
||||
"ryu",
|
||||
"saphyr-parser",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec 2.0.0-alpha.12",
|
||||
"smallvec 2.0.0-alpha.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4880,9 +4930,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "2.0.0-alpha.12"
|
||||
version = "2.0.0-alpha.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef784004ca8777809dcdad6ac37629f0a97caee4c685fcea805278d81dd8b857"
|
||||
checksum = "87b96efa4bd6bdd2ff0c6615cc36fc4970cbae63cfd46ddff5cee35a1b4df570"
|
||||
|
||||
[[package]]
|
||||
name = "socket2"
|
||||
@@ -4974,9 +5024,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
version = "2.0.109"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
checksum = "2f17c7e013e88258aa9543dcbe81aca68a667a9ac37cd69c9fbc07858bfe0e2f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -5003,12 +5053,31 @@ 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"
|
||||
@@ -5106,7 +5175,7 @@ dependencies = [
|
||||
"half",
|
||||
"quick-error",
|
||||
"weezl",
|
||||
"zune-jpeg 0.4.21",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5731,6 +5800,12 @@ 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"
|
||||
@@ -5914,6 +5989,41 @@ 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"
|
||||
@@ -5926,6 +6036,24 @@ 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"
|
||||
@@ -6218,7 +6346,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
@@ -6227,7 +6355,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "xtask-generate-commands"
|
||||
version = "0.5.0-rc.8.1"
|
||||
version = "0.5.0-rc.8"
|
||||
dependencies = [
|
||||
"clap-markdown",
|
||||
"clap_builder",
|
||||
@@ -6236,12 +6364,6 @@ 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"
|
||||
@@ -6386,12 +6508,6 @@ 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"
|
||||
@@ -6407,14 +6523,5 @@ version = "0.4.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713"
|
||||
dependencies = [
|
||||
"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",
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
+15
-3
@@ -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.1"
|
||||
version = "0.5.0-rc.8"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
@@ -48,7 +48,7 @@ features = ["ffi", "std", "union"]
|
||||
version = "0.7.0"
|
||||
|
||||
[workspace.dependencies.ctor]
|
||||
version = "0.6.0"
|
||||
version = "0.5.0"
|
||||
|
||||
[workspace.dependencies.cargo_toml]
|
||||
version = "0.22"
|
||||
@@ -103,6 +103,9 @@ features = [
|
||||
"matched-path",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"query",
|
||||
# Needed for debug_handler.
|
||||
#"macros",
|
||||
]
|
||||
|
||||
[workspace.dependencies.axum-extra]
|
||||
@@ -167,7 +170,7 @@ features = ["raw_value"]
|
||||
|
||||
# Used for appservice registration files
|
||||
[workspace.dependencies.serde-saphyr]
|
||||
version = "0.0.8"
|
||||
version = "0.0.7"
|
||||
|
||||
# Used to load forbidden room/user regex from config
|
||||
[workspace.dependencies.serde_regex]
|
||||
@@ -369,6 +372,7 @@ features = [
|
||||
"unstable-msc2666",
|
||||
"unstable-msc2867",
|
||||
"unstable-msc2870",
|
||||
"unstable-msc2965",
|
||||
"unstable-msc3026",
|
||||
"unstable-msc3061",
|
||||
"unstable-msc3245",
|
||||
@@ -557,6 +561,14 @@ 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
|
||||
#
|
||||
|
||||
@@ -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 the official community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
|
||||
It's a community continuation of the [conduwuit](https://github.com/girlbossceo/conduwuit) homeserver.
|
||||
|
||||
<!-- ANCHOR: body -->
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ description = "continuwuity is a community continuation of the conduwuit Matrix
|
||||
language = "en"
|
||||
authors = ["The continuwuity Community"]
|
||||
text-direction = "ltr"
|
||||
multilingual = false
|
||||
src = "docs"
|
||||
|
||||
[build]
|
||||
@@ -17,7 +18,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 = "fab-git-alt"
|
||||
git-repository-icon = "fa-git-alt"
|
||||
|
||||
[output.html.search]
|
||||
limit-results = 15
|
||||
|
||||
+20
-1
@@ -23,7 +23,8 @@
|
||||
# See the docs for reverse proxying and delegation:
|
||||
# https://continuwuity.org/deploying/generic.html#setting-up-the-reverse-proxy
|
||||
#
|
||||
# Also see the `[global.well_known]` config section at the very bottom.
|
||||
# Also see the `[global.auth]` and `[global.well_known]` config sections
|
||||
# at the very bottom.
|
||||
#
|
||||
# Examples of delegation:
|
||||
# - https://puppygock.gay/.well-known/matrix/server
|
||||
@@ -1754,6 +1755,24 @@
|
||||
#
|
||||
#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
@@ -48,7 +48,7 @@ EOF
|
||||
|
||||
# Developer tool versions
|
||||
# renovate: datasource=github-releases depName=cargo-bins/cargo-binstall
|
||||
ENV BINSTALL_VERSION=1.15.11
|
||||
ENV BINSTALL_VERSION=1.15.10
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -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.11
|
||||
ENV BINSTALL_VERSION=1.15.10
|
||||
# renovate: datasource=github-releases depName=psastras/sbom-rs
|
||||
ENV CARGO_SBOM_VERSION=0.9.1
|
||||
# renovate: datasource=crate depName=lddtree
|
||||
|
||||
@@ -17,5 +17,3 @@ ## systemd unit file
|
||||
```
|
||||
{{#include ../../pkg/conduwuit.service}}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
Vendored
-4
@@ -8,10 +8,6 @@
|
||||
{
|
||||
"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!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -94,6 +94,9 @@ 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
|
||||
|
||||
@@ -75,7 +75,9 @@ 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"
|
||||
"Device ID in keys uploaded ({}) does not match your own device ID ({})",
|
||||
deser_device_keys.device_id,
|
||||
sender_device,
|
||||
)));
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
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;
|
||||
@@ -58,6 +59,7 @@
|
||||
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::*;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
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:?}"))))
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
/// 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)))
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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:?}"))))
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! 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,
|
||||
};
|
||||
@@ -0,0 +1,139 @@
|
||||
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))
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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:?}")))),
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,9 @@
|
||||
use futures::StreamExt;
|
||||
use ruma::api::client::{
|
||||
discovery::{
|
||||
discover_homeserver::{self, HomeserverInfo, SlidingSyncProxyInfo},
|
||||
discover_homeserver::{
|
||||
self, AuthenticationServerInfo, HomeserverInfo, SlidingSyncProxyInfo,
|
||||
},
|
||||
discover_support::{self, Contact},
|
||||
},
|
||||
error::ErrorKind,
|
||||
@@ -26,8 +28,16 @@ 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 }),
|
||||
sliding_sync_proxy: Some(SlidingSyncProxyInfo { url: client_url.clone() }),
|
||||
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")),
|
||||
))
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -115,6 +115,21 @@ 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)
|
||||
|
||||
@@ -289,6 +289,15 @@ 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(())
|
||||
}
|
||||
|
||||
|
||||
+25
-2
@@ -53,7 +53,7 @@
|
||||
### For more information, see:
|
||||
### https://continuwuity.org/configuration.html
|
||||
"#,
|
||||
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"
|
||||
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"
|
||||
)]
|
||||
pub struct Config {
|
||||
/// The server_name is the pretty name of this server. It is used as a
|
||||
@@ -62,7 +62,8 @@ 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.well_known]` config section at the very bottom.
|
||||
/// Also see the `[global.auth]` and `[global.well_known]` config sections
|
||||
/// at the very bottom.
|
||||
///
|
||||
/// Examples of delegation:
|
||||
/// - https://puppygock.gay/.well-known/matrix/server
|
||||
@@ -104,6 +105,9 @@ 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
|
||||
@@ -2020,6 +2024,25 @@ 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")]
|
||||
|
||||
@@ -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 (unconflicted, conflicting) = separate(state_sets.into_iter());
|
||||
let (clean, conflicting) = separate(state_sets.into_iter());
|
||||
|
||||
debug!(count = unconflicted.len(), "non-conflicting events");
|
||||
trace!(map = ?unconflicted, "non-conflicting events");
|
||||
debug!(count = clean.len(), "non-conflicting events");
|
||||
trace!(map = ?clean, "non-conflicting events");
|
||||
|
||||
if conflicting.is_empty() {
|
||||
debug!("no conflicting state found");
|
||||
return Ok(unconflicted);
|
||||
return Ok(clean);
|
||||
}
|
||||
|
||||
debug!(count = conflicting.len(), "conflicting events");
|
||||
trace!(map = ?conflicting, "conflicting events");
|
||||
let (conflicted_state_subgraph, initial_state) =
|
||||
if stateres_version == StateResolutionVersion::V2_1 {
|
||||
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
let conflicted_state_subgraph: HashSet<_> = match stateres_version {
|
||||
| StateResolutionVersion::V2_1 =>
|
||||
calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
|
||||
})?;
|
||||
debug!(count = csg.len(), "conflicted subgraph");
|
||||
trace!(set = ?csg, "conflicted subgraph");
|
||||
(csg, HashMap::new())
|
||||
} else {
|
||||
(HashSet::new(), unconflicted.clone())
|
||||
};
|
||||
})?,
|
||||
| _ => 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();
|
||||
|
||||
// `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.into_values().flatten().stream())
|
||||
.broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id))
|
||||
.chain(conflicting_values)
|
||||
.chain(conflicted_state_subgraph.into_iter().stream())
|
||||
.broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id))
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
@@ -169,8 +169,9 @@ 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),
|
||||
initial_state,
|
||||
clean.clone(),
|
||||
&event_fetch,
|
||||
)
|
||||
.await?;
|
||||
@@ -200,7 +201,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);
|
||||
|
||||
trace!(event_id = ?power_event, "power event");
|
||||
debug!(event_id = ?power_event, "power event");
|
||||
|
||||
let sorted_left_events =
|
||||
mainline_sort(&events_to_resolve, power_event.cloned(), &event_fetch).await?;
|
||||
@@ -209,14 +210,15 @@ 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, // The control events are added to the final resolved state
|
||||
resolved_control.clone(), // The control events are added to the final resolved state
|
||||
&event_fetch,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Ensure unconflicting state is in the final state
|
||||
resolved_state.extend(unconflicted);
|
||||
resolved_state.extend(clean);
|
||||
|
||||
debug!("state resolution finished");
|
||||
trace!( map = ?resolved_state, "final resolved state" );
|
||||
@@ -596,6 +598,7 @@ 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,
|
||||
@@ -620,10 +623,6 @@ 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()
|
||||
@@ -644,11 +643,10 @@ 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;
|
||||
// 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;
|
||||
let mut resolved_state = match stateres_version {
|
||||
| StateResolutionVersion::V2_1 => StateMap::new(),
|
||||
| _ => unconflicted_state,
|
||||
};
|
||||
for event in events_to_check {
|
||||
trace!(event_id = event.event_id().as_str(), "checking event");
|
||||
let state_key = event
|
||||
@@ -1036,6 +1034,7 @@ 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,
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -434,6 +434,14 @@ 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
|
||||
|
||||
@@ -125,6 +125,8 @@ 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
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
pub mod key_backups;
|
||||
pub mod media;
|
||||
pub mod moderation;
|
||||
pub mod oidc;
|
||||
pub mod presence;
|
||||
pub mod pusher;
|
||||
pub mod resolver;
|
||||
|
||||
@@ -0,0 +1,361 @@
|
||||
//! 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)
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
account_data, admin, announcements, appservice, client, config, emergency, federation,
|
||||
globals, key_backups,
|
||||
manager::Manager,
|
||||
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
|
||||
media, moderation, oidc, presence, pusher, resolver, rooms, sending, server_keys, service,
|
||||
service::{Args, Map, Service},
|
||||
sync, transaction_ids, uiaa, users,
|
||||
};
|
||||
@@ -39,6 +39,7 @@ 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>,
|
||||
@@ -107,6 +108,7 @@ macro_rules! build {
|
||||
users: build!(users::Service),
|
||||
moderation: build!(moderation::Service),
|
||||
announcements: build!(announcements::Service),
|
||||
oidc: build!(oidc::Service),
|
||||
|
||||
manager: Mutex::new(None),
|
||||
service,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{Dep, account_data, admin, appservice, globals, rooms};
|
||||
use crate::{Dep, account_data, admin, appservice, globals, oidc, rooms};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct UserSuspension {
|
||||
@@ -52,6 +52,7 @@ 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>,
|
||||
}
|
||||
@@ -89,6 +90,7 @@ 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"),
|
||||
@@ -270,7 +272,18 @@ 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)> {
|
||||
self.db.token_userdeviceid.get(token).await.deserialized()
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an iterator over all users on this homeserver (offered for
|
||||
@@ -536,7 +549,24 @@ 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.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
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() {
|
||||
return Err!(Database(error!(
|
||||
?user_id,
|
||||
?device_id,
|
||||
|
||||
@@ -22,6 +22,8 @@ crate-type = [
|
||||
[dependencies]
|
||||
conduwuit-build-metadata.workspace = true
|
||||
conduwuit-service.workspace = true
|
||||
conduwuit-core.workspace = true
|
||||
async-trait.workspace = true
|
||||
|
||||
askama = "0.14.0"
|
||||
|
||||
@@ -30,6 +32,10 @@ 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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
};
|
||||
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();
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
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()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
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) }
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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")
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
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 })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
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(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{% 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 -%}
|
||||
@@ -0,0 +1,22 @@
|
||||
{% 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 -%}
|
||||
Reference in New Issue
Block a user