Compare commits
6 commits
main
...
neilalexan
Author | SHA1 | Date | |
---|---|---|---|
d08d319a9d | |||
0d74b296f1 | |||
1b49a9deff | |||
35f417627d | |||
ee4d8d6de7 | |||
b5f7d7adcb |
|
@ -1,2 +1,3 @@
|
||||||
bin
|
bin
|
||||||
*.wasm
|
*.wasm
|
||||||
|
.git
|
|
@ -1,59 +0,0 @@
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
|
|
||||||
env:
|
|
||||||
GHCR_NAMESPACE: sigb.us
|
|
||||||
PLATFORMS: linux/amd64
|
|
||||||
FORGEJO_USER: signaryk
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
monolith:
|
|
||||||
name: Monolith image
|
|
||||||
runs-on: docker
|
|
||||||
container:
|
|
||||||
image: ghcr.io/catthehacker/ubuntu:act-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Get release tag & build flags
|
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
|
||||||
run: |
|
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to sigb.us container registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.sigb.us
|
|
||||||
username: ${{ env.FORGEJO_USER }}
|
|
||||||
password: ${{ secrets.FORGEJO_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build main monolith image
|
|
||||||
id: docker_build_monolith
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ github.ref_name }}
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:devel
|
|
||||||
|
|
||||||
- name: Build release monolith image
|
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
|
||||||
id: docker_build_monolith_release
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:latest
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:stable
|
|
||||||
git.sigb.us/${{ env.GHCR_NAMESPACE }}/dendrite:${{ env.RELEASE_VERSION }}
|
|
18
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
18
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
@ -7,27 +7,24 @@ about: Create a report to help us improve
|
||||||
<!--
|
<!--
|
||||||
All bug reports must provide the following background information
|
All bug reports must provide the following background information
|
||||||
Text between <!-- and --> marks will be invisible in the report.
|
Text between <!-- and --> marks will be invisible in the report.
|
||||||
|
|
||||||
IF YOUR ISSUE IS CONSIDERED A SECURITY VULNERABILITY THEN PLEASE STOP
|
|
||||||
AND DO NOT POST IT AS A GITHUB ISSUE! Please report the issue responsibly by
|
|
||||||
disclosing in private by email to security@matrix.org instead. For more details, please
|
|
||||||
see: https://www.matrix.org/security-disclosure-policy/
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Background information
|
### Background information
|
||||||
<!-- Please include versions of all software when known e.g database versions, docker versions, client versions -->
|
<!-- Please include versions of all software when known e.g database versions, docker versions, client versions -->
|
||||||
- **Dendrite version or git SHA**:
|
- **Dendrite version or git SHA**:
|
||||||
|
- **Monolith or Polylith?**:
|
||||||
- **SQLite3 or Postgres?**:
|
- **SQLite3 or Postgres?**:
|
||||||
- **Running in Docker?**:
|
- **Running in Docker?**:
|
||||||
- **`go version`**:
|
- **`go version`**:
|
||||||
- **Client used (if applicable)**:
|
- **Client used (if applicable)**:
|
||||||
|
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
- **What** is the problem:
|
- **What** is the problem:
|
||||||
- **Who** is affected:
|
- **Who** is affected:
|
||||||
- **How** is this bug manifesting:
|
- **How** is this bug manifesting:
|
||||||
- **When** did this first appear:
|
- **When** did this first appear:
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Examples of good descriptions:
|
Examples of good descriptions:
|
||||||
|
@ -41,6 +38,7 @@ Examples of good descriptions:
|
||||||
- How: "Lots of logs about device change updates"
|
- How: "Lots of logs about device change updates"
|
||||||
- When: "After my server joined Matrix HQ"
|
- When: "After my server joined Matrix HQ"
|
||||||
|
|
||||||
|
|
||||||
Examples of bad descriptions:
|
Examples of bad descriptions:
|
||||||
- What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail?
|
- What: "Can't send messages" - This is bad because it isn't specfic enough. Which endpoint isn't working and what is the response code? Does the message send but encryption fail?
|
||||||
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server?
|
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server?
|
||||||
|
@ -62,6 +60,6 @@ If you can identify any relevant log snippets from server logs, please include
|
||||||
those (please be careful to remove any personal or private data). Please surround them with
|
those (please be careful to remove any personal or private data). Please surround them with
|
||||||
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
``` (three backticks, on a line on their own), so that they are formatted legibly.
|
||||||
|
|
||||||
Alternatively, please send logs to @kegan:matrix.org, @s7evink:matrix.org or @devonh:one.ems.host
|
Alternatively, please send logs to @kegan:matrix.org or @neilalexander:matrix.org
|
||||||
with a link to the respective Github issue, thanks!
|
with a link to the respective Github issue, thanks!
|
||||||
-->
|
-->
|
||||||
|
|
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
@ -1,8 +1,8 @@
|
||||||
### Pull Request Checklist
|
### Pull Request Checklist
|
||||||
|
|
||||||
<!-- Please read https://matrix-org.github.io/dendrite/development/contributing before submitting your pull request -->
|
<!-- Please read docs/CONTRIBUTING.md before submitting your pull request -->
|
||||||
|
|
||||||
* [ ] I have added Go unit tests or [Complement integration tests](https://github.com/matrix-org/complement) for this PR _or_ I have justified why this PR doesn't need tests
|
* [ ] I have added added tests for PR _or_ I have justified why this PR doesn't need tests.
|
||||||
* [ ] Pull request includes a [sign off below using a legally identifiable name](https://matrix-org.github.io/dendrite/development/contributing#sign-off) _or_ I have already signed off privately
|
* [ ] Pull request includes a [sign off](https://github.com/matrix-org/dendrite/blob/main/docs/CONTRIBUTING.md#sign-off)
|
||||||
|
|
||||||
Signed-off-by: `Your Name <your@email.example.org>`
|
Signed-off-by: `Your Name <your@email.example.org>`
|
||||||
|
|
20
.github/codecov.yaml
vendored
20
.github/codecov.yaml
vendored
|
@ -1,20 +0,0 @@
|
||||||
flag_management:
|
|
||||||
default_rules:
|
|
||||||
carryforward: true
|
|
||||||
|
|
||||||
coverage:
|
|
||||||
status:
|
|
||||||
project:
|
|
||||||
default:
|
|
||||||
target: auto
|
|
||||||
threshold: 0.1%
|
|
||||||
base: auto
|
|
||||||
flags:
|
|
||||||
- unittests
|
|
||||||
patch:
|
|
||||||
default:
|
|
||||||
target: 75%
|
|
||||||
threshold: 0%
|
|
||||||
base: auto
|
|
||||||
flags:
|
|
||||||
- unittests
|
|
259
.github/workflows/dendrite.yml
vendored
259
.github/workflows/dendrite.yml
vendored
|
@ -4,15 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
|
||||||
- '**.go' # only execute on changes to go files
|
|
||||||
- 'go.sum' # or dependency updates
|
|
||||||
- '.github/workflows/**' # or workflow changes
|
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
|
||||||
- '**.go'
|
|
||||||
- 'go.sum' # or dependency updates
|
|
||||||
- '.github/workflows/**'
|
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
@ -28,20 +20,28 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ false }} # disable for now
|
if: ${{ false }} # disable for now
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: 1.18
|
||||||
cache: true
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-wasm-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-wasm
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: 14
|
node-version: 14
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v2
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
@ -66,20 +66,18 @@ jobs:
|
||||||
name: Linting
|
name: Linting
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Install libolm
|
|
||||||
run: sudo apt-get install libolm-dev libolm3
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: 1.18
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v3
|
uses: golangci/golangci-lint-action@v3
|
||||||
|
|
||||||
# run go test with different go versions
|
# run go test with different go versions
|
||||||
test:
|
test:
|
||||||
timeout-minutes: 10
|
timeout-minutes: 5
|
||||||
name: Unit tests
|
name: Unit tests (Go ${{ matrix.go }})
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
# Service containers to run with `container-job`
|
# Service containers to run with `container-job`
|
||||||
services:
|
services:
|
||||||
|
@ -101,29 +99,25 @@ jobs:
|
||||||
--health-interval 10s
|
--health-interval 10s
|
||||||
--health-timeout 5s
|
--health-timeout 5s
|
||||||
--health-retries 5
|
--health-retries 5
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
go: ["1.18", "1.19"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Install libolm
|
|
||||||
run: sudo apt-get install libolm-dev libolm3
|
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: ${{ matrix.go }}
|
||||||
- uses: actions/cache@v4
|
- uses: actions/cache@v3
|
||||||
# manually set up caches, as they otherwise clash with different steps using setup-go with cache=true
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-stable-unit-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go${{ matrix.go }}-test-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-stable-unit-
|
${{ runner.os }}-go${{ matrix.go }}-test-
|
||||||
- name: Set up gotestfmt
|
- run: go test ./...
|
||||||
uses: gotesttools/gotestfmt-action@v2
|
|
||||||
with:
|
|
||||||
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- run: go test -json -v ./... 2>&1 | gotestfmt -hide all
|
|
||||||
env:
|
env:
|
||||||
POSTGRES_HOST: localhost
|
POSTGRES_HOST: localhost
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
|
@ -138,25 +132,26 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
|
go: ["1.18", "1.19"]
|
||||||
goos: ["linux"]
|
goos: ["linux"]
|
||||||
goarch: ["amd64", "386"]
|
goarch: ["amd64", "386"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: ${{ matrix.go }}
|
||||||
- uses: actions/cache@v4
|
- name: Install dependencies x86
|
||||||
|
if: ${{ matrix.goarch == '386' }}
|
||||||
|
run: sudo apt update && sudo apt-get install -y gcc-multilib
|
||||||
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goarch }}-
|
||||||
- name: Install dependencies x86
|
|
||||||
if: ${{ matrix.goarch == '386' }}
|
|
||||||
run: sudo apt update && sudo apt-get install -y gcc-multilib
|
|
||||||
- env:
|
- env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
@ -171,24 +166,25 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
|
go: ["1.18", "1.19"]
|
||||||
goos: ["windows"]
|
goos: ["windows"]
|
||||||
goarch: ["amd64"]
|
goarch: ["amd64"]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup Go
|
- name: Setup Go ${{ matrix.go }}
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: ${{ matrix.go }}
|
||||||
- uses: actions/cache@v4
|
- name: Install dependencies
|
||||||
|
run: sudo apt update && sudo apt install -y gcc-mingw-w64-x86-64 # install required gcc
|
||||||
|
- uses: actions/cache@v3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goos }}-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
${{ runner.os }}-go${{ matrix.go }}-${{ matrix.goos }}
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y gcc-mingw-w64-x86-64 # install required gcc
|
|
||||||
- env:
|
- env:
|
||||||
GOOS: ${{ matrix.goos }}
|
GOOS: ${{ matrix.goos }}
|
||||||
GOARCH: ${{ matrix.goarch }}
|
GOARCH: ${{ matrix.goarch }}
|
||||||
|
@ -208,66 +204,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
jobs: ${{ toJSON(needs) }}
|
jobs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
# run go test with different go versions
|
|
||||||
integration:
|
|
||||||
timeout-minutes: 20
|
|
||||||
needs: initial-tests-done
|
|
||||||
name: Integration tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
# Service containers to run with `container-job`
|
|
||||||
services:
|
|
||||||
# Label used to access the service container
|
|
||||||
postgres:
|
|
||||||
# Docker Hub image
|
|
||||||
image: postgres:13-alpine
|
|
||||||
# Provide the password for postgres
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: dendrite
|
|
||||||
ports:
|
|
||||||
# Maps tcp port 5432 on service container to the host
|
|
||||||
- 5432:5432
|
|
||||||
# Set health checks to wait until postgres has started
|
|
||||||
options: >-
|
|
||||||
--health-cmd pg_isready
|
|
||||||
--health-interval 10s
|
|
||||||
--health-timeout 5s
|
|
||||||
--health-retries 5
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install libolm
|
|
||||||
run: sudo apt-get install libolm-dev libolm3
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: "stable"
|
|
||||||
- name: Set up gotestfmt
|
|
||||||
uses: gotesttools/gotestfmt-action@v2
|
|
||||||
with:
|
|
||||||
# Optional: pass GITHUB_TOKEN to avoid rate limiting.
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-stable-test-race-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-stable-test-race-
|
|
||||||
- run: go test -race -json -v -coverpkg=./... -coverprofile=cover.out $(go list ./... | grep -v /cmd/dendrite*) 2>&1 | gotestfmt -hide all
|
|
||||||
env:
|
|
||||||
POSTGRES_HOST: localhost
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: dendrite
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
flags: unittests
|
|
||||||
fail_ci_if_error: true
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
# run database upgrade tests
|
# run database upgrade tests
|
||||||
upgrade_test:
|
upgrade_test:
|
||||||
name: Upgrade tests
|
name: Upgrade tests
|
||||||
|
@ -275,28 +211,23 @@ jobs:
|
||||||
needs: initial-tests-done
|
needs: initial-tests-done
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "1.18"
|
||||||
cache: true
|
- uses: actions/cache@v3
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-upgrade-test-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-upgrade-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-upgrade-test-
|
${{ runner.os }}-go-upgrade
|
||||||
- name: Docker version
|
|
||||||
run: docker version
|
|
||||||
- name: Build upgrade-tests
|
- name: Build upgrade-tests
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
- name: Test upgrade (PostgreSQL)
|
- name: Test upgrade
|
||||||
run: ./dendrite-upgrade-tests --head .
|
run: ./dendrite-upgrade-tests --head .
|
||||||
- name: Test upgrade (SQLite)
|
|
||||||
run: ./dendrite-upgrade-tests --sqlite --head .
|
|
||||||
|
|
||||||
# run database upgrade tests, skipping over one version
|
# run database upgrade tests, skipping over one version
|
||||||
upgrade_test_direct:
|
upgrade_test_direct:
|
||||||
|
@ -305,27 +236,22 @@ jobs:
|
||||||
needs: initial-tests-done
|
needs: initial-tests-done
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v3
|
||||||
- name: Setup go
|
- name: Setup go
|
||||||
uses: actions/setup-go@v4
|
uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: "stable"
|
go-version: "1.18"
|
||||||
cache: true
|
- uses: actions/cache@v3
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.cache/go-build
|
~/.cache/go-build
|
||||||
~/go/pkg/mod
|
~/go/pkg/mod
|
||||||
key: ${{ runner.os }}-go-upgrade-direct-test-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-upgrade-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-upgrade-direct-test-
|
${{ runner.os }}-go-upgrade
|
||||||
- name: Docker version
|
|
||||||
run: docker version
|
|
||||||
- name: Build upgrade-tests
|
- name: Build upgrade-tests
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
run: go build ./cmd/dendrite-upgrade-tests
|
||||||
- name: Test upgrade (PostgreSQL)
|
- name: Test upgrade
|
||||||
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
|
||||||
- name: Test upgrade (SQLite)
|
|
||||||
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
||||||
|
|
||||||
# run Sytest in different variations
|
# run Sytest in different variations
|
||||||
|
@ -338,34 +264,27 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- label: SQLite native
|
- label: SQLite
|
||||||
|
|
||||||
- label: SQLite Cgo
|
- label: SQLite, full HTTP APIs
|
||||||
cgo: 1
|
api: full-http
|
||||||
|
|
||||||
- label: PostgreSQL
|
- label: PostgreSQL
|
||||||
postgres: postgres
|
postgres: postgres
|
||||||
|
|
||||||
|
- label: PostgreSQL, full HTTP APIs
|
||||||
|
postgres: postgres
|
||||||
|
api: full-http
|
||||||
container:
|
container:
|
||||||
image: matrixdotorg/sytest-dendrite
|
image: matrixdotorg/sytest-dendrite:latest
|
||||||
volumes:
|
volumes:
|
||||||
- ${{ github.workspace }}:/src
|
- ${{ github.workspace }}:/src
|
||||||
- /root/.cache/go-build:/github/home/.cache/go-build
|
|
||||||
- /root/.cache/go-mod:/gopath/pkg/mod
|
|
||||||
env:
|
env:
|
||||||
POSTGRES: ${{ matrix.postgres && 1}}
|
POSTGRES: ${{ matrix.postgres && 1}}
|
||||||
|
API: ${{ matrix.api && 1 }}
|
||||||
SYTEST_BRANCH: ${{ github.head_ref }}
|
SYTEST_BRANCH: ${{ github.head_ref }}
|
||||||
CGO_ENABLED: ${{ matrix.cgo && 1 }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v2
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
/gopath/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-sytest-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-sytest-
|
|
||||||
- name: Run Sytest
|
- name: Run Sytest
|
||||||
run: /bootstrap.sh dendrite
|
run: /bootstrap.sh dendrite
|
||||||
working-directory: /src
|
working-directory: /src
|
||||||
|
@ -381,7 +300,7 @@ jobs:
|
||||||
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
||||||
continue-on-error: true # not fatal
|
continue-on-error: true # not fatal
|
||||||
- name: Upload Sytest logs
|
- name: Upload Sytest logs
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v2
|
||||||
if: ${{ always() }}
|
if: ${{ always() }}
|
||||||
with:
|
with:
|
||||||
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
name: Sytest Logs - ${{ job.status }} - (Dendrite, ${{ join(matrix.*, ', ') }})
|
||||||
|
@ -399,15 +318,17 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- label: SQLite native
|
- label: SQLite
|
||||||
cgo: 0
|
|
||||||
|
|
||||||
- label: SQLite Cgo
|
- label: SQLite, full HTTP APIs
|
||||||
cgo: 1
|
api: full-http
|
||||||
|
|
||||||
- label: PostgreSQL
|
- label: PostgreSQL
|
||||||
postgres: Postgres
|
postgres: Postgres
|
||||||
cgo: 0
|
|
||||||
|
- label: PostgreSQL, full HTTP APIs
|
||||||
|
postgres: Postgres
|
||||||
|
api: full-http
|
||||||
steps:
|
steps:
|
||||||
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
||||||
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
||||||
|
@ -415,14 +336,16 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
||||||
echo "~/go/bin" >> $GITHUB_PATH
|
echo "~/go/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
- name: "Install Complement Dependencies"
|
- name: "Install Complement Dependencies"
|
||||||
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
||||||
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
||||||
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
go get -v github.com/haveyoudebuggedit/gotestfmt/v2/cmd/gotestfmt@latest
|
||||||
- name: Run actions/checkout@v4 for dendrite
|
|
||||||
uses: actions/checkout@v4
|
- name: Run actions/checkout@v2 for dendrite
|
||||||
|
uses: actions/checkout@v2
|
||||||
with:
|
with:
|
||||||
path: dendrite
|
path: dendrite
|
||||||
|
|
||||||
|
@ -446,10 +369,12 @@ jobs:
|
||||||
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
|
|
||||||
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
||||||
done
|
done
|
||||||
|
|
||||||
# Build initial Dendrite image
|
# Build initial Dendrite image
|
||||||
- run: docker build --build-arg=CGO=${{ matrix.cgo }} -t complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }} -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
- run: docker build -t complement-dendrite -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
||||||
working-directory: dendrite
|
working-directory: dendrite
|
||||||
env:
|
env:
|
||||||
DOCKER_BUILDKIT: 1
|
DOCKER_BUILDKIT: 1
|
||||||
|
@ -457,12 +382,12 @@ jobs:
|
||||||
# Run Complement
|
# Run Complement
|
||||||
- run: |
|
- run: |
|
||||||
set -o pipefail &&
|
set -o pipefail &&
|
||||||
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
go test -v -json -tags dendrite_blacklist ./tests/... 2>&1 | gotestfmt
|
||||||
shell: bash
|
shell: bash
|
||||||
name: Run Complement Tests
|
name: Run Complement Tests
|
||||||
env:
|
env:
|
||||||
COMPLEMENT_BASE_IMAGE: complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }}
|
COMPLEMENT_BASE_IMAGE: complement-dendrite:latest
|
||||||
COMPLEMENT_SHARE_ENV_PREFIX: COMPLEMENT_DENDRITE_
|
API: ${{ matrix.api && 1 }}
|
||||||
working-directory: complement
|
working-directory: complement
|
||||||
|
|
||||||
integration-tests-done:
|
integration-tests-done:
|
||||||
|
@ -474,7 +399,6 @@ jobs:
|
||||||
upgrade_test_direct,
|
upgrade_test_direct,
|
||||||
sytest,
|
sytest,
|
||||||
complement,
|
complement,
|
||||||
integration
|
|
||||||
]
|
]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
||||||
|
@ -489,7 +413,6 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
packages: write
|
packages: write
|
||||||
contents: read
|
contents: read
|
||||||
security-events: write # To upload Trivy sarif files
|
|
||||||
if: github.repository == 'matrix-org/dendrite' && github.ref_name == 'main'
|
if: github.repository == 'matrix-org/dendrite' && github.ref_name == 'main'
|
||||||
needs: [integration-tests-done]
|
needs: [integration-tests-done]
|
||||||
uses: matrix-org/dendrite/.github/workflows/docker.yml@main
|
uses: matrix-org/dendrite/.github/workflows/docker.yml@main
|
||||||
|
|
146
.github/workflows/docker.yml
vendored
146
.github/workflows/docker.yml
vendored
|
@ -24,25 +24,23 @@ jobs:
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
security-events: write # To upload Trivy sarif files
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
- name: Get release tag & build flags
|
- name: Get release tag
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: |
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -51,11 +49,12 @@ jobs:
|
||||||
- name: Build main monolith image
|
- name: Build main monolith image
|
||||||
if: github.ref_name == 'main'
|
if: github.ref_name == 'main'
|
||||||
id: docker_build_monolith
|
id: docker_build_monolith
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache
|
cache-from: type=gha
|
||||||
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.monolith
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -65,11 +64,12 @@ jobs:
|
||||||
- name: Build release monolith image
|
- name: Build release monolith image
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
id: docker_build_monolith_release
|
id: docker_build_monolith_release
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.monolith
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
|
@ -78,136 +78,62 @@ jobs:
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:latest
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:latest
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
polylith:
|
||||||
uses: aquasecurity/trivy-action@master
|
name: Polylith image
|
||||||
with:
|
|
||||||
image-ref: ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ github.ref_name }}
|
|
||||||
format: "sarif"
|
|
||||||
output: "trivy-results.sarif"
|
|
||||||
|
|
||||||
- name: Upload Trivy scan results to GitHub Security tab
|
|
||||||
uses: github/codeql-action/upload-sarif@v2
|
|
||||||
with:
|
|
||||||
sarif_file: "trivy-results.sarif"
|
|
||||||
|
|
||||||
demo-pinecone:
|
|
||||||
name: Pinecone demo image
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
- name: Get release tag & build flags
|
- name: Get release tag
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
run: |
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v1
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
- name: Login to GitHub Containers
|
- name: Login to GitHub Containers
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v1
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build main Pinecone demo image
|
- name: Build main polylith image
|
||||||
if: github.ref_name == 'main'
|
if: github.ref_name == 'main'
|
||||||
id: docker_build_demo_pinecone
|
id: docker_build_polylith
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
file: ./build/docker/Dockerfile.polylith
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:${{ github.ref_name }}
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Build release Pinecone demo image
|
- name: Build release polylith image
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
if: github.event_name == 'release' # Only for GitHub releases
|
||||||
id: docker_build_demo_pinecone_release
|
id: docker_build_polylith_release
|
||||||
uses: docker/build-push-action@v3
|
uses: docker/build-push-action@v2
|
||||||
with:
|
with:
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
context: .
|
context: .
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
file: ./build/docker/Dockerfile.polylith
|
||||||
platforms: ${{ env.PLATFORMS }}
|
platforms: ${{ env.PLATFORMS }}
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:latest
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:${{ env.RELEASE_VERSION }}
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:latest
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-polylith:${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
demo-yggdrasil:
|
|
||||||
name: Yggdrasil demo image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Get release tag & build flags
|
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
|
||||||
run: |
|
|
||||||
echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ env.DOCKER_HUB_USER }}
|
|
||||||
password: ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
- name: Login to GitHub Containers
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.repository_owner }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build main Yggdrasil demo image
|
|
||||||
if: github.ref_name == 'main'
|
|
||||||
id: docker_build_demo_yggdrasil
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
context: .
|
|
||||||
file: ./build/docker/Dockerfile.demo-yggdrasil
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ github.ref_name }}
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Build release Yggdrasil demo image
|
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
|
||||||
id: docker_build_demo_yggdrasil_release
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
context: .
|
|
||||||
file: ./build/docker/Dockerfile.demo-yggdrasil
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:latest
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-yggdrasil:${{ env.RELEASE_VERSION }}
|
|
||||||
|
|
52
.github/workflows/gh-pages.yml
vendored
52
.github/workflows/gh-pages.yml
vendored
|
@ -1,52 +0,0 @@
|
||||||
# Sample workflow for building and deploying a Jekyll site to GitHub Pages
|
|
||||||
name: Deploy GitHub Pages dependencies preinstalled
|
|
||||||
|
|
||||||
on:
|
|
||||||
# Runs on pushes targeting the default branch
|
|
||||||
push:
|
|
||||||
branches: ["gh-pages"]
|
|
||||||
paths:
|
|
||||||
- 'docs/**' # only execute if we have docs changes
|
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Allow one concurrent deployment
|
|
||||||
concurrency:
|
|
||||||
group: "pages"
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# Build job
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Setup Pages
|
|
||||||
uses: actions/configure-pages@v2
|
|
||||||
- name: Build with Jekyll
|
|
||||||
uses: actions/jekyll-build-pages@v1
|
|
||||||
with:
|
|
||||||
source: ./docs
|
|
||||||
destination: ./_site
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-pages-artifact@v1
|
|
||||||
|
|
||||||
# Deployment job
|
|
||||||
deploy:
|
|
||||||
environment:
|
|
||||||
name: github-pages
|
|
||||||
url: ${{ steps.deployment.outputs.page_url }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: build
|
|
||||||
steps:
|
|
||||||
- name: Deploy to GitHub Pages
|
|
||||||
id: deployment
|
|
||||||
uses: actions/deploy-pages@v1
|
|
41
.github/workflows/helm.yml
vendored
41
.github/workflows/helm.yml
vendored
|
@ -1,41 +0,0 @@
|
||||||
name: Release Charts
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- 'helm/**' # only execute if we have helm chart changes
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
# depending on default permission settings for your org (contents being read-only or read-write for workloads), you will have to add permissions
|
|
||||||
# see: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#modifying-the-permissions-for-the-github_token
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Configure Git
|
|
||||||
run: |
|
|
||||||
git config user.name "$GITHUB_ACTOR"
|
|
||||||
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
|
|
||||||
|
|
||||||
- name: Install Helm
|
|
||||||
uses: azure/setup-helm@v3
|
|
||||||
with:
|
|
||||||
version: v3.10.0
|
|
||||||
|
|
||||||
- name: Run chart-releaser
|
|
||||||
uses: helm/chart-releaser-action@ed43eb303604cbc0eeec8390544f7748dc6c790d # specific commit, since `mark_as_latest` is not yet in a release
|
|
||||||
env:
|
|
||||||
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
with:
|
|
||||||
config: helm/cr.yaml
|
|
||||||
charts_dir: helm/
|
|
||||||
mark_as_latest: false
|
|
91
.github/workflows/k8s.yml
vendored
91
.github/workflows/k8s.yml
vendored
|
@ -1,91 +0,0 @@
|
||||||
name: k8s
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
paths:
|
|
||||||
- 'helm/**' # only execute if we have helm chart changes
|
|
||||||
pull_request:
|
|
||||||
branches: ["main"]
|
|
||||||
paths:
|
|
||||||
- 'helm/**'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
lint:
|
|
||||||
name: Lint Helm chart
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
outputs:
|
|
||||||
changed: ${{ steps.list-changed.outputs.changed }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- uses: azure/setup-helm@v3
|
|
||||||
with:
|
|
||||||
version: v3.10.0
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: 3.11
|
|
||||||
check-latest: true
|
|
||||||
- uses: helm/chart-testing-action@v2.3.1
|
|
||||||
- name: Get changed status
|
|
||||||
id: list-changed
|
|
||||||
run: |
|
|
||||||
changed=$(ct list-changed --config helm/ct.yaml --target-branch ${{ github.event.repository.default_branch }})
|
|
||||||
if [[ -n "$changed" ]]; then
|
|
||||||
echo "::set-output name=changed::true"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Run lint
|
|
||||||
run: ct lint --config helm/ct.yaml
|
|
||||||
|
|
||||||
# only bother to run if lint step reports a change to the helm chart
|
|
||||||
install:
|
|
||||||
needs:
|
|
||||||
- lint
|
|
||||||
if: ${{ needs.lint.outputs.changed == 'true' }}
|
|
||||||
name: Install Helm charts
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
ref: ${{ inputs.checkoutCommit }}
|
|
||||||
- name: Install Kubernetes tools
|
|
||||||
uses: yokawasa/action-setup-kube-tools@v0.8.2
|
|
||||||
with:
|
|
||||||
setup-tools: |
|
|
||||||
helmv3
|
|
||||||
helm: "3.10.3"
|
|
||||||
- uses: actions/setup-python@v4
|
|
||||||
with:
|
|
||||||
python-version: "3.10"
|
|
||||||
- name: Set up chart-testing
|
|
||||||
uses: helm/chart-testing-action@v2.3.1
|
|
||||||
- name: Create k3d cluster
|
|
||||||
uses: nolar/setup-k3d-k3s@v1
|
|
||||||
with:
|
|
||||||
version: v1.28
|
|
||||||
- name: Remove node taints
|
|
||||||
run: |
|
|
||||||
kubectl taint --all=true nodes node.cloudprovider.kubernetes.io/uninitialized- || true
|
|
||||||
- name: Run chart-testing (install)
|
|
||||||
run: ct install --config helm/ct.yaml
|
|
||||||
|
|
||||||
# Install the chart using helm directly and test with create-account
|
|
||||||
- name: Install chart
|
|
||||||
run: |
|
|
||||||
helm install --values helm/dendrite/ci/ct-postgres-sharedsecret-values.yaml dendrite helm/dendrite
|
|
||||||
- name: Wait for Postgres and Dendrite to be up
|
|
||||||
run: |
|
|
||||||
kubectl wait --for=condition=ready --timeout=90s pod -l app.kubernetes.io/name=postgresql || kubectl get pods -A
|
|
||||||
kubectl wait --for=condition=ready --timeout=90s pod -l app.kubernetes.io/name=dendrite || kubectl get pods -A
|
|
||||||
kubectl get pods -A
|
|
||||||
kubectl get services
|
|
||||||
kubectl get ingress
|
|
||||||
kubectl logs -l app.kubernetes.io/name=dendrite
|
|
||||||
- name: Run create account
|
|
||||||
run: |
|
|
||||||
podName=$(kubectl get pods -l app.kubernetes.io/name=dendrite -o name)
|
|
||||||
kubectl exec "${podName}" -- /usr/bin/create-account -username alice -password somerandompassword
|
|
322
.github/workflows/schedules.yaml
vendored
322
.github/workflows/schedules.yaml
vendored
|
@ -1,322 +0,0 @@
|
||||||
name: Scheduled
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: '0 0 * * *' # every day at midnight
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check_date: # https://stackoverflow.com/questions/63014786/how-to-schedule-a-github-actions-nightly-build-but-run-it-only-when-there-where
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Check latest commit
|
|
||||||
outputs:
|
|
||||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: print latest_commit
|
|
||||||
run: echo ${{ github.sha }}
|
|
||||||
|
|
||||||
- id: should_run
|
|
||||||
continue-on-error: true
|
|
||||||
name: check latest commit is less than a day
|
|
||||||
if: ${{ github.event_name == 'schedule' }}
|
|
||||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
|
||||||
|
|
||||||
# run Sytest in different variations
|
|
||||||
sytest:
|
|
||||||
needs: check_date
|
|
||||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
|
||||||
timeout-minutes: 60
|
|
||||||
name: "Sytest (${{ matrix.label }})"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- label: SQLite native
|
|
||||||
|
|
||||||
- label: SQLite Cgo
|
|
||||||
cgo: 1
|
|
||||||
|
|
||||||
- label: PostgreSQL
|
|
||||||
postgres: postgres
|
|
||||||
container:
|
|
||||||
image: matrixdotorg/sytest-dendrite:latest
|
|
||||||
volumes:
|
|
||||||
- ${{ github.workspace }}:/src
|
|
||||||
- /root/.cache/go-build:/github/home/.cache/go-build
|
|
||||||
- /root/.cache/go-mod:/gopath/pkg/mod
|
|
||||||
env:
|
|
||||||
POSTGRES: ${{ matrix.postgres && 1}}
|
|
||||||
SYTEST_BRANCH: ${{ github.head_ref }}
|
|
||||||
RACE_DETECTION: 1
|
|
||||||
COVER: 1
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
/gopath/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-sytest-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-sytest-
|
|
||||||
- name: Run Sytest
|
|
||||||
run: /bootstrap.sh dendrite
|
|
||||||
working-directory: /src
|
|
||||||
- name: Summarise results.tap
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: /sytest/scripts/tap_to_gha.pl /logs/results.tap
|
|
||||||
- name: Sytest List Maintenance
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: /src/show-expected-fail-tests.sh /logs/results.tap /src/sytest-whitelist /src/sytest-blacklist
|
|
||||||
continue-on-error: true # not fatal
|
|
||||||
- name: Are We Synapse Yet?
|
|
||||||
if: ${{ always() }}
|
|
||||||
run: /src/are-we-synapse-yet.py /logs/results.tap -v
|
|
||||||
continue-on-error: true # not fatal
|
|
||||||
- name: Upload Sytest logs
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: Sytest Logs - ${{ job.status }} - (Dendrite ${{ join(matrix.*, ' ') }})
|
|
||||||
path: |
|
|
||||||
/logs/results.tap
|
|
||||||
/logs/**/*.log*
|
|
||||||
/logs/**/covdatafiles/**
|
|
||||||
|
|
||||||
sytest-coverage:
|
|
||||||
timeout-minutes: 5
|
|
||||||
name: "Sytest Coverage"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ sytest, check_date ] # only run once Sytest is done and there was a commit
|
|
||||||
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: 'stable'
|
|
||||||
cache: true
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
- name: Collect coverage
|
|
||||||
run: |
|
|
||||||
go tool covdata textfmt -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o sytest.cov
|
|
||||||
grep -Ev 'relayapi|setup/mscs|api_trace' sytest.cov > final.cov
|
|
||||||
go tool covdata func -i="$(find Sytest* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
files: ./final.cov
|
|
||||||
flags: sytest
|
|
||||||
fail_ci_if_error: true
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
|
||||||
|
|
||||||
# run Complement
|
|
||||||
complement:
|
|
||||||
needs: check_date
|
|
||||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
|
||||||
name: "Complement (${{ matrix.label }})"
|
|
||||||
timeout-minutes: 60
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- label: SQLite native
|
|
||||||
cgo: 0
|
|
||||||
|
|
||||||
- label: SQLite Cgo
|
|
||||||
cgo: 1
|
|
||||||
|
|
||||||
- label: PostgreSQL
|
|
||||||
postgres: Postgres
|
|
||||||
cgo: 0
|
|
||||||
steps:
|
|
||||||
# Env vars are set file a file given by $GITHUB_PATH. We need both Go 1.17 and GOPATH on env to run Complement.
|
|
||||||
# See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-system-path
|
|
||||||
- name: "Set Go Version"
|
|
||||||
run: |
|
|
||||||
echo "$GOROOT_1_17_X64/bin" >> $GITHUB_PATH
|
|
||||||
echo "~/go/bin" >> $GITHUB_PATH
|
|
||||||
- name: "Install Complement Dependencies"
|
|
||||||
# We don't need to install Go because it is included on the Ubuntu 20.04 image:
|
|
||||||
# See https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-Readme.md specifically GOROOT_1_17_X64
|
|
||||||
run: |
|
|
||||||
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
|
|
||||||
go install github.com/gotesttools/gotestfmt/v2/cmd/gotestfmt@latest
|
|
||||||
- name: Run actions/checkout@v4 for dendrite
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
path: dendrite
|
|
||||||
|
|
||||||
# Attempt to check out the same branch of Complement as the PR. If it
|
|
||||||
# doesn't exist, fallback to main.
|
|
||||||
- name: Checkout complement
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p complement
|
|
||||||
# Attempt to use the version of complement which best matches the current
|
|
||||||
# build. Depending on whether this is a PR or release, etc. we need to
|
|
||||||
# use different fallbacks.
|
|
||||||
#
|
|
||||||
# 1. First check if there's a similarly named branch (GITHUB_HEAD_REF
|
|
||||||
# for pull requests, otherwise GITHUB_REF).
|
|
||||||
# 2. Attempt to use the base branch, e.g. when merging into release-vX.Y
|
|
||||||
# (GITHUB_BASE_REF for pull requests).
|
|
||||||
# 3. Use the default complement branch ("master").
|
|
||||||
for BRANCH_NAME in "$GITHUB_HEAD_REF" "$GITHUB_BASE_REF" "${GITHUB_REF#refs/heads/}" "master"; do
|
|
||||||
# Skip empty branch names and merge commits.
|
|
||||||
if [[ -z "$BRANCH_NAME" || $BRANCH_NAME =~ ^refs/pull/.* ]]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
(wget -O - "https://github.com/matrix-org/complement/archive/$BRANCH_NAME.tar.gz" | tar -xz --strip-components=1 -C complement) && break
|
|
||||||
done
|
|
||||||
# Build initial Dendrite image
|
|
||||||
- run: docker build --build-arg=CGO=${{ matrix.cgo }} -t complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }} -f build/scripts/Complement${{ matrix.postgres }}.Dockerfile .
|
|
||||||
working-directory: dendrite
|
|
||||||
env:
|
|
||||||
DOCKER_BUILDKIT: 1
|
|
||||||
|
|
||||||
- name: Create post test script
|
|
||||||
run: |
|
|
||||||
cat <<EOF > /tmp/posttest.sh
|
|
||||||
#!/bin/bash
|
|
||||||
mkdir -p /tmp/Complement/logs/\$2/\$1/
|
|
||||||
docker cp \$1:/tmp/covdatafiles/. /tmp/Complement/logs/\$2/\$1/
|
|
||||||
EOF
|
|
||||||
|
|
||||||
chmod +x /tmp/posttest.sh
|
|
||||||
# Run Complement
|
|
||||||
- run: |
|
|
||||||
set -o pipefail &&
|
|
||||||
go test -v -json -tags dendrite_blacklist ./tests ./tests/csapi 2>&1 | gotestfmt -hide all
|
|
||||||
shell: bash
|
|
||||||
name: Run Complement Tests
|
|
||||||
env:
|
|
||||||
COMPLEMENT_BASE_IMAGE: complement-dendrite:${{ matrix.postgres }}${{ matrix.cgo }}
|
|
||||||
COMPLEMENT_SHARE_ENV_PREFIX: COMPLEMENT_DENDRITE_
|
|
||||||
COMPLEMENT_DENDRITE_COVER: 1
|
|
||||||
COMPLEMENT_POST_TEST_SCRIPT: /tmp/posttest.sh
|
|
||||||
working-directory: complement
|
|
||||||
|
|
||||||
- name: Upload Complement logs
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
if: ${{ always() }}
|
|
||||||
with:
|
|
||||||
name: Complement Logs - (Dendrite ${{ join(matrix.*, ' ') }})
|
|
||||||
path: |
|
|
||||||
/tmp/Complement/logs/**
|
|
||||||
|
|
||||||
complement-coverage:
|
|
||||||
timeout-minutes: 5
|
|
||||||
name: "Complement Coverage"
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
needs: [ complement, check_date ] # only run once Complements is done and there was a commit
|
|
||||||
if: ${{ always() && needs.check_date.outputs.should_run != 'false' }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version: 'stable'
|
|
||||||
cache: true
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
- name: Collect coverage
|
|
||||||
run: |
|
|
||||||
go tool covdata textfmt -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)" -o complement.cov
|
|
||||||
grep -Ev 'relayapi|setup/mscs|api_trace' complement.cov > final.cov
|
|
||||||
go tool covdata func -i="$(find Complement* -name 'covmeta*' -type f -exec dirname {} \; | uniq | paste -s -d ',' -)"
|
|
||||||
- name: Upload coverage to Codecov
|
|
||||||
uses: codecov/codecov-action@v4
|
|
||||||
with:
|
|
||||||
files: ./final.cov
|
|
||||||
flags: complement
|
|
||||||
fail_ci_if_error: true
|
|
||||||
token: ${{ secrets.CODECOV_TOKEN }} # required
|
|
||||||
|
|
||||||
element-web:
|
|
||||||
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
|
||||||
timeout-minutes: 120
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: tecolicom/actions-use-apt-tools@v1
|
|
||||||
with:
|
|
||||||
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
|
||||||
# supposed to be covered by STIXGeneral.
|
|
||||||
tools: fonts-stix
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: matrix-org/matrix-react-sdk
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
cache: 'yarn'
|
|
||||||
- name: Fetch layered build
|
|
||||||
run: scripts/ci/layered.sh
|
|
||||||
- name: Copy config
|
|
||||||
run: cp element.io/develop/config.json config.json
|
|
||||||
working-directory: ./element-web
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
CI_PACKAGE: true
|
|
||||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
|
||||||
run: yarn build
|
|
||||||
working-directory: ./element-web
|
|
||||||
- name: Edit Test Config
|
|
||||||
run: |
|
|
||||||
sed -i '/HOMESERVER/c\ HOMESERVER: "dendrite",' cypress.config.ts
|
|
||||||
- name: "Run cypress tests"
|
|
||||||
uses: cypress-io/github-action@v4.1.1
|
|
||||||
with:
|
|
||||||
browser: chrome
|
|
||||||
start: npx serve -p 8080 ./element-web/webapp
|
|
||||||
wait-on: 'http://localhost:8080'
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
|
||||||
TMPDIR: ${{ runner.temp }}
|
|
||||||
|
|
||||||
element-web-pinecone:
|
|
||||||
if: ${{ false }} # disable for now, as Cypress has been replaced by Playwright
|
|
||||||
timeout-minutes: 120
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: tecolicom/actions-use-apt-tools@v1
|
|
||||||
with:
|
|
||||||
# Our test suite includes some screenshot tests with unusual diacritics, which are
|
|
||||||
# supposed to be covered by STIXGeneral.
|
|
||||||
tools: fonts-stix
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
repository: matrix-org/matrix-react-sdk
|
|
||||||
- uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
cache: 'yarn'
|
|
||||||
- name: Fetch layered build
|
|
||||||
run: scripts/ci/layered.sh
|
|
||||||
- name: Copy config
|
|
||||||
run: cp element.io/develop/config.json config.json
|
|
||||||
working-directory: ./element-web
|
|
||||||
- name: Build
|
|
||||||
env:
|
|
||||||
CI_PACKAGE: true
|
|
||||||
NODE_OPTIONS: "--openssl-legacy-provider"
|
|
||||||
run: yarn build
|
|
||||||
working-directory: ./element-web
|
|
||||||
- name: Edit Test Config
|
|
||||||
run: |
|
|
||||||
sed -i '/HOMESERVER/c\ HOMESERVER: "dendritePinecone",' cypress.config.ts
|
|
||||||
- name: "Run cypress tests"
|
|
||||||
uses: cypress-io/github-action@v4.1.1
|
|
||||||
with:
|
|
||||||
browser: chrome
|
|
||||||
start: npx serve -p 8080 ./element-web/webapp
|
|
||||||
wait-on: 'http://localhost:8080'
|
|
||||||
env:
|
|
||||||
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: true
|
|
||||||
TMPDIR: ${{ runner.temp }}
|
|
9
.gitignore
vendored
9
.gitignore
vendored
|
@ -5,7 +5,6 @@
|
||||||
|
|
||||||
# Allow GitHub config
|
# Allow GitHub config
|
||||||
!.github
|
!.github
|
||||||
!.forgejo
|
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
/.downloads
|
/.downloads
|
||||||
|
@ -57,7 +56,6 @@ dendrite.yaml
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log*
|
*.log*
|
||||||
|
@ -75,10 +73,3 @@ complement/
|
||||||
docs/_site
|
docs/_site
|
||||||
|
|
||||||
media_store/
|
media_store/
|
||||||
build
|
|
||||||
|
|
||||||
# golang workspaces
|
|
||||||
go.work*
|
|
||||||
|
|
||||||
# helm chart
|
|
||||||
helm/dendrite/charts/
|
|
||||||
|
|
|
@ -6,7 +6,7 @@ run:
|
||||||
concurrency: 4
|
concurrency: 4
|
||||||
|
|
||||||
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
# timeout for analysis, e.g. 30s, 5m, default is 1m
|
||||||
timeout: 5m
|
deadline: 30m
|
||||||
|
|
||||||
# exit code when at least one issue was found, default is 1
|
# exit code when at least one issue was found, default is 1
|
||||||
issues-exit-code: 1
|
issues-exit-code: 1
|
||||||
|
@ -18,6 +18,24 @@ run:
|
||||||
#build-tags:
|
#build-tags:
|
||||||
# - mytag
|
# - mytag
|
||||||
|
|
||||||
|
# which dirs to skip: they won't be analyzed;
|
||||||
|
# can use regexp here: generated.*, regexp is applied on full path;
|
||||||
|
# default value is empty list, but next dirs are always skipped independently
|
||||||
|
# from this option's value:
|
||||||
|
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
||||||
|
skip-dirs:
|
||||||
|
- bin
|
||||||
|
- docs
|
||||||
|
|
||||||
|
# which files to skip: they will be analyzed, but issues from them
|
||||||
|
# won't be reported. Default value is empty list, but there is
|
||||||
|
# no need to include all autogenerated files, we confidently recognize
|
||||||
|
# autogenerated files. If it's not please let us know.
|
||||||
|
skip-files:
|
||||||
|
- ".*\\.md$"
|
||||||
|
- ".*\\.sh$"
|
||||||
|
- "^cmd/syncserver-integration-tests/testdata.go$"
|
||||||
|
|
||||||
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
# by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules":
|
||||||
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
# If invoked with -mod=readonly, the go command is disallowed from the implicit
|
||||||
# automatic updating of go.mod described above. Instead, it fails when any changes
|
# automatic updating of go.mod described above. Instead, it fails when any changes
|
||||||
|
@ -32,8 +50,7 @@ run:
|
||||||
# output configuration options
|
# output configuration options
|
||||||
output:
|
output:
|
||||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||||
formats:
|
format: colored-line-number
|
||||||
- format: colored-line-number
|
|
||||||
|
|
||||||
# print lines of code with issue, default is true
|
# print lines of code with issue, default is true
|
||||||
print-issued-lines: true
|
print-issued-lines: true
|
||||||
|
@ -62,8 +79,9 @@ linters-settings:
|
||||||
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
||||||
#exclude: /path/to/file.txt
|
#exclude: /path/to/file.txt
|
||||||
govet:
|
govet:
|
||||||
enable:
|
# report about shadowed variables
|
||||||
- shadow
|
check-shadowing: true
|
||||||
|
|
||||||
# settings per analyzer
|
# settings per analyzer
|
||||||
settings:
|
settings:
|
||||||
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
||||||
|
@ -161,7 +179,9 @@ linters-settings:
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
enable:
|
enable:
|
||||||
|
- deadcode
|
||||||
- errcheck
|
- errcheck
|
||||||
|
- goconst
|
||||||
- gocyclo
|
- gocyclo
|
||||||
- goimports # Does everything gofmt does
|
- goimports # Does everything gofmt does
|
||||||
- gosimple
|
- gosimple
|
||||||
|
@ -171,8 +191,10 @@ linters:
|
||||||
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
||||||
- nakedret
|
- nakedret
|
||||||
- staticcheck
|
- staticcheck
|
||||||
|
- structcheck
|
||||||
- unparam
|
- unparam
|
||||||
- unused
|
- unused
|
||||||
|
- varcheck
|
||||||
enable-all: false
|
enable-all: false
|
||||||
disable:
|
disable:
|
||||||
- bodyclose
|
- bodyclose
|
||||||
|
@ -192,31 +214,12 @@ linters:
|
||||||
- stylecheck
|
- stylecheck
|
||||||
- typecheck # Should turn back on soon
|
- typecheck # Should turn back on soon
|
||||||
- unconvert # Should turn back on soon
|
- unconvert # Should turn back on soon
|
||||||
- goconst # Slightly annoying, as it reports "issues" in SQL statements
|
|
||||||
disable-all: false
|
disable-all: false
|
||||||
presets:
|
presets:
|
||||||
fast: false
|
fast: false
|
||||||
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
# which files to skip: they will be analyzed, but issues from them
|
|
||||||
# won't be reported. Default value is empty list, but there is
|
|
||||||
# no need to include all autogenerated files, we confidently recognize
|
|
||||||
# autogenerated files. If it's not please let us know.
|
|
||||||
exclude-files:
|
|
||||||
- ".*\\.md$"
|
|
||||||
- ".*\\.sh$"
|
|
||||||
- "^cmd/syncserver-integration-tests/testdata.go$"
|
|
||||||
|
|
||||||
# which dirs to skip: they won't be analyzed;
|
|
||||||
# can use regexp here: generated.*, regexp is applied on full path;
|
|
||||||
# default value is empty list, but next dirs are always skipped independently
|
|
||||||
# from this option's value:
|
|
||||||
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
|
|
||||||
exclude-dirs:
|
|
||||||
- bin
|
|
||||||
- docs
|
|
||||||
|
|
||||||
# List of regexps of issue texts to exclude, empty list by default.
|
# List of regexps of issue texts to exclude, empty list by default.
|
||||||
# But independently from this option we use default exclude patterns,
|
# But independently from this option we use default exclude patterns,
|
||||||
# it can be disabled by `exclude-use-default: false`. To list all
|
# it can be disabled by `exclude-use-default: false`. To list all
|
||||||
|
|
454
CHANGES.md
454
CHANGES.md
|
@ -1,459 +1,5 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Dendrite 0.13.7 (2024-04-09)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Fixed an issue where the displayname/avatar of an invited user was replaced with the inviter's details
|
|
||||||
- Improved server startup performance by avoiding unnecessary room ACL queries
|
|
||||||
- This change reduces memory footprint as it caches ACL regex patterns once instead of for each room
|
|
||||||
- Unnecessary Relay related queries have been removed. **Note**: To use relays, you now need to explicitly enable them using the `federation_api.enable_relays` config
|
|
||||||
- Fixed space summaries over federation
|
|
||||||
- Improved usage of external NATS JetStream by reusing existing connections instead of opening new ones unnecessarily
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Modernized Appservices (contributed by [tulir](https://github.com/tulir))
|
|
||||||
- Added event reporting with Synapse Admin endpoints for querying them
|
|
||||||
- Updated dependencies
|
|
||||||
|
|
||||||
## Dendrite 0.13.6 (2024-01-26)
|
|
||||||
|
|
||||||
Upgrading to this version is **highly** recommended, as it contains several QoL improvements.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Use `AckExplicitPolicy` for JetStream consumers, so messages don't pile up in NATS
|
|
||||||
- A rare panic when assigning a state key NID has been fixed
|
|
||||||
- A rare panic when checking powerlevels has been fixed
|
|
||||||
- Notary keys requests for all keys now work correctly
|
|
||||||
- Spec compliance:
|
|
||||||
- Return `M_INVALID_PARAM` when querying room aliases
|
|
||||||
- Handle empty `from` parameter when requesting `/messages`
|
|
||||||
- Add CORP headers on media endpoints
|
|
||||||
- Remove `aliases` from `/publicRooms` responses
|
|
||||||
- Allow `+` in MXIDs (Contributed by [RosstheRoss](https://github.com/RosstheRoss))
|
|
||||||
- Fixes membership transitions from `knock` to `join` in `knock_restricted` rooms
|
|
||||||
- Incremental syncs now batch querying events (Contributed by [recht](https://github.com/recht))
|
|
||||||
- Move `/joined_members` back to the clientAPI/roomserver, which should make bridges happier again
|
|
||||||
- Backfilling from other servers now only uses at max 100 events instead of potentially thousands
|
|
||||||
|
|
||||||
## Dendrite 0.13.5 (2023-12-12)
|
|
||||||
|
|
||||||
Upgrading to this version is **highly** recommended, as it fixes several long-standing bugs in
|
|
||||||
our CanonicalJSON implementation.
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- Convert unicode escapes to lowercase (gomatrixserverlib)
|
|
||||||
- Fix canonical json utf-16 surrogate pair detection logic (gomatrixserverlib)
|
|
||||||
- Handle negative zero and exponential numbers in Canonical JSON verification (gomatrixserverlib)
|
|
||||||
- Avoid logging unnecessary messages when unable to fetch server keys if multiple fetchers are used (gomatrixserverlib)
|
|
||||||
- Issues around the device list updater have been fixed, which should ensure that there are always
|
|
||||||
workers available to process incoming device list updates.
|
|
||||||
- A panic in the `/hierarchy` endpoints used for spaces has been fixed (client-server and server-server API)
|
|
||||||
- Fixes around the way we handle database transactions (including a potential connection leak)
|
|
||||||
- ACLs are now updated when received as outliers
|
|
||||||
- A race condition, which could lead to bridges instantly leaving a room after joining it, between the SyncAPI and
|
|
||||||
Appservices has been fixed
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- **Appservice login is now supported!**
|
|
||||||
- Users can now kick themselves (used by some bridges)
|
|
||||||
|
|
||||||
## Dendrite 0.13.4 (2023-10-25)
|
|
||||||
|
|
||||||
Upgrading to this version is **highly** recommended, as it fixes a long-standing bug in the state resolution
|
|
||||||
algorithm.
|
|
||||||
|
|
||||||
### Fixes:
|
|
||||||
|
|
||||||
- The "device list updater" now de-duplicates the servers to fetch devices from on startup. (This also
|
|
||||||
avoids spamming the logs when shutting down.)
|
|
||||||
- A bug in the state resolution algorithm has been fixed. This bug could result in users "being reset"
|
|
||||||
out of rooms and other missing state events due to calculating the wrong state.
|
|
||||||
- A bug when setting notifications from Element Android has been fixed by implementing MSC3987
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Updated dependencies
|
|
||||||
- Internal NATS Server has been updated from v2.9.19 to v2.9.23
|
|
||||||
|
|
||||||
## Dendrite 0.13.3 (2023-09-28)
|
|
||||||
|
|
||||||
### Fixes:
|
|
||||||
|
|
||||||
- The `user_id` query parameter when authenticating is now used correctly (contributed by [tulir](https://github.com/tulir))
|
|
||||||
- Invitations are now correctly pushed to devices
|
|
||||||
- A bug which could result in the corruption of `m.direct` account data has been fixed
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- [Sliding Sync proxy](https://github.com/matrix-org/sliding-sync) can be configured in the `/.well-known/matrix/client` response
|
|
||||||
- Room version 11 is now supported
|
|
||||||
- Clients can request the `federation` `event_format` when creating filters
|
|
||||||
- Many under the hood improvements for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- Dendrite now requires Go 1.20 if building from source
|
|
||||||
|
|
||||||
## Dendrite 0.13.2 (2023-08-23)
|
|
||||||
|
|
||||||
### Fixes:
|
|
||||||
|
|
||||||
- Migrations in SQLite are now prepared on the correct context (transaction or database)
|
|
||||||
- The `InputRoomEvent` stream now has a maximum age of 24h, which should help with slow start up times of NATS JetStream (contributed by [neilalexander](https://github.com/neilalexander))
|
|
||||||
- Event size checks are more in line with Synapse
|
|
||||||
- Requests to `/messages` have been optimized, possibly reducing database round trips
|
|
||||||
- Re-add the revision of Dendrite when building from source (Note: This only works if git is installed)
|
|
||||||
- Getting local members to notify has been optimized, which should significantly reduce memory allocation and cache usage
|
|
||||||
- When getting queried about user profiles, we now return HTTP404 if the user/profiles does not exist
|
|
||||||
- Background federated joins should now be fixed and not timeout after a short time
|
|
||||||
- Database connections are now correctly re-used
|
|
||||||
- Restored the old behavior of the `/purgeRoom` admin endpoint (does not evacuate the room before purging)
|
|
||||||
- Don't expose information about the system when trying to download files that don't exist
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Further improvements and fixes for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
|
||||||
- Lookup correct prev events in the sync API
|
|
||||||
- Populate `prev_sender` correctly in the sync API
|
|
||||||
- Event federation should work better
|
|
||||||
- Added new `dendrite_up` Prometheus metric, containing the version of Dendrite
|
|
||||||
- Space summaries ([MSC2946](https://github.com/matrix-org/matrix-spec-proposals/pull/2946)) have been moved from MSC to being natively supported
|
|
||||||
- For easier issue investigation, logs for application services now contain the application service ID (contributed by [maxberger](https://github.com/maxberger))
|
|
||||||
- The default room version to use when creating rooms can now be configured using `room_server.default_room_version`
|
|
||||||
|
|
||||||
## Dendrite 0.13.1 (2023-07-06)
|
|
||||||
|
|
||||||
This releases fixes a long-standing "off-by-one" error which could result in state resets. Upgrading to this version is **highly** recommended.
|
|
||||||
|
|
||||||
When deduplicating state events, we were checking if the event in question was already in a state snapshot. If it was in a previous state snapshot, we would
|
|
||||||
then remove it from the list of events to store. If this happened, we were, unfortunately, skipping the next event to check. This resulted in
|
|
||||||
events getting stored in state snapshots where they may not be needed. When we now compared two of those state snapshots, one of them
|
|
||||||
contained the skipped event, while the other didn't. This difference possibly shouldn't exist, resulting in unexpected state resets and explains
|
|
||||||
reports of missing state events as well.
|
|
||||||
|
|
||||||
Rooms where a state reset occurred earlier should, hopefully, reconcile over time.
|
|
||||||
|
|
||||||
### Fixes:
|
|
||||||
|
|
||||||
- A long-standing "off-by-one" error has been fixed, which could result in state resets
|
|
||||||
- Roomserver Prometheus Metrics are available again
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Updated dependencies
|
|
||||||
- Internal NATS Server has been updated from v2.9.15 to v2.9.19
|
|
||||||
|
|
||||||
## Dendrite 0.13.0 (2023-06-30)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- Results in responses to `/search` now highlight words more accurately and not only the search terms as before
|
|
||||||
- Support for connecting to appservices listening on unix sockets has been added (contributed by [cyberb](https://github.com/cyberb))
|
|
||||||
- Admin APIs for token authenticated registration have been added (contributed by [santhoshivan23](https://github.com/santhoshivan23))
|
|
||||||
- Initial support for [MSC4014: Pseudonymous Identities](https://github.com/matrix-org/matrix-spec-proposals/blob/kegan/pseudo-ids/proposals/4014-pseudonymous-identities.md)
|
|
||||||
- This is **highly experimental**, things like changing usernames/avatars, inviting users, upgrading rooms isn't working
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- `m.upload.size` is now optional, finally allowing uploads with unlimited file size
|
|
||||||
- A bug while resolving server names has been fixed (contributed by [anton-molyboha](https://github.com/anton-molyboha))
|
|
||||||
- Application services should only receive one invitation instead of 2 (or worse), which could result in state resets previously
|
|
||||||
- Several admin endpoints are now using `POST` instead of `GET`
|
|
||||||
- `/delete_devices` now uses user-interactive authentication
|
|
||||||
- Several "membership" (e.g `/kick`, `/ban`) endpoints are using less heavy database queries to check if the user is allowed to perform this action
|
|
||||||
- `/3pid` endpoints are now available on `/v3` instead of the `/unstable` prefix
|
|
||||||
- Upgrading rooms ignores state events of other users, which could result in failed upgrades before
|
|
||||||
- Uploading key backups with a wrong version now returns `M_WRONG_ROOM_KEYS_VERSION`
|
|
||||||
- A potential state reset when joining the same room multiple times in short sequence has been fixed
|
|
||||||
- A bug where we returned the full event as `redacted_because` in redaction events has been fixed
|
|
||||||
- The `displayname` and `avatar_url` can now be set to empty strings
|
|
||||||
- Unsafe hotserving of files has been fixed (contributed by [joshqou](https://github.com/joshqou))
|
|
||||||
- Joining new rooms would potentially return "redacted" events, due to history visibility not being set correctly, this could result in events being rejected
|
|
||||||
- Backfilling resulting in `unsuported room version ''` should now be solved
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- Huge refactoring of Dendrite and gomatrixserverlib
|
|
||||||
|
|
||||||
## Dendrite 0.12.0 (2023-03-13)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
- The userapi and keyserver have been merged (no actions needed regarding the database)
|
|
||||||
- The internal NATS JetStream server is now using logrus for logging (contributed by [dvob](https://github.com/dvob))
|
|
||||||
- The roomserver database has been refactored to have separate interfaces when working with rooms and events. Also includes increased usage of the cache to avoid database round trips. (database is unchanged)
|
|
||||||
- The pinecone demo now shuts down more cleanly
|
|
||||||
- The Helm chart now has the ability to deploy a Grafana chart as well (contributed by [genofire](https://github.com/genofire))
|
|
||||||
- Support for listening on unix sockets has been added (contributed by [cyberb](https://github.com/cyberb))
|
|
||||||
- The internal NATS server was updated to v2.9.15
|
|
||||||
- Initial support for `runtime/trace` has been added, to further track down long-running tasks
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- The `session_id` is now correctly set when using SQLite
|
|
||||||
- An issue where device keys could be removed if a device ID is reused has been fixed
|
|
||||||
- A possible DoS issue related to relations has been fixed (reported by [sleroq](https://github.com/sleroq))
|
|
||||||
- When backfilling events, errors are now ignored if we still could fetch events
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- **⚠️ DEPRECATION: Polylith/HTTP API mode has been removed**
|
|
||||||
- The default endpoint to report usages stats to has been updated
|
|
||||||
|
|
||||||
## Dendrite 0.11.1 (2023-02-10)
|
|
||||||
|
|
||||||
**⚠️ DEPRECATION WARNING: This is the last release to have polylith and HTTP API mode. Future releases are monolith only.**
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Dendrite can now be compiled against Go 1.20
|
|
||||||
* Initial store and forward support has been added
|
|
||||||
* A landing page showing that Dendrite is running has been added (contributed by [LukasLJL](https://github.com/LukasLJL))
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
- `/sync` is now using significantly less database round trips when using Postgres, resulting in faster initial syncs, allowing larger accounts to login again
|
|
||||||
- Many under the hood pinecone improvements
|
|
||||||
- Publishing rooms is now possible again
|
|
||||||
|
|
||||||
## Dendrite 0.11.0 (2023-01-20)
|
|
||||||
|
|
||||||
The last three missing federation API Sytests have been fixed - bringing us to 100% server-server Synapse parity, with client-server parity at 93% 🎉
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Added `/_dendrite/admin/purgeRoom/{roomID}` to clean up the database
|
|
||||||
* The default room version was updated to 10 (contributed by [FSG-Cat](https://github.com/FSG-Cat))
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* An oversight in the `create-config` binary, which now correctly sets the media path if specified (contributed by [BieHDC](https://github.com/BieHDC))
|
|
||||||
* The Helm chart now uses the `$.Chart.AppVersion` as the default image version to pull, with the possibility to override it (contributed by [genofire](https://github.com/genofire))
|
|
||||||
|
|
||||||
## Dendrite 0.10.9 (2023-01-17)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Stale device lists are now cleaned up on startup, removing entries for users the server doesn't share a room with anymore
|
|
||||||
* Dendrite now has its own Helm chart
|
|
||||||
* Guest access is now handled correctly (disallow joins, kick guests on revocation of guest access, as well as over federation)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Push rules have seen several tweaks and fixes, which should, for example, fix notifications for `m.read_receipts`
|
|
||||||
* Outgoing presence will now correctly be sent to newly joined hosts
|
|
||||||
* Fixes the `/_dendrite/admin/resetPassword/{userID}` admin endpoint to use the correct variable
|
|
||||||
* Federated backfilling for medium/large rooms has been fixed
|
|
||||||
* `/login` causing wrong device list updates has been resolved
|
|
||||||
* `/sync` should now return the correct room summary heroes
|
|
||||||
* The default config options for `recaptcha_sitekey_class` and `recaptcha_form_field` are now set correctly
|
|
||||||
* `/messages` now omits empty `state` to be more spec compliant (contributed by [handlerug](https://github.com/handlerug))
|
|
||||||
* `/sync` has been optimised to only query state events for history visibility if they are really needed
|
|
||||||
|
|
||||||
## Dendrite 0.10.8 (2022-11-29)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* The built-in NATS Server has been updated to version 2.9.8
|
|
||||||
* A number of under-the-hood changes have been merged for future virtual hosting support in Dendrite (running multiple domain names on the same Dendrite deployment)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Event auth handling of invites has been refactored, which should fix some edge cases being handled incorrectly
|
|
||||||
* Fix a bug when returning an empty protocol list, which could cause Element to display "The homeserver may be too old to support third party networks" when opening the public room directory
|
|
||||||
* The sync API will no longer filter out the user's own membership when using lazy-loading
|
|
||||||
* Dendrite will now correctly detect JetStream consumers being deleted, stopping the consumer goroutine as needed
|
|
||||||
* A panic in the federation API where the server list could go out of bounds has been fixed
|
|
||||||
* Blacklisted servers will now be excluded when querying joined servers, which improves CPU usage and performs less unnecessary outbound requests
|
|
||||||
* A database writer will now be used to assign state key NIDs when requesting NIDs that may not exist yet
|
|
||||||
* Dendrite will now correctly move local aliases for an upgraded room when the room is upgraded remotely
|
|
||||||
* Dendrite will now correctly move account data for an upgraded room when the room is upgraded remotely
|
|
||||||
* Missing state key NIDs will now be allocated on request rather than returning an error
|
|
||||||
* Guest access is now correctly denied on a number of endpoints
|
|
||||||
* Presence information will now be correctly sent for new private chats
|
|
||||||
* A number of unspecced fields have been removed from outbound `/send` transactions
|
|
||||||
|
|
||||||
## Dendrite 0.10.7 (2022-11-04)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Dendrite will now use a native SQLite port when building with `CGO_ENABLED=0`
|
|
||||||
* A number of `thirdparty` endpoints have been added, improving support for appservices
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* The `"state"` section of the `/sync` response is no longer limited, so state events should not be dropped unexpectedly
|
|
||||||
* The deduplication of the `"timeline"` and `"state"` sections in `/sync` is now performed after applying history visibility, so state events should not be dropped unexpectedly
|
|
||||||
* The `prev_batch` token returned by `/sync` is now calculated after applying history visibility, so that the pagination boundaries are correct
|
|
||||||
* The room summary membership counts in `/sync` should now be calculated properly in more cases
|
|
||||||
* A false membership leave event should no longer be sent down `/sync` as a result of retiring an accepted invite (contributed by [tak-hntlabs](https://github.com/tak-hntlabs))
|
|
||||||
* Presence updates are now only sent to other servers for which the user shares rooms
|
|
||||||
* A bug which could cause a panic when converting events into the `ClientEvent` format has been fixed
|
|
||||||
|
|
||||||
## Dendrite 0.10.6 (2022-11-01)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* History visibility checks have been optimised, which should speed up response times on a variety of endpoints (including `/sync`, `/messages`, `/context` and others) and reduce database load
|
|
||||||
* The built-in NATS Server has been updated to version 2.9.4
|
|
||||||
* Some other minor dependencies have been updated
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* A panic has been fixed in the sync API PDU stream which could cause requests to fail
|
|
||||||
* The `/members` response now contains the `room_id` field, which may fix some E2EE problems with clients using the JS SDK (contributed by [ashkitten](https://github.com/ashkitten))
|
|
||||||
* The auth difference calculation in state resolution v2 has been tweaked for clarity (and moved into gomatrixserverlib with the rest of the state resolution code)
|
|
||||||
|
|
||||||
## Dendrite 0.10.5 (2022-10-31)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* It is now possible to use hCaptcha instead of reCAPTCHA for protecting registration
|
|
||||||
* A new `auto_join_rooms` configuration option has been added for automatically joining new users to a set of rooms
|
|
||||||
* A new `/_dendrite/admin/downloadState/{serverName}/{roomID}` endpoint has been added, which allows a server administrator to attempt to repair a room with broken room state by downloading a state snapshot from another federated server in the room
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Querying cross-signing keys for users should now be considerably faster
|
|
||||||
* A bug in state resolution where some events were not correctly selected for third-party invites has been fixed
|
|
||||||
* A bug in state resolution which could result in `not in room` event rejections has been fixed
|
|
||||||
* When accepting a DM invite, it should now be possible to see messages that were sent before the invite was accepted
|
|
||||||
* Claiming remote E2EE one-time keys has been refactored and should be more reliable now
|
|
||||||
* Various fixes have been made to the `/members` endpoint, which may help with E2EE reliability and clients rendering memberships
|
|
||||||
* A race condition in the federation API destination queues has been fixed when associating queued events with remote server destinations
|
|
||||||
* A bug in the sync API where too many events were selected resulting in high CPU usage has been fixed
|
|
||||||
* Configuring the avatar URL for the Server Notices user should work correctly now
|
|
||||||
|
|
||||||
## Dendrite 0.10.4 (2022-10-21)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Various tables belonging to the user API will be renamed so that they are namespaced with the `userapi_` prefix
|
|
||||||
* Note that, after upgrading to this version, you should not revert to an older version of Dendrite as the database changes **will not** be reverted automatically
|
|
||||||
* The backoff and retry behaviour in the federation API has been refactored and improved
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Private read receipt support is now advertised in the client `/versions` endpoint
|
|
||||||
* Private read receipts will now clear notification counts properly
|
|
||||||
* A bug where a false `leave` membership transition was inserted into the timeline after accepting an invite has been fixed
|
|
||||||
* Some panics caused by concurrent map writes in the key server have been fixed
|
|
||||||
* The sync API now calculates membership transitions from state deltas more accurately
|
|
||||||
* Transaction IDs are now scoped to endpoints, which should fix some bugs where transaction ID reuse could cause nonsensical cached responses from some endpoints
|
|
||||||
* The length of the `type`, `sender`, `state_key` and `room_id` fields in events are now verified by number of bytes rather than codepoints after a spec clarification, reverting a change made in Dendrite 0.9.6
|
|
||||||
|
|
||||||
## Dendrite 0.10.3 (2022-10-14)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Event relations are now tracked and support for the `/room/{roomID}/relations/...` client API endpoints have been added
|
|
||||||
* Support has been added for private read receipts
|
|
||||||
* The built-in NATS Server has been updated to version 2.9.3
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* The `unread_notifications` are now always populated in joined room responses
|
|
||||||
* The `/get_missing_events` federation API endpoint should now work correctly for rooms with `joined` and `invited` visibility settings, returning redacted events for events that other servers are not allowed to see
|
|
||||||
* The `/event` client API endpoint now applies history visibility correctly
|
|
||||||
* Read markers should now be updated much more reliably
|
|
||||||
* A rare bug in the sync API which could cause some `join` memberships to be incorrectly overwritten by other memberships when working out which rooms to populate has been fixed
|
|
||||||
* The federation API now correctly updates the joined hosts table during a state rewrite
|
|
||||||
|
|
||||||
## Dendrite 0.10.2 (2022-10-07)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Dendrite will now fail to start if there is an obvious problem with the configured `max_open_conns` when using PostgreSQL database backends, since this can lead to instability and performance issues
|
|
||||||
* More information on this is available [in the documentation](https://matrix-org.github.io/dendrite/installation/start/optimisation#postgresql-connection-limit)
|
|
||||||
* Unnecessary/empty fields will no longer be sent in `/sync` responses
|
|
||||||
* It is now possible to configure `old_private_keys` from previous Matrix installations on the same domain if only public key is known, to make it easier to expire old keys correctly
|
|
||||||
* You can configure either just the `private_key` path, or you can supply both the `public_key` and `key_id`
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* The sync transaction behaviour has been modified further so that errors in one stream should not propagate to other streams unnecessarily
|
|
||||||
* Rooms should now be classified as DM rooms correctly by passing through `is_direct` and unsigned hints
|
|
||||||
* A bug which caused marking device lists as stale to consume lots of CPU has been fixed
|
|
||||||
* Users accepting invites should no longer cause unnecessary federated joins if there are already other local users in the room
|
|
||||||
* The sync API state range queries have been optimised by adding missing indexes
|
|
||||||
* It should now be possible to configure non-English languages for full-text search in `search.language`
|
|
||||||
* The roomserver will no longer attempt to perform federated requests to the local server when trying to fetch missing events
|
|
||||||
* The `/keys/upload` endpoint will now always return the `one_time_keys_counts`, which may help with E2EE reliability
|
|
||||||
* The sync API will now retrieve the latest stream position before processing each stream rather than at the beginning of the request, to hopefully reduce the number of round-trips to `/sync`
|
|
||||||
|
|
||||||
## Dendrite 0.10.1 (2022-09-30)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* The built-in NATS Server has been updated to version 2.9.2
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* A regression introduced in 0.10.0 in `/sync` as a result of transaction errors has been fixed
|
|
||||||
* Account data updates will no longer send duplicate output events
|
|
||||||
|
|
||||||
## Dendrite 0.10.0 (2022-09-30)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* High performance full-text searching has been added to Dendrite
|
|
||||||
* Search must be enabled in the [`search` section of the `sync_api` config](https://github.com/matrix-org/dendrite/blob/6348486a1365c7469a498101f5035a9b6bd16d22/dendrite-sample.monolith.yaml#L279-L290) before it can be used
|
|
||||||
* The search index is stored on the filesystem rather than the sync API database, so a path to a suitable storage location on disk must be configured
|
|
||||||
* Sync requests should now complete faster and use considerably less database connections as a result of better transactional isolation
|
|
||||||
* The notifications code has been refactored to hopefully make notifications more reliable
|
|
||||||
* A new `/_dendrite/admin/refreshDevices/{userID}` admin endpoint has been added for forcing a refresh of a remote user's device lists without having to modify the database by hand
|
|
||||||
* A new `/_dendrite/admin/fulltext/reindex` admin endpoint has been added for rebuilding the search index (although this may take some time)
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* A number of bugs in the device list updater have been fixed, which should help considerably with federated device list synchronisation and E2EE reliability
|
|
||||||
* A state resolution bug has been fixed which should help to prevent unexpected state resets
|
|
||||||
* The deprecated `"origin"` field in events will now be correctly ignored in all cases
|
|
||||||
* Room versions 8 and 9 will now correctly evaluate `"knock"` join rules and membership states
|
|
||||||
* A database index has been added to speed up finding room memberships in the sync API (contributed by [PiotrKozimor](https://github.com/PiotrKozimor))
|
|
||||||
* The client API will now return an `M_UNRECOGNIZED` error for unknown endpoints/methods, which should help with client error handling
|
|
||||||
* A bug has been fixed when updating push rules which could result in `database is locked` on SQLite
|
|
||||||
|
|
||||||
## Dendrite 0.9.9 (2022-09-22)
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* Dendrite will now try to keep HTTP connections open to remote federated servers for a few minutes after a request and attempt to reuse those connections where possible
|
|
||||||
* This should reduce the amount of time spent on TLS handshakes and often speed up requests to remote servers
|
|
||||||
* This new behaviour can be disabled with the `federation_api.disable_http_keepalives` option if needed
|
|
||||||
* A number of dependencies have been updated
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* A bug where the roomserver did not correctly propagate rewritten room state to downstream components (like the federation API and sync API) has been fixed, which could cause issues when performing a federated join to a previously left room
|
|
||||||
* Event auth now correctly parses the `join_authorised_via_users_server` field in the membership event content
|
|
||||||
* Database migrations should no longer produce unique constraint errors at Dendrite startup
|
|
||||||
* The `origin` of device list updates should now be populated correctly
|
|
||||||
* Send-to-device messages will no longer be dropped if we fail to publish them to specific devices
|
|
||||||
* The roomserver query to find state after events will now always resolve state if there are multiple prev events
|
|
||||||
* The roomserver will now return no memberships if querying history visibility for an event which has no state snapshot
|
|
||||||
* The device list updater will now mark a device list as stale if a requesting device ID is not known
|
|
||||||
* Transactions sent to appservices should no longer have accidental duplicated transaction IDs (contributed by [tak-hntlabs](https://github.com/tak-hntlabs))
|
|
||||||
|
|
||||||
## Dendrite 0.9.8 (2022-09-12)
|
|
||||||
|
|
||||||
### Important
|
|
||||||
|
|
||||||
* This is a **security release** to fix a vulnerability where missing events retrieved from other servers did not have their signatures verified in all cases, affecting all versions of Dendrite before 0.9.8. Upgrading to this version is highly recommended. For more information, [see here](https://github.com/matrix-org/dendrite/security/advisories/GHSA-pfw4-xjgm-267c).
|
|
||||||
|
|
||||||
### Features
|
|
||||||
|
|
||||||
* The built-in NATS Server has been updated to the final 2.9.0 release version
|
|
||||||
|
|
||||||
### Fixes
|
|
||||||
|
|
||||||
* Dendrite will now correctly verify the signatures of events retrieved using `/get_missing_events`
|
|
||||||
|
|
||||||
## Dendrite 0.9.7 (2022-09-09)
|
## Dendrite 0.9.7 (2022-09-09)
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
49
Dockerfile
49
Dockerfile
|
@ -1,49 +0,0 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
|
||||||
|
|
||||||
#
|
|
||||||
# base installs required dependencies and runs go mod download to cache dependencies
|
|
||||||
#
|
|
||||||
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.21-alpine3.18 AS base
|
|
||||||
RUN apk --update --no-cache add bash build-base curl git
|
|
||||||
|
|
||||||
#
|
|
||||||
# build creates all needed binaries
|
|
||||||
#
|
|
||||||
FROM --platform=${BUILDPLATFORM} base AS build
|
|
||||||
WORKDIR /src
|
|
||||||
ARG TARGETOS
|
|
||||||
ARG TARGETARCH
|
|
||||||
RUN --mount=target=. \
|
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
|
||||||
USERARCH=`go env GOARCH` \
|
|
||||||
GOARCH="$TARGETARCH" \
|
|
||||||
GOOS="linux" \
|
|
||||||
CGO_ENABLED=$([ "$TARGETARCH" = "$USERARCH" ] && echo "1" || echo "0") \
|
|
||||||
go build -v -trimpath -o /out/ ./cmd/...
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Builds the Dendrite image containing all required binaries
|
|
||||||
#
|
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk --update --no-cache add curl
|
|
||||||
LABEL org.opencontainers.image.title="Dendrite"
|
|
||||||
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
|
||||||
LABEL org.opencontainers.image.documentation="https://matrix-org.github.io/dendrite/"
|
|
||||||
LABEL org.opencontainers.image.vendor="The Matrix.org Foundation C.I.C."
|
|
||||||
|
|
||||||
COPY --from=build /out/create-account /usr/bin/create-account
|
|
||||||
COPY --from=build /out/generate-config /usr/bin/generate-config
|
|
||||||
COPY --from=build /out/generate-keys /usr/bin/generate-keys
|
|
||||||
COPY --from=build /out/dendrite /usr/bin/dendrite
|
|
||||||
|
|
||||||
VOLUME /etc/dendrite
|
|
||||||
WORKDIR /etc/dendrite
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite"]
|
|
||||||
EXPOSE 8008 8448
|
|
||||||
|
|
29
README.md
29
README.md
|
@ -11,19 +11,21 @@ It intends to provide an **efficient**, **reliable** and **scalable** alternativ
|
||||||
a [brand new Go test suite](https://github.com/matrix-org/complement).
|
a [brand new Go test suite](https://github.com/matrix-org/complement).
|
||||||
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
||||||
|
|
||||||
Dendrite is **beta** software, which means:
|
As of October 2020 (current [progress below](#progress)), Dendrite has now entered **beta** which means:
|
||||||
|
|
||||||
- Dendrite is ready for early adopters. We recommend running Dendrite with a PostgreSQL database.
|
- Dendrite is ready for early adopters. We recommend running in Monolith mode with a PostgreSQL database.
|
||||||
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and land significant features.
|
- Dendrite has periodic semver releases. We intend to release new versions as we land significant features.
|
||||||
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
||||||
|
- Breaking changes will not occur on minor releases. This means you can safely upgrade Dendrite without modifying your database or config file.
|
||||||
|
|
||||||
This does not mean:
|
This does not mean:
|
||||||
|
|
||||||
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
||||||
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
||||||
- Dendrite is ready for massive homeserver deployments. There is no high-availability/clustering support.
|
- Dendrite is ready for massive homeserver deployments. You cannot shard each microservice, only run each one on a different machine.
|
||||||
|
|
||||||
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices.
|
||||||
|
In the future, we will be able to scale up to gigantic servers (equivalent to matrix.org) via polylith mode.
|
||||||
|
|
||||||
If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or join us in:
|
If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or join us in:
|
||||||
|
|
||||||
|
@ -36,7 +38,7 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
||||||
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
||||||
more information on requirements.
|
more information on requirements.
|
||||||
|
|
||||||
To build Dendrite, you will need Go 1.20 or later.
|
To build Dendrite, you will need Go 1.18 or later.
|
||||||
|
|
||||||
For a usable federating Dendrite deployment, you will also need:
|
For a usable federating Dendrite deployment, you will also need:
|
||||||
|
|
||||||
|
@ -47,7 +49,7 @@ For a usable federating Dendrite deployment, you will also need:
|
||||||
Also recommended are:
|
Also recommended are:
|
||||||
|
|
||||||
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
- A PostgreSQL database engine, which will perform better than SQLite with many users and/or larger rooms
|
||||||
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/dendrite-sample.conf)
|
- A reverse proxy server, such as nginx, configured [like this sample](https://github.com/matrix-org/dendrite/blob/master/docs/nginx/monolith-sample.conf)
|
||||||
|
|
||||||
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
The [Federation Tester](https://federationtester.matrix.org) can be used to verify your deployment.
|
||||||
|
|
||||||
|
@ -60,7 +62,7 @@ The following instructions are enough to get Dendrite started as a non-federatin
|
||||||
```bash
|
```bash
|
||||||
$ git clone https://github.com/matrix-org/dendrite
|
$ git clone https://github.com/matrix-org/dendrite
|
||||||
$ cd dendrite
|
$ cd dendrite
|
||||||
$ go build -o bin/ ./cmd/...
|
$ ./build.sh
|
||||||
|
|
||||||
# Generate a Matrix signing key for federation (required)
|
# Generate a Matrix signing key for federation (required)
|
||||||
$ ./bin/generate-keys --private-key matrix_key.pem
|
$ ./bin/generate-keys --private-key matrix_key.pem
|
||||||
|
@ -71,25 +73,25 @@ $ ./bin/generate-keys --tls-cert server.crt --tls-key server.key
|
||||||
|
|
||||||
# Copy and modify the config file - you'll need to set a server name and paths to the keys
|
# Copy and modify the config file - you'll need to set a server name and paths to the keys
|
||||||
# at the very least, along with setting up the database connection strings.
|
# at the very least, along with setting up the database connection strings.
|
||||||
$ cp dendrite-sample.yaml dendrite.yaml
|
$ cp dendrite-sample.monolith.yaml dendrite.yaml
|
||||||
|
|
||||||
# Build and run the server:
|
# Build and run the server:
|
||||||
$ ./bin/dendrite --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
$ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
||||||
|
|
||||||
# Create an user account (add -admin for an admin user).
|
# Create an user account (add -admin for an admin user).
|
||||||
# Specify the localpart only, e.g. 'alice' for '@alice:domain.com'
|
# Specify the localpart only, e.g. 'alice' for '@alice:domain.com'
|
||||||
$ ./bin/create-account --config dendrite.yaml --username alice
|
$ ./bin/create-account --config dendrite.yaml --url http://localhost:8008 --username alice
|
||||||
```
|
```
|
||||||
|
|
||||||
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
|
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
|
||||||
|
|
||||||
## Progress
|
## Progress
|
||||||
|
|
||||||
We use a script called "Are We Synapse Yet" which checks Sytest compliance rates. Sytest is a black-box homeserver
|
We use a script called Are We Synapse Yet which checks Sytest compliance rates. Sytest is a black-box homeserver
|
||||||
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it
|
||||||
updates with CI. As of January 2023, we have 100% server-server parity with Synapse, and the client-server parity is at 93% , though check
|
updates with CI. As of August 2022 we're at around 90% CS API coverage and 95% Federation coverage, though check
|
||||||
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
CI for the latest numbers. In practice, this means you can communicate locally and via federation with Synapse
|
||||||
servers such as matrix.org reasonably well, although there are still some missing features (like SSO and Third-party ID APIs).
|
servers such as matrix.org reasonably well, although there are still some missing features (like Search).
|
||||||
|
|
||||||
We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather
|
We are prioritising features that will benefit single-user homeservers first (e.g Receipts, E2E) rather
|
||||||
than features that massive deployments may be interested in (OpenID, Guests, Admin APIs, AS API).
|
than features that massive deployments may be interested in (OpenID, Guests, Admin APIs, AS API).
|
||||||
|
@ -111,7 +113,6 @@ This means Dendrite supports amongst others:
|
||||||
- Guests
|
- Guests
|
||||||
- User Directory
|
- User Directory
|
||||||
- Presence
|
- Presence
|
||||||
- Fulltext search
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,11 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppServiceInternalAPI is used to query user and room alias data from application
|
// AppServiceInternalAPI is used to query user and room alias data from application
|
||||||
|
@ -41,10 +41,6 @@ type AppServiceInternalAPI interface {
|
||||||
req *UserIDExistsRequest,
|
req *UserIDExistsRequest,
|
||||||
resp *UserIDExistsResponse,
|
resp *UserIDExistsResponse,
|
||||||
) error
|
) error
|
||||||
|
|
||||||
Locations(ctx context.Context, req *LocationRequest, resp *LocationResponse) error
|
|
||||||
User(ctx context.Context, request *UserRequest, response *UserResponse) error
|
|
||||||
Protocols(ctx context.Context, req *ProtocolRequest, resp *ProtocolResponse) error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomAliasExistsRequest is a request to an application service
|
// RoomAliasExistsRequest is a request to an application service
|
||||||
|
@ -81,85 +77,6 @@ type UserIDExistsResponse struct {
|
||||||
UserIDExists bool `json:"exists"`
|
UserIDExists bool `json:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
|
||||||
ASProtocolLegacyPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
|
||||||
ASUserLegacyPath = "/_matrix/app/unstable/thirdparty/user"
|
|
||||||
ASLocationLegacyPath = "/_matrix/app/unstable/thirdparty/location"
|
|
||||||
ASRoomAliasExistsLegacyPath = "/rooms/"
|
|
||||||
ASUserExistsLegacyPath = "/users/"
|
|
||||||
|
|
||||||
ASProtocolPath = "/_matrix/app/v1/thirdparty/protocol/"
|
|
||||||
ASUserPath = "/_matrix/app/v1/thirdparty/user"
|
|
||||||
ASLocationPath = "/_matrix/app/v1/thirdparty/location"
|
|
||||||
ASRoomAliasExistsPath = "/_matrix/app/v1/rooms/"
|
|
||||||
ASUserExistsPath = "/_matrix/app/v1/users/"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProtocolRequest struct {
|
|
||||||
Protocol string `json:"protocol,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtocolResponse struct {
|
|
||||||
Protocols map[string]ASProtocolResponse `json:"protocols"`
|
|
||||||
Exists bool `json:"exists"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ASProtocolResponse struct {
|
|
||||||
FieldTypes map[string]FieldType `json:"field_types,omitempty"` // NOTSPEC: field_types is required by the spec
|
|
||||||
Icon string `json:"icon"`
|
|
||||||
Instances []ProtocolInstance `json:"instances"`
|
|
||||||
LocationFields []string `json:"location_fields"`
|
|
||||||
UserFields []string `json:"user_fields"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FieldType struct {
|
|
||||||
Placeholder string `json:"placeholder"`
|
|
||||||
Regexp string `json:"regexp"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProtocolInstance struct {
|
|
||||||
Description string `json:"desc"`
|
|
||||||
Icon string `json:"icon,omitempty"`
|
|
||||||
NetworkID string `json:"network_id,omitempty"` // NOTSPEC: network_id is required by the spec
|
|
||||||
Fields json.RawMessage `json:"fields,omitempty"` // NOTSPEC: fields is required by the spec
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserRequest struct {
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Params string `json:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UserResponse struct {
|
|
||||||
Users []ASUserResponse `json:"users,omitempty"`
|
|
||||||
Exists bool `json:"exists,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ASUserResponse struct {
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
UserID string `json:"userid"`
|
|
||||||
Fields json.RawMessage `json:"fields"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationRequest struct {
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Params string `json:"params"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LocationResponse struct {
|
|
||||||
Locations []ASLocationResponse `json:"locations,omitempty"`
|
|
||||||
Exists bool `json:"exists,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ASLocationResponse struct {
|
|
||||||
Alias string `json:"alias"`
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Fields json.RawMessage `json:"fields"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ErrProfileNotExists is returned when trying to lookup a user's profile that
|
|
||||||
// doesn't exist locally.
|
|
||||||
var ErrProfileNotExists = errors.New("no known profile for given user ID")
|
|
||||||
|
|
||||||
// RetrieveUserProfile is a wrapper that queries both the local database and
|
// RetrieveUserProfile is a wrapper that queries both the local database and
|
||||||
// application services for a given user's profile
|
// application services for a given user's profile
|
||||||
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
||||||
|
@ -167,11 +84,25 @@ func RetrieveUserProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI AppServiceInternalAPI,
|
asAPI AppServiceInternalAPI,
|
||||||
profileAPI userapi.ProfileAPI,
|
profileAPI userapi.ClientUserAPI,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
|
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Try to query the user from the local database
|
// Try to query the user from the local database
|
||||||
profile, err := profileAPI.QueryProfile(ctx, userID)
|
res := &userapi.QueryProfileResponse{}
|
||||||
if err == nil {
|
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
profile := &authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: res.DisplayName,
|
||||||
|
AvatarURL: res.AvatarURL,
|
||||||
|
}
|
||||||
|
if res.UserExists {
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,15 +115,19 @@ func RetrieveUserProfile(
|
||||||
|
|
||||||
// If no user exists, return
|
// If no user exists, return
|
||||||
if !userResp.UserIDExists {
|
if !userResp.UserIDExists {
|
||||||
return nil, ErrProfileNotExists
|
return nil, errors.New("no known profile for given user ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to query the user from the local database again
|
// Try to query the user from the local database again
|
||||||
profile, err = profileAPI.QueryProfile(ctx, userID)
|
err = profileAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: userID}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// profile should not be nil at this point
|
// profile should not be nil at this point
|
||||||
return profile, nil
|
return &authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: res.DisplayName,
|
||||||
|
AvatarURL: res.AvatarURL,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,49 +16,62 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"sync"
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/appservice/consumers"
|
"github.com/matrix-org/dendrite/appservice/consumers"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/inthttp"
|
||||||
"github.com/matrix-org/dendrite/appservice/query"
|
"github.com/matrix-org/dendrite/appservice/query"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AddInternalRoutes registers HTTP handlers for internal API calls
|
||||||
|
func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceInternalAPI) {
|
||||||
|
inthttp.AddRoutes(queryAPI, router)
|
||||||
|
}
|
||||||
|
|
||||||
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
// NewInternalAPI returns a concerete implementation of the internal API. Callers
|
||||||
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
||||||
func NewInternalAPI(
|
func NewInternalAPI(
|
||||||
processContext *process.ProcessContext,
|
base *base.BaseDendrite,
|
||||||
cfg *config.Dendrite,
|
userAPI userapi.UserInternalAPI,
|
||||||
natsInstance *jetstream.NATSInstance,
|
|
||||||
userAPI userapi.AppserviceUserAPI,
|
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) appserviceAPI.AppServiceInternalAPI {
|
) appserviceAPI.AppServiceInternalAPI {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: time.Second * 30,
|
||||||
|
Transport: &http.Transport{
|
||||||
|
DisableKeepAlives: true,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
|
||||||
|
},
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
},
|
||||||
|
}
|
||||||
// Create appserivce query API with an HTTP client that will be used for all
|
// Create appserivce query API with an HTTP client that will be used for all
|
||||||
// outbound and inbound requests (inbound only for the internal API)
|
// outbound and inbound requests (inbound only for the internal API)
|
||||||
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
||||||
Cfg: &cfg.AppServiceAPI,
|
HTTPClient: client,
|
||||||
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
Cfg: &base.Cfg.AppServiceAPI,
|
||||||
CacheMu: sync.Mutex{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(cfg.Derived.ApplicationServices) == 0 {
|
if len(base.Cfg.Derived.ApplicationServices) == 0 {
|
||||||
return appserviceQueryAPI
|
return appserviceQueryAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wrap application services in a type that relates the application service and
|
// Wrap application services in a type that relates the application service and
|
||||||
// a sync.Cond object that can be used to notify workers when there are new
|
// a sync.Cond object that can be used to notify workers when there are new
|
||||||
// events to be sent out.
|
// events to be sent out.
|
||||||
for _, appservice := range cfg.Derived.ApplicationServices {
|
for _, appservice := range base.Cfg.Derived.ApplicationServices {
|
||||||
// Create bot account for this AS if it doesn't already exist
|
// Create bot account for this AS if it doesn't already exist
|
||||||
if err := generateAppServiceAccount(userAPI, appservice, cfg.Global.ServerName); err != nil {
|
if err := generateAppServiceAccount(userAPI, appservice); err != nil {
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
||||||
|
@ -67,10 +80,10 @@ func NewInternalAPI(
|
||||||
|
|
||||||
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
// Only consume if we actually have ASes to track, else we'll just chew cycles needlessly.
|
||||||
// We can't add ASes at runtime so this is safe to do.
|
// We can't add ASes at runtime so this is safe to do.
|
||||||
js, _ := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
js, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream)
|
||||||
consumer := consumers.NewOutputRoomEventConsumer(
|
consumer := consumers.NewOutputRoomEventConsumer(
|
||||||
processContext, &cfg.AppServiceAPI,
|
base.ProcessContext, &base.Cfg.AppServiceAPI,
|
||||||
js, rsAPI,
|
client, js, rsAPI,
|
||||||
)
|
)
|
||||||
if err := consumer.Start(); err != nil {
|
if err := consumer.Start(); err != nil {
|
||||||
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
||||||
|
@ -85,13 +98,11 @@ func NewInternalAPI(
|
||||||
func generateAppServiceAccount(
|
func generateAppServiceAccount(
|
||||||
userAPI userapi.AppserviceUserAPI,
|
userAPI userapi.AppserviceUserAPI,
|
||||||
as config.ApplicationService,
|
as config.ApplicationService,
|
||||||
serverName spec.ServerName,
|
|
||||||
) error {
|
) error {
|
||||||
var accRes userapi.PerformAccountCreationResponse
|
var accRes userapi.PerformAccountCreationResponse
|
||||||
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
||||||
AccountType: userapi.AccountTypeAppService,
|
AccountType: userapi.AccountTypeAppService,
|
||||||
Localpart: as.SenderLocalpart,
|
Localpart: as.SenderLocalpart,
|
||||||
ServerName: serverName,
|
|
||||||
AppServiceID: as.ID,
|
AppServiceID: as.ID,
|
||||||
OnConflict: userapi.ConflictUpdate,
|
OnConflict: userapi.ConflictUpdate,
|
||||||
}, &accRes)
|
}, &accRes)
|
||||||
|
@ -101,7 +112,6 @@ func generateAppServiceAccount(
|
||||||
var devRes userapi.PerformDeviceCreationResponse
|
var devRes userapi.PerformDeviceCreationResponse
|
||||||
err = userAPI.PerformDeviceCreation(context.Background(), &userapi.PerformDeviceCreationRequest{
|
err = userAPI.PerformDeviceCreation(context.Background(), &userapi.PerformDeviceCreationRequest{
|
||||||
Localpart: as.SenderLocalpart,
|
Localpart: as.SenderLocalpart,
|
||||||
ServerName: serverName,
|
|
||||||
AccessToken: as.ASToken,
|
AccessToken: as.ASToken,
|
||||||
DeviceID: &as.SenderLocalpart,
|
DeviceID: &as.SenderLocalpart,
|
||||||
DeviceDisplayName: &as.SenderLocalpart,
|
DeviceDisplayName: &as.SenderLocalpart,
|
||||||
|
|
|
@ -1,606 +0,0 @@
|
||||||
package appservice_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"path"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/federationapi/statistics"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
|
||||||
"github.com/matrix-org/dendrite/syncapi"
|
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
|
||||||
"github.com/matrix-org/dendrite/appservice/consumers"
|
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
|
||||||
rsapi "github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/dendrite/test"
|
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
|
||||||
return &statistics.ServerStatistics{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppserviceInternalAPI(t *testing.T) {
|
|
||||||
|
|
||||||
// Set expected results
|
|
||||||
existingProtocol := "irc"
|
|
||||||
wantLocationResponse := []api.ASLocationResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
|
||||||
wantUserResponse := []api.ASUserResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
|
||||||
wantProtocolResponse := api.ASProtocolResponse{Instances: []api.ProtocolInstance{{Fields: []byte("{}")}}}
|
|
||||||
wantProtocolResult := map[string]api.ASProtocolResponse{
|
|
||||||
existingProtocol: wantProtocolResponse,
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a dummy AS url, handling some cases
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(r.URL.Path, "location"):
|
|
||||||
// Check if we've got an existing protocol, if so, return a proper response.
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantLocationResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode([]api.ASLocationResponse{}); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case strings.Contains(r.URL.Path, "user"):
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantUserResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode([]api.UserResponse{}); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case strings.Contains(r.URL.Path, "protocol"):
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantProtocolResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode(nil); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Logf("hit location: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
// The test cases to run
|
|
||||||
runCases := func(t *testing.T, testAPI api.AppServiceInternalAPI) {
|
|
||||||
t.Run("UserIDExists", func(t *testing.T) {
|
|
||||||
testUserIDExists(t, testAPI, "@as-testing:test", true)
|
|
||||||
testUserIDExists(t, testAPI, "@as1-testing:test", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("AliasExists", func(t *testing.T) {
|
|
||||||
testAliasExists(t, testAPI, "@asroom-testing:test", true)
|
|
||||||
testAliasExists(t, testAPI, "@asroom1-testing:test", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Locations", func(t *testing.T) {
|
|
||||||
testLocations(t, testAPI, existingProtocol, wantLocationResponse)
|
|
||||||
testLocations(t, testAPI, "abc", nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("User", func(t *testing.T) {
|
|
||||||
testUser(t, testAPI, existingProtocol, wantUserResponse)
|
|
||||||
testUser(t, testAPI, "abc", nil)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Protocols", func(t *testing.T) {
|
|
||||||
testProtocol(t, testAPI, existingProtocol, wantProtocolResult)
|
|
||||||
testProtocol(t, testAPI, existingProtocol, wantProtocolResult) // tests the cache
|
|
||||||
testProtocol(t, testAPI, "", wantProtocolResult) // tests getting all protocols
|
|
||||||
testProtocol(t, testAPI, "abc", nil)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
|
||||||
cfg, ctx, close := testrig.CreateConfig(t, dbType)
|
|
||||||
defer close()
|
|
||||||
|
|
||||||
// Create a dummy application service
|
|
||||||
as := &config.ApplicationService{
|
|
||||||
ID: "someID",
|
|
||||||
URL: srv.URL,
|
|
||||||
ASToken: "",
|
|
||||||
HSToken: "",
|
|
||||||
SenderLocalpart: "senderLocalPart",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
|
||||||
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
|
||||||
},
|
|
||||||
Protocols: []string{existingProtocol},
|
|
||||||
}
|
|
||||||
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
|
||||||
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
|
||||||
t.Cleanup(func() {
|
|
||||||
ctx.ShutdownDendrite()
|
|
||||||
ctx.WaitForShutdown()
|
|
||||||
})
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
// Create required internal APIs
|
|
||||||
natsInstance := jetstream.NATSInstance{}
|
|
||||||
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
|
||||||
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil)
|
|
||||||
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
|
||||||
|
|
||||||
runCases(t, asAPI)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAppserviceInternalAPI_UnixSocket_Simple(t *testing.T) {
|
|
||||||
|
|
||||||
// Set expected results
|
|
||||||
existingProtocol := "irc"
|
|
||||||
wantLocationResponse := []api.ASLocationResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
|
||||||
wantUserResponse := []api.ASUserResponse{{Protocol: existingProtocol, Fields: []byte("{}")}}
|
|
||||||
wantProtocolResponse := api.ASProtocolResponse{Instances: []api.ProtocolInstance{{Fields: []byte("{}")}}}
|
|
||||||
|
|
||||||
// create a dummy AS url, handling some cases
|
|
||||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch {
|
|
||||||
case strings.Contains(r.URL.Path, "location"):
|
|
||||||
// Check if we've got an existing protocol, if so, return a proper response.
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantLocationResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode([]api.ASLocationResponse{}); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case strings.Contains(r.URL.Path, "user"):
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantUserResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode([]api.UserResponse{}); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
case strings.Contains(r.URL.Path, "protocol"):
|
|
||||||
if r.URL.Path[len(r.URL.Path)-len(existingProtocol):] == existingProtocol {
|
|
||||||
if err := json.NewEncoder(w).Encode(wantProtocolResponse); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := json.NewEncoder(w).Encode(nil); err != nil {
|
|
||||||
t.Fatalf("failed to encode response: %s", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
default:
|
|
||||||
t.Logf("hit location: %s", r.URL.Path)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
|
||||||
socket := path.Join(tmpDir, "socket")
|
|
||||||
l, err := net.Listen("unix", socket)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
_ = srv.Listener.Close()
|
|
||||||
srv.Listener = l
|
|
||||||
srv.Start()
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
cfg, ctx, tearDown := testrig.CreateConfig(t, test.DBTypeSQLite)
|
|
||||||
defer tearDown()
|
|
||||||
|
|
||||||
// Create a dummy application service
|
|
||||||
as := &config.ApplicationService{
|
|
||||||
ID: "someID",
|
|
||||||
URL: fmt.Sprintf("unix://%s", socket),
|
|
||||||
ASToken: "",
|
|
||||||
HSToken: "",
|
|
||||||
SenderLocalpart: "senderLocalPart",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {{RegexpObject: regexp.MustCompile("as-.*")}},
|
|
||||||
"aliases": {{RegexpObject: regexp.MustCompile("asroom-.*")}},
|
|
||||||
},
|
|
||||||
Protocols: []string{existingProtocol},
|
|
||||||
}
|
|
||||||
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
|
||||||
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
ctx.ShutdownDendrite()
|
|
||||||
ctx.WaitForShutdown()
|
|
||||||
})
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
// Create required internal APIs
|
|
||||||
natsInstance := jetstream.NATSInstance{}
|
|
||||||
cm := sqlutil.NewConnectionManager(ctx, cfg.Global.DatabaseOptions)
|
|
||||||
rsAPI := roomserver.NewInternalAPI(ctx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil)
|
|
||||||
usrAPI := userapi.NewInternalAPI(ctx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
asAPI := appservice.NewInternalAPI(ctx, cfg, &natsInstance, usrAPI, rsAPI)
|
|
||||||
|
|
||||||
t.Run("UserIDExists", func(t *testing.T) {
|
|
||||||
testUserIDExists(t, asAPI, "@as-testing:test", true)
|
|
||||||
testUserIDExists(t, asAPI, "@as1-testing:test", false)
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUserIDExists(t *testing.T, asAPI api.AppServiceInternalAPI, userID string, wantExists bool) {
|
|
||||||
ctx := context.Background()
|
|
||||||
userResp := &api.UserIDExistsResponse{}
|
|
||||||
|
|
||||||
if err := asAPI.UserIDExists(ctx, &api.UserIDExistsRequest{
|
|
||||||
UserID: userID,
|
|
||||||
}, userResp); err != nil {
|
|
||||||
t.Errorf("failed to get userID: %s", err)
|
|
||||||
}
|
|
||||||
if userResp.UserIDExists != wantExists {
|
|
||||||
t.Errorf("unexpected result for UserIDExists(%s): %v, expected %v", userID, userResp.UserIDExists, wantExists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testAliasExists(t *testing.T, asAPI api.AppServiceInternalAPI, alias string, wantExists bool) {
|
|
||||||
ctx := context.Background()
|
|
||||||
aliasResp := &api.RoomAliasExistsResponse{}
|
|
||||||
|
|
||||||
if err := asAPI.RoomAliasExists(ctx, &api.RoomAliasExistsRequest{
|
|
||||||
Alias: alias,
|
|
||||||
}, aliasResp); err != nil {
|
|
||||||
t.Errorf("failed to get alias: %s", err)
|
|
||||||
}
|
|
||||||
if aliasResp.AliasExists != wantExists {
|
|
||||||
t.Errorf("unexpected result for RoomAliasExists(%s): %v, expected %v", alias, aliasResp.AliasExists, wantExists)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testLocations(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult []api.ASLocationResponse) {
|
|
||||||
ctx := context.Background()
|
|
||||||
locationResp := &api.LocationResponse{}
|
|
||||||
|
|
||||||
if err := asAPI.Locations(ctx, &api.LocationRequest{
|
|
||||||
Protocol: proto,
|
|
||||||
}, locationResp); err != nil {
|
|
||||||
t.Errorf("failed to get locations: %s", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(locationResp.Locations, wantResult) {
|
|
||||||
t.Errorf("unexpected result for Locations(%s): %+v, expected %+v", proto, locationResp.Locations, wantResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUser(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult []api.ASUserResponse) {
|
|
||||||
ctx := context.Background()
|
|
||||||
userResp := &api.UserResponse{}
|
|
||||||
|
|
||||||
if err := asAPI.User(ctx, &api.UserRequest{
|
|
||||||
Protocol: proto,
|
|
||||||
}, userResp); err != nil {
|
|
||||||
t.Errorf("failed to get user: %s", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(userResp.Users, wantResult) {
|
|
||||||
t.Errorf("unexpected result for User(%s): %+v, expected %+v", proto, userResp.Users, wantResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testProtocol(t *testing.T, asAPI api.AppServiceInternalAPI, proto string, wantResult map[string]api.ASProtocolResponse) {
|
|
||||||
ctx := context.Background()
|
|
||||||
protoResp := &api.ProtocolResponse{}
|
|
||||||
|
|
||||||
if err := asAPI.Protocols(ctx, &api.ProtocolRequest{
|
|
||||||
Protocol: proto,
|
|
||||||
}, protoResp); err != nil {
|
|
||||||
t.Errorf("failed to get Protocols: %s", err)
|
|
||||||
}
|
|
||||||
if !reflect.DeepEqual(protoResp.Protocols, wantResult) {
|
|
||||||
t.Errorf("unexpected result for Protocols(%s): %+v, expected %+v", proto, protoResp.Protocols[proto], wantResult)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tests that the roomserver consumer only receives one invite
|
|
||||||
func TestRoomserverConsumerOneInvite(t *testing.T) {
|
|
||||||
|
|
||||||
alice := test.NewUser(t)
|
|
||||||
bob := test.NewUser(t)
|
|
||||||
room := test.NewRoom(t, alice)
|
|
||||||
|
|
||||||
// Invite Bob
|
|
||||||
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
|
||||||
"membership": "invite",
|
|
||||||
}, test.WithStateKey(bob.ID))
|
|
||||||
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
|
||||||
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
|
||||||
defer closeDB()
|
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
|
||||||
natsInstance := &jetstream.NATSInstance{}
|
|
||||||
|
|
||||||
evChan := make(chan struct{})
|
|
||||||
// create a dummy AS url, handling the events
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var txn consumers.ApplicationServiceTransaction
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&txn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, ev := range txn.Events {
|
|
||||||
if ev.Type != spec.MRoomMember {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Usually we would check the event content for the membership, but since
|
|
||||||
// we only invited bob, this should be fine for this test.
|
|
||||||
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
|
||||||
evChan <- struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
as := &config.ApplicationService{
|
|
||||||
ID: "someID",
|
|
||||||
URL: srv.URL,
|
|
||||||
ASToken: "",
|
|
||||||
HSToken: "",
|
|
||||||
SenderLocalpart: "senderLocalPart",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
|
||||||
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
|
||||||
|
|
||||||
// Create a dummy application service
|
|
||||||
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
|
||||||
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
// Create required internal APIs
|
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil)
|
|
||||||
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
// start the consumer
|
|
||||||
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
|
||||||
|
|
||||||
// Create the room
|
|
||||||
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
|
||||||
t.Fatalf("failed to send events: %v", err)
|
|
||||||
}
|
|
||||||
var seenInvitesForBob int
|
|
||||||
waitLoop:
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-time.After(time.Millisecond * 50): // wait for the AS to process the events
|
|
||||||
break waitLoop
|
|
||||||
case <-evChan:
|
|
||||||
seenInvitesForBob++
|
|
||||||
if seenInvitesForBob != 1 {
|
|
||||||
t.Fatalf("received unexpected invites: %d", seenInvitesForBob)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close(evChan)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: If this test panics, it is because we timed out waiting for the
|
|
||||||
// join event to come through to the appservice and we close the DB/shutdown Dendrite. This makes the
|
|
||||||
// syncAPI unhappy, as it is unable to write to the database.
|
|
||||||
func TestOutputAppserviceEvent(t *testing.T) {
|
|
||||||
alice := test.NewUser(t)
|
|
||||||
bob := test.NewUser(t)
|
|
||||||
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
|
||||||
cfg, processCtx, closeDB := testrig.CreateConfig(t, dbType)
|
|
||||||
defer closeDB()
|
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
|
||||||
natsInstance := &jetstream.NATSInstance{}
|
|
||||||
|
|
||||||
evChan := make(chan struct{})
|
|
||||||
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
// Create required internal APIs
|
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil)
|
|
||||||
|
|
||||||
// Create the router, so we can hit `/joined_members`
|
|
||||||
routers := httputil.NewRouters()
|
|
||||||
|
|
||||||
accessTokens := map[*test.User]userDevice{
|
|
||||||
bob: {},
|
|
||||||
}
|
|
||||||
|
|
||||||
usrAPI := userapi.NewInternalAPI(processCtx, cfg, cm, natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
clientapi.AddPublicRoutes(processCtx, routers, cfg, natsInstance, nil, rsAPI, nil, nil, nil, usrAPI, nil, nil, caching.DisableMetrics)
|
|
||||||
createAccessTokens(t, accessTokens, usrAPI, processCtx.Context(), routers)
|
|
||||||
|
|
||||||
room := test.NewRoom(t, alice)
|
|
||||||
|
|
||||||
// Invite Bob
|
|
||||||
room.CreateAndInsert(t, alice, spec.MRoomMember, map[string]interface{}{
|
|
||||||
"membership": "invite",
|
|
||||||
}, test.WithStateKey(bob.ID))
|
|
||||||
|
|
||||||
// create a dummy AS url, handling the events
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var txn consumers.ApplicationServiceTransaction
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&txn)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, ev := range txn.Events {
|
|
||||||
if ev.Type != spec.MRoomMember {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ev.StateKey != nil && *ev.StateKey == bob.ID {
|
|
||||||
membership := gjson.GetBytes(ev.Content, "membership").Str
|
|
||||||
t.Logf("Processing membership: %s", membership)
|
|
||||||
switch membership {
|
|
||||||
case spec.Invite:
|
|
||||||
// Accept the invite
|
|
||||||
joinEv := room.CreateAndInsert(t, bob, spec.MRoomMember, map[string]interface{}{
|
|
||||||
"membership": "join",
|
|
||||||
}, test.WithStateKey(bob.ID))
|
|
||||||
|
|
||||||
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, []*types.HeaderedEvent{joinEv}, "test", "test", "test", nil, false); err != nil {
|
|
||||||
t.Fatalf("failed to send events: %v", err)
|
|
||||||
}
|
|
||||||
case spec.Join: // the AS has received the join event, now hit `/joined_members` to validate that
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/_matrix/client/v3/rooms/"+room.ID+"/joined_members", nil)
|
|
||||||
req.Header.Set("Authorization", "Bearer "+accessTokens[bob].accessToken)
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("expected HTTP 200, got %d: %s", rec.Code, rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Both Alice and Bob should be joined. If not, we have a race condition
|
|
||||||
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+alice.ID).Exists() {
|
|
||||||
t.Errorf("Alice is not joined to the room") // in theory should not happen
|
|
||||||
}
|
|
||||||
if !gjson.GetBytes(rec.Body.Bytes(), "joined."+bob.ID).Exists() {
|
|
||||||
t.Errorf("Bob is not joined to the room")
|
|
||||||
}
|
|
||||||
evChan <- struct{}{}
|
|
||||||
default:
|
|
||||||
t.Fatalf("Unexpected membership: %s", membership)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
as := &config.ApplicationService{
|
|
||||||
ID: "someID",
|
|
||||||
URL: srv.URL,
|
|
||||||
ASToken: "",
|
|
||||||
HSToken: "",
|
|
||||||
SenderLocalpart: "senderLocalPart",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {{RegexpObject: regexp.MustCompile(bob.ID)}},
|
|
||||||
"aliases": {{RegexpObject: regexp.MustCompile(room.ID)}},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
as.CreateHTTPClient(cfg.AppServiceAPI.DisableTLSValidation)
|
|
||||||
|
|
||||||
// Create a dummy application service
|
|
||||||
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
|
||||||
|
|
||||||
// Prepare AS Streams on the old topic to validate that they get deleted
|
|
||||||
jsCtx, _ := natsInstance.Prepare(processCtx, &cfg.Global.JetStream)
|
|
||||||
|
|
||||||
token := jetstream.Tokenise(as.ID)
|
|
||||||
if err := jetstream.JetStreamConsumer(
|
|
||||||
processCtx.Context(), jsCtx, cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
|
||||||
cfg.Global.JetStream.Durable("Appservice_"+token),
|
|
||||||
50, // maximum number of events to send in a single transaction
|
|
||||||
func(ctx context.Context, msgs []*nats.Msg) bool {
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start the syncAPI to have `/joined_members` available
|
|
||||||
syncapi.AddPublicRoutes(processCtx, routers, cfg, cm, natsInstance, usrAPI, rsAPI, caches, caching.DisableMetrics)
|
|
||||||
|
|
||||||
// start the consumer
|
|
||||||
appservice.NewInternalAPI(processCtx, cfg, natsInstance, usrAPI, rsAPI)
|
|
||||||
|
|
||||||
// At this point, the old JetStream consumers should be deleted
|
|
||||||
for consumer := range jsCtx.Consumers(cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent)) {
|
|
||||||
if consumer.Name == cfg.Global.JetStream.Durable("Appservice_"+token)+"Pull" {
|
|
||||||
t.Fatalf("Consumer still exists")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the room, this triggers the AS to receive an invite for Bob.
|
|
||||||
if err := rsapi.SendEvents(context.Background(), rsAPI, rsapi.KindNew, room.Events(), "test", "test", "test", nil, false); err != nil {
|
|
||||||
t.Fatalf("failed to send events: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
// Pretty generous timeout duration...
|
|
||||||
case <-time.After(time.Millisecond * 1000): // wait for the AS to process the events
|
|
||||||
t.Errorf("Timed out waiting for join event")
|
|
||||||
case <-evChan:
|
|
||||||
}
|
|
||||||
close(evChan)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
type userDevice struct {
|
|
||||||
accessToken string
|
|
||||||
deviceID string
|
|
||||||
password string
|
|
||||||
}
|
|
||||||
|
|
||||||
func createAccessTokens(t *testing.T, accessTokens map[*test.User]userDevice, userAPI uapi.UserInternalAPI, ctx context.Context, routers httputil.Routers) {
|
|
||||||
t.Helper()
|
|
||||||
for u := range accessTokens {
|
|
||||||
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
|
||||||
userRes := &uapi.PerformAccountCreationResponse{}
|
|
||||||
password := util.RandomString(8)
|
|
||||||
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
|
||||||
AccountType: u.AccountType,
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: password,
|
|
||||||
}, userRes); err != nil {
|
|
||||||
t.Errorf("failed to create account: %s", err)
|
|
||||||
}
|
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
|
||||||
"type": authtypes.LoginTypePassword,
|
|
||||||
"identifier": map[string]interface{}{
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": u.ID,
|
|
||||||
},
|
|
||||||
"password": password,
|
|
||||||
}))
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("failed to login: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
accessTokens[u] = userDevice{
|
|
||||||
accessToken: gjson.GetBytes(rec.Body.Bytes(), "access_token").String(),
|
|
||||||
deviceID: gjson.GetBytes(rec.Body.Bytes(), "device_id").String(),
|
|
||||||
password: password,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -22,33 +22,24 @@ import (
|
||||||
"math"
|
"math"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ApplicationServiceTransaction is the transaction that is sent off to an
|
|
||||||
// application service.
|
|
||||||
type ApplicationServiceTransaction struct {
|
|
||||||
Events []synctypes.ClientEvent `json:"events"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// OutputRoomEventConsumer consumes events that originated in the room server.
|
// OutputRoomEventConsumer consumes events that originated in the room server.
|
||||||
type OutputRoomEventConsumer struct {
|
type OutputRoomEventConsumer struct {
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cfg *config.AppServiceAPI
|
cfg *config.AppServiceAPI
|
||||||
|
client *http.Client
|
||||||
jetstream nats.JetStreamContext
|
jetstream nats.JetStreamContext
|
||||||
topic string
|
topic string
|
||||||
rsAPI api.AppserviceRoomserverAPI
|
rsAPI api.AppserviceRoomserverAPI
|
||||||
|
@ -64,21 +55,22 @@ type appserviceState struct {
|
||||||
func NewOutputRoomEventConsumer(
|
func NewOutputRoomEventConsumer(
|
||||||
process *process.ProcessContext,
|
process *process.ProcessContext,
|
||||||
cfg *config.AppServiceAPI,
|
cfg *config.AppServiceAPI,
|
||||||
|
client *http.Client,
|
||||||
js nats.JetStreamContext,
|
js nats.JetStreamContext,
|
||||||
rsAPI api.AppserviceRoomserverAPI,
|
rsAPI api.AppserviceRoomserverAPI,
|
||||||
) *OutputRoomEventConsumer {
|
) *OutputRoomEventConsumer {
|
||||||
return &OutputRoomEventConsumer{
|
return &OutputRoomEventConsumer{
|
||||||
ctx: process.Context(),
|
ctx: process.Context(),
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
|
client: client,
|
||||||
jetstream: js,
|
jetstream: js,
|
||||||
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputAppserviceEvent),
|
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputRoomEvent),
|
||||||
rsAPI: rsAPI,
|
rsAPI: rsAPI,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start consuming from room servers
|
// Start consuming from room servers
|
||||||
func (s *OutputRoomEventConsumer) Start() error {
|
func (s *OutputRoomEventConsumer) Start() error {
|
||||||
durableNames := make([]string, 0, len(s.cfg.Derived.ApplicationServices))
|
|
||||||
for _, as := range s.cfg.Derived.ApplicationServices {
|
for _, as := range s.cfg.Derived.ApplicationServices {
|
||||||
appsvc := as
|
appsvc := as
|
||||||
state := &appserviceState{
|
state := &appserviceState{
|
||||||
|
@ -96,15 +88,6 @@ func (s *OutputRoomEventConsumer) Start() error {
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("failed to create %q consumer: %w", token, err)
|
return fmt.Errorf("failed to create %q consumer: %w", token, err)
|
||||||
}
|
}
|
||||||
durableNames = append(durableNames, s.cfg.Matrix.JetStream.Durable("Appservice_"+token))
|
|
||||||
}
|
|
||||||
// Cleanup any consumers still existing on the OutputRoomEvent stream
|
|
||||||
// to avoid messages not being deleted
|
|
||||||
for _, consumerName := range durableNames {
|
|
||||||
err := s.jetstream.DeleteConsumer(s.cfg.Matrix.JetStream.Prefixed(jetstream.OutputRoomEvent), consumerName+"Pull")
|
|
||||||
if err != nil && err != nats.ErrConsumerNotFound {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -115,13 +98,8 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
||||||
) bool {
|
) bool {
|
||||||
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
||||||
events := make([]*types.HeaderedEvent, 0, len(msgs))
|
events := make([]*gomatrixserverlib.HeaderedEvent, 0, len(msgs))
|
||||||
for _, msg := range msgs {
|
for _, msg := range msgs {
|
||||||
// Only handle events we care about
|
|
||||||
receivedType := api.OutputType(msg.Header.Get(jetstream.RoomEventType))
|
|
||||||
if receivedType != api.OutputTypeNewRoomEvent && receivedType != api.OutputTypeNewInviteEvent {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Parse out the event JSON
|
// Parse out the event JSON
|
||||||
var output api.OutputEvent
|
var output api.OutputEvent
|
||||||
if err := json.Unmarshal(msg.Data, &output); err != nil {
|
if err := json.Unmarshal(msg.Data, &output); err != nil {
|
||||||
|
@ -138,7 +116,6 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
||||||
newEventID := output.NewRoomEvent.Event.EventID()
|
newEventID := output.NewRoomEvent.Event.EventID()
|
||||||
eventsReq := &api.QueryEventsByIDRequest{
|
eventsReq := &api.QueryEventsByIDRequest{
|
||||||
RoomID: output.NewRoomEvent.Event.RoomID().String(),
|
|
||||||
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
||||||
}
|
}
|
||||||
eventsRes := &api.QueryEventsByIDResponse{}
|
eventsRes := &api.QueryEventsByIDResponse{}
|
||||||
|
@ -156,6 +133,12 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case api.OutputTypeNewInviteEvent:
|
||||||
|
if output.NewInviteEvent == nil || !s.appserviceIsInterestedInEvent(ctx, output.NewInviteEvent.Event, state.ApplicationService) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
events = append(events, output.NewInviteEvent.Event)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -168,60 +151,41 @@ func (s *OutputRoomEventConsumer) onMessage(
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
txnID := ""
|
|
||||||
// Try to get the message metadata, if we're able to, use the timestamp as the txnID
|
|
||||||
metadata, err := msgs[0].Metadata()
|
|
||||||
if err == nil {
|
|
||||||
txnID = strconv.Itoa(int(metadata.Timestamp.UnixNano()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send event to any relevant application services. If we hit
|
// Send event to any relevant application services. If we hit
|
||||||
// an error here, return false, so that we negatively ack.
|
// an error here, return false, so that we negatively ack.
|
||||||
log.WithField("appservice", state.ID).Debugf("Appservice worker sending %d events(s) from roomserver", len(events))
|
log.WithField("appservice", state.ID).Debugf("Appservice worker sending %d events(s) from roomserver", len(events))
|
||||||
return s.sendEvents(ctx, state, events, txnID) == nil
|
return s.sendEvents(ctx, state, events) == nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendEvents passes events to the appservice by using the transactions
|
// sendEvents passes events to the appservice by using the transactions
|
||||||
// endpoint. It will block for the backoff period if necessary.
|
// endpoint. It will block for the backoff period if necessary.
|
||||||
func (s *OutputRoomEventConsumer) sendEvents(
|
func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
ctx context.Context, state *appserviceState,
|
ctx context.Context, state *appserviceState,
|
||||||
events []*types.HeaderedEvent,
|
events []*gomatrixserverlib.HeaderedEvent,
|
||||||
txnID string,
|
|
||||||
) error {
|
) error {
|
||||||
// Create the transaction body.
|
// Create the transaction body.
|
||||||
transaction, err := json.Marshal(
|
transaction, err := json.Marshal(
|
||||||
ApplicationServiceTransaction{
|
gomatrixserverlib.ApplicationServiceTransaction{
|
||||||
Events: synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(events), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
Events: gomatrixserverlib.HeaderedToClientEvents(events, gomatrixserverlib.FormatAll),
|
||||||
return s.rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If txnID is not defined, generate one from the events.
|
// TODO: We should probably be more intelligent and pick something not
|
||||||
if txnID == "" {
|
// in the control of the event. A NATS timestamp header or something maybe.
|
||||||
txnID = fmt.Sprintf("%d_%d", events[0].PDU.OriginServerTS(), len(transaction))
|
txnID := events[0].Event.OriginServerTS()
|
||||||
}
|
|
||||||
|
|
||||||
// Send the transaction to the appservice.
|
// Send the transaction to the appservice.
|
||||||
// https://spec.matrix.org/v1.9/application-service-api/#pushing-events
|
// https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid
|
||||||
path := "_matrix/app/v1/transactions"
|
address := fmt.Sprintf("%s/transactions/%d?access_token=%s", state.URL, txnID, url.QueryEscape(state.HSToken))
|
||||||
if s.cfg.LegacyPaths {
|
|
||||||
path = "transactions"
|
|
||||||
}
|
|
||||||
address := fmt.Sprintf("%s/%s/%s", state.RequestUrl(), path, txnID)
|
|
||||||
if s.cfg.LegacyAuth {
|
|
||||||
address += "?access_token=" + url.QueryEscape(state.HSToken)
|
|
||||||
}
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "PUT", address, bytes.NewBuffer(transaction))
|
req, err := http.NewRequestWithContext(ctx, "PUT", address, bytes.NewBuffer(transaction))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", state.HSToken))
|
resp, err := s.client.Do(req)
|
||||||
resp, err := state.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return state.backoffAndPause(err)
|
return state.backoffAndPause(err)
|
||||||
}
|
}
|
||||||
|
@ -232,7 +196,7 @@ func (s *OutputRoomEventConsumer) sendEvents(
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
state.backoff = 0
|
state.backoff = 0
|
||||||
default:
|
default:
|
||||||
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice url %s", resp.StatusCode, address))
|
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice", resp.StatusCode))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -252,30 +216,24 @@ func (s *appserviceState) backoffAndPause(err error) error {
|
||||||
// event falls within one of a given application service's namespaces.
|
// event falls within one of a given application service's namespaces.
|
||||||
//
|
//
|
||||||
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
// TODO: This should be cached, see https://github.com/matrix-org/dendrite/issues/1682
|
||||||
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
user := ""
|
|
||||||
userID, err := s.rsAPI.QueryUserIDForSender(ctx, event.RoomID(), event.SenderID())
|
|
||||||
if err == nil {
|
|
||||||
user = userID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case appservice.URL == "":
|
case appservice.URL == "":
|
||||||
return false
|
return false
|
||||||
case appservice.IsInterestedInUserID(user):
|
case appservice.IsInterestedInUserID(event.Sender()):
|
||||||
return true
|
return true
|
||||||
case appservice.IsInterestedInRoomID(event.RoomID().String()):
|
case appservice.IsInterestedInRoomID(event.RoomID()):
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if event.Type() == spec.MRoomMember && event.StateKey() != nil {
|
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
||||||
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check all known room aliases of the room the event came from
|
// Check all known room aliases of the room the event came from
|
||||||
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID().String()}
|
queryReq := api.GetAliasesForRoomIDRequest{RoomID: event.RoomID()}
|
||||||
var queryRes api.GetAliasesForRoomIDResponse
|
var queryRes api.GetAliasesForRoomIDResponse
|
||||||
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
||||||
for _, alias := range queryRes.Aliases {
|
for _, alias := range queryRes.Aliases {
|
||||||
|
@ -286,7 +244,7 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
"room_id": event.RoomID().String(),
|
"room_id": event.RoomID(),
|
||||||
}).WithError(err).Errorf("Unable to get aliases for room")
|
}).WithError(err).Errorf("Unable to get aliases for room")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -296,13 +254,13 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
|
|
||||||
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
||||||
// appservice has membership at the time a given event was created.
|
// appservice has membership at the time a given event was created.
|
||||||
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||||
// TODO: This is only checking the current room state, not the state at
|
// TODO: This is only checking the current room state, not the state at
|
||||||
// the event in question. Pretty sure this is what Synapse does too, but
|
// the event in question. Pretty sure this is what Synapse does too, but
|
||||||
// until we have a lighter way of checking the state before the event that
|
// until we have a lighter way of checking the state before the event that
|
||||||
// doesn't involve state res, then this is probably OK.
|
// doesn't involve state res, then this is probably OK.
|
||||||
membershipReq := &api.QueryMembershipsForRoomRequest{
|
membershipReq := &api.QueryMembershipsForRoomRequest{
|
||||||
RoomID: event.RoomID().String(),
|
RoomID: event.RoomID(),
|
||||||
JoinedOnly: true,
|
JoinedOnly: true,
|
||||||
}
|
}
|
||||||
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
||||||
|
@ -314,7 +272,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
switch {
|
switch {
|
||||||
case ev.StateKey == nil:
|
case ev.StateKey == nil:
|
||||||
continue
|
continue
|
||||||
case ev.Type != spec.MRoomMember:
|
case ev.Type != gomatrixserverlib.MRoomMember:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
var membership gomatrixserverlib.MemberContent
|
var membership gomatrixserverlib.MemberContent
|
||||||
|
@ -322,7 +280,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
switch {
|
switch {
|
||||||
case err != nil:
|
case err != nil:
|
||||||
continue
|
continue
|
||||||
case membership.Membership == spec.Join:
|
case membership.Membership == gomatrixserverlib.Join:
|
||||||
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -331,7 +289,7 @@ func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, e
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"appservice": appservice.ID,
|
"appservice": appservice.ID,
|
||||||
"room_id": event.RoomID().String(),
|
"room_id": event.RoomID(),
|
||||||
}).WithError(err).Errorf("Unable to get membership for room")
|
}).WithError(err).Errorf("Unable to get membership for room")
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
|
60
appservice/inthttp/client.go
Normal file
60
appservice/inthttp/client.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package inthttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HTTP paths for the internal HTTP APIs
|
||||||
|
const (
|
||||||
|
AppServiceRoomAliasExistsPath = "/appservice/RoomAliasExists"
|
||||||
|
AppServiceUserIDExistsPath = "/appservice/UserIDExists"
|
||||||
|
)
|
||||||
|
|
||||||
|
// httpAppServiceQueryAPI contains the URL to an appservice query API and a
|
||||||
|
// reference to a httpClient used to reach it
|
||||||
|
type httpAppServiceQueryAPI struct {
|
||||||
|
appserviceURL string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAppserviceClient creates a AppServiceQueryAPI implemented by talking
|
||||||
|
// to a HTTP POST API.
|
||||||
|
// If httpClient is nil an error is returned
|
||||||
|
func NewAppserviceClient(
|
||||||
|
appserviceURL string,
|
||||||
|
httpClient *http.Client,
|
||||||
|
) (api.AppServiceInternalAPI, error) {
|
||||||
|
if httpClient == nil {
|
||||||
|
return nil, errors.New("NewRoomserverAliasAPIHTTP: httpClient is <nil>")
|
||||||
|
}
|
||||||
|
return &httpAppServiceQueryAPI{appserviceURL, httpClient}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomAliasExists implements AppServiceQueryAPI
|
||||||
|
func (h *httpAppServiceQueryAPI) RoomAliasExists(
|
||||||
|
ctx context.Context,
|
||||||
|
request *api.RoomAliasExistsRequest,
|
||||||
|
response *api.RoomAliasExistsResponse,
|
||||||
|
) error {
|
||||||
|
return httputil.CallInternalRPCAPI(
|
||||||
|
"RoomAliasExists", h.appserviceURL+AppServiceRoomAliasExistsPath,
|
||||||
|
h.httpClient, ctx, request, response,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserIDExists implements AppServiceQueryAPI
|
||||||
|
func (h *httpAppServiceQueryAPI) UserIDExists(
|
||||||
|
ctx context.Context,
|
||||||
|
request *api.UserIDExistsRequest,
|
||||||
|
response *api.UserIDExistsResponse,
|
||||||
|
) error {
|
||||||
|
return httputil.CallInternalRPCAPI(
|
||||||
|
"UserIDExists", h.appserviceURL+AppServiceUserIDExistsPath,
|
||||||
|
h.httpClient, ctx, request, response,
|
||||||
|
)
|
||||||
|
}
|
20
appservice/inthttp/server.go
Normal file
20
appservice/inthttp/server.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package inthttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddRoutes adds the AppServiceQueryAPI handlers to the http.ServeMux.
|
||||||
|
func AddRoutes(a api.AppServiceInternalAPI, internalAPIMux *mux.Router) {
|
||||||
|
internalAPIMux.Handle(
|
||||||
|
AppServiceRoomAliasExistsPath,
|
||||||
|
httputil.MakeInternalRPCAPI("AppserviceRoomAliasExists", a.RoomAliasExists),
|
||||||
|
)
|
||||||
|
|
||||||
|
internalAPIMux.Handle(
|
||||||
|
AppServiceUserIDExistsPath,
|
||||||
|
httputil.MakeInternalRPCAPI("AppserviceUserIDExists", a.UserIDExists),
|
||||||
|
)
|
||||||
|
}
|
|
@ -18,25 +18,22 @@ package query
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"sync"
|
|
||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice/api"
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
opentracing "github.com/opentracing/opentracing-go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const roomAliasExistsPath = "/rooms/"
|
||||||
|
const userIDExistsPath = "/users/"
|
||||||
|
|
||||||
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
// AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI
|
||||||
type AppServiceQueryAPI struct {
|
type AppServiceQueryAPI struct {
|
||||||
Cfg *config.AppServiceAPI
|
HTTPClient *http.Client
|
||||||
ProtocolCache map[string]api.ASProtocolResponse
|
Cfg *config.AppServiceAPI
|
||||||
CacheMu sync.Mutex
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoomAliasExists performs a request to '/room/{roomAlias}' on all known
|
// RoomAliasExists performs a request to '/room/{roomAlias}' on all known
|
||||||
|
@ -46,29 +43,20 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
request *api.RoomAliasExistsRequest,
|
request *api.RoomAliasExistsRequest,
|
||||||
response *api.RoomAliasExistsResponse,
|
response *api.RoomAliasExistsResponse,
|
||||||
) error {
|
) error {
|
||||||
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceRoomAlias")
|
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceRoomAlias")
|
||||||
defer trace.EndRegion()
|
defer span.Finish()
|
||||||
|
|
||||||
// Determine which application service should handle this request
|
// Determine which application service should handle this request
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
if appservice.URL != "" && appservice.IsInterestedInRoomAlias(request.Alias) {
|
||||||
path := api.ASRoomAliasExistsPath
|
|
||||||
if a.Cfg.LegacyPaths {
|
|
||||||
path = api.ASRoomAliasExistsLegacyPath
|
|
||||||
}
|
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
URL, err := url.Parse(appservice.RequestUrl() + path)
|
URL, err := url.Parse(appservice.URL + roomAliasExistsPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
URL.Path += request.Alias
|
URL.Path += request.Alias
|
||||||
if a.Cfg.LegacyAuth {
|
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
||||||
q := URL.Query()
|
|
||||||
q.Set("access_token", appservice.HSToken)
|
|
||||||
URL.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
apiURL := URL.String()
|
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the room, immediately return.
|
// created the room, immediately return.
|
||||||
|
@ -76,10 +64,9 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
|
||||||
req = req.WithContext(ctx)
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
resp, err := appservice.HTTPClient.Do(req)
|
resp, err := a.HTTPClient.Do(req)
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -123,28 +110,19 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
request *api.UserIDExistsRequest,
|
request *api.UserIDExistsRequest,
|
||||||
response *api.UserIDExistsResponse,
|
response *api.UserIDExistsResponse,
|
||||||
) error {
|
) error {
|
||||||
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceUserID")
|
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceUserID")
|
||||||
defer trace.EndRegion()
|
defer span.Finish()
|
||||||
|
|
||||||
// Determine which application service should handle this request
|
// Determine which application service should handle this request
|
||||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||||
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
||||||
// The full path to the rooms API, includes hs token
|
// The full path to the rooms API, includes hs token
|
||||||
path := api.ASUserExistsPath
|
URL, err := url.Parse(appservice.URL + userIDExistsPath)
|
||||||
if a.Cfg.LegacyPaths {
|
|
||||||
path = api.ASUserExistsLegacyPath
|
|
||||||
}
|
|
||||||
URL, err := url.Parse(appservice.RequestUrl() + path)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
URL.Path += request.UserID
|
URL.Path += request.UserID
|
||||||
if a.Cfg.LegacyAuth {
|
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
||||||
q := URL.Query()
|
|
||||||
q.Set("access_token", appservice.HSToken)
|
|
||||||
URL.RawQuery = q.Encode()
|
|
||||||
}
|
|
||||||
apiURL := URL.String()
|
|
||||||
|
|
||||||
// Send a request to each application service. If one responds that it has
|
// Send a request to each application service. If one responds that it has
|
||||||
// created the user, immediately return.
|
// created the user, immediately return.
|
||||||
|
@ -152,8 +130,7 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
resp, err := a.HTTPClient.Do(req.WithContext(ctx))
|
||||||
resp, err := appservice.HTTPClient.Do(req.WithContext(ctx))
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
defer func() {
|
defer func() {
|
||||||
err = resp.Body.Close()
|
err = resp.Body.Close()
|
||||||
|
@ -188,191 +165,3 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
||||||
response.UserIDExists = false
|
response.UserIDExists = false
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type thirdpartyResponses interface {
|
|
||||||
api.ASProtocolResponse | []api.ASUserResponse | []api.ASLocationResponse
|
|
||||||
}
|
|
||||||
|
|
||||||
func requestDo[T thirdpartyResponses](as *config.ApplicationService, url string, response *T) error {
|
|
||||||
req, err := http.NewRequest(http.MethodGet, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", as.HSToken))
|
|
||||||
resp, err := as.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() // nolint: errcheck
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(body, &response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AppServiceQueryAPI) Locations(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.LocationRequest,
|
|
||||||
resp *api.LocationResponse,
|
|
||||||
) error {
|
|
||||||
params, err := url.ParseQuery(req.Params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := api.ASLocationPath
|
|
||||||
if a.Cfg.LegacyPaths {
|
|
||||||
path = api.ASLocationLegacyPath
|
|
||||||
}
|
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
|
||||||
var asLocations []api.ASLocationResponse
|
|
||||||
if a.Cfg.LegacyAuth {
|
|
||||||
params.Set("access_token", as.HSToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := as.RequestUrl() + path
|
|
||||||
if req.Protocol != "" {
|
|
||||||
url += "/" + req.Protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := requestDo[[]api.ASLocationResponse](&as, url+"?"+params.Encode(), &asLocations); err != nil {
|
|
||||||
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'locations' from application service")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Locations = append(resp.Locations, asLocations...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Locations) == 0 {
|
|
||||||
resp.Exists = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp.Exists = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AppServiceQueryAPI) User(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.UserRequest,
|
|
||||||
resp *api.UserResponse,
|
|
||||||
) error {
|
|
||||||
params, err := url.ParseQuery(req.Params)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
path := api.ASUserPath
|
|
||||||
if a.Cfg.LegacyPaths {
|
|
||||||
path = api.ASUserLegacyPath
|
|
||||||
}
|
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
|
||||||
var asUsers []api.ASUserResponse
|
|
||||||
if a.Cfg.LegacyAuth {
|
|
||||||
params.Set("access_token", as.HSToken)
|
|
||||||
}
|
|
||||||
|
|
||||||
url := as.RequestUrl() + path
|
|
||||||
if req.Protocol != "" {
|
|
||||||
url += "/" + req.Protocol
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := requestDo[[]api.ASUserResponse](&as, url+"?"+params.Encode(), &asUsers); err != nil {
|
|
||||||
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'user' from application service")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Users = append(resp.Users, asUsers...)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(resp.Users) == 0 {
|
|
||||||
resp.Exists = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp.Exists = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AppServiceQueryAPI) Protocols(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.ProtocolRequest,
|
|
||||||
resp *api.ProtocolResponse,
|
|
||||||
) error {
|
|
||||||
protocolPath := api.ASProtocolPath
|
|
||||||
if a.Cfg.LegacyPaths {
|
|
||||||
protocolPath = api.ASProtocolLegacyPath
|
|
||||||
}
|
|
||||||
|
|
||||||
// get a single protocol response
|
|
||||||
if req.Protocol != "" {
|
|
||||||
|
|
||||||
a.CacheMu.Lock()
|
|
||||||
defer a.CacheMu.Unlock()
|
|
||||||
if proto, ok := a.ProtocolCache[req.Protocol]; ok {
|
|
||||||
resp.Exists = true
|
|
||||||
resp.Protocols = map[string]api.ASProtocolResponse{
|
|
||||||
req.Protocol: proto,
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
response := api.ASProtocolResponse{}
|
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
|
||||||
var proto api.ASProtocolResponse
|
|
||||||
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+req.Protocol, &proto); err != nil {
|
|
||||||
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Instances) != 0 {
|
|
||||||
response.Instances = append(response.Instances, proto.Instances...)
|
|
||||||
} else {
|
|
||||||
response = proto
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response.Instances) == 0 {
|
|
||||||
resp.Exists = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
resp.Exists = true
|
|
||||||
resp.Protocols = map[string]api.ASProtocolResponse{
|
|
||||||
req.Protocol: response,
|
|
||||||
}
|
|
||||||
a.ProtocolCache[req.Protocol] = response
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
response := make(map[string]api.ASProtocolResponse, len(a.Cfg.Derived.ApplicationServices))
|
|
||||||
|
|
||||||
for _, as := range a.Cfg.Derived.ApplicationServices {
|
|
||||||
for _, p := range as.Protocols {
|
|
||||||
var proto api.ASProtocolResponse
|
|
||||||
if err := requestDo[api.ASProtocolResponse](&as, as.RequestUrl()+protocolPath+p, &proto); err != nil {
|
|
||||||
log.WithError(err).WithField("application_service", as.ID).Error("unable to get 'protocol' from application service")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
existing, ok := response[p]
|
|
||||||
if !ok {
|
|
||||||
response[p] = proto
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
existing.Instances = append(existing.Instances, proto.Instances...)
|
|
||||||
response[p] = existing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(response) == 0 {
|
|
||||||
resp.Exists = false
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
a.CacheMu.Lock()
|
|
||||||
defer a.CacheMu.Unlock()
|
|
||||||
a.ProtocolCache = response
|
|
||||||
|
|
||||||
resp.Exists = true
|
|
||||||
resp.Protocols = response
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -643,7 +643,7 @@ fed Inbound federation redacts events from erased users
|
||||||
fme Outbound federation can request missing events
|
fme Outbound federation can request missing events
|
||||||
fme Inbound federation can return missing events for world_readable visibility
|
fme Inbound federation can return missing events for world_readable visibility
|
||||||
fme Inbound federation can return missing events for shared visibility
|
fme Inbound federation can return missing events for shared visibility
|
||||||
fme Inbound federation can return missing events for invited visibility
|
fme Inbound federation can return missing events for invite visibility
|
||||||
fme Inbound federation can return missing events for joined visibility
|
fme Inbound federation can return missing events for joined visibility
|
||||||
fme outliers whose auth_events are in a different room are correctly rejected
|
fme outliers whose auth_events are in a different room are correctly rejected
|
||||||
fbk Outbound federation can backfill events
|
fbk Outbound federation can backfill events
|
||||||
|
@ -937,19 +937,3 @@ fst Room state after a rejected state event is the same as before
|
||||||
fpb Federation publicRoom Name/topic keys are correct
|
fpb Federation publicRoom Name/topic keys are correct
|
||||||
fed New federated private chats get full presence information (SYN-115) (10 subtests)
|
fed New federated private chats get full presence information (SYN-115) (10 subtests)
|
||||||
dvk Rejects invalid device keys
|
dvk Rejects invalid device keys
|
||||||
rmv User can create and send/receive messages in a room with version 10
|
|
||||||
rmv local user can join room with version 10
|
|
||||||
rmv User can invite local user to room with version 10
|
|
||||||
rmv remote user can join room with version 10
|
|
||||||
rmv User can invite remote user to room with version 10
|
|
||||||
rmv Remote user can backfill in a room with version 10
|
|
||||||
rmv Can reject invites over federation for rooms with version 10
|
|
||||||
rmv Can receive redactions from regular users over federation in room version 10
|
|
||||||
rmv User can create and send/receive messages in a room with version 11
|
|
||||||
rmv local user can join room with version 11
|
|
||||||
rmv User can invite local user to room with version 11
|
|
||||||
rmv remote user can join room with version 11
|
|
||||||
rmv User can invite remote user to room with version 11
|
|
||||||
rmv Remote user can backfill in a room with version 11
|
|
||||||
rmv Can reject invites over federation for rooms with version 11
|
|
||||||
rmv Can receive redactions from regular users over federation in room version 11
|
|
51
build.cmd
Normal file
51
build.cmd
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
@echo off
|
||||||
|
|
||||||
|
:ENTRY_POINT
|
||||||
|
setlocal EnableDelayedExpansion
|
||||||
|
|
||||||
|
REM script base dir
|
||||||
|
set SCRIPTDIR=%~dp0
|
||||||
|
set PROJDIR=%SCRIPTDIR:~0,-1%
|
||||||
|
|
||||||
|
REM Put installed packages into ./bin
|
||||||
|
set GOBIN=%PROJDIR%\bin
|
||||||
|
|
||||||
|
set FLAGS=
|
||||||
|
|
||||||
|
REM Check if sources are under Git control
|
||||||
|
if not exist ".git" goto :CHECK_BIN
|
||||||
|
|
||||||
|
REM set BUILD=`git rev-parse --short HEAD \\ ""`
|
||||||
|
FOR /F "tokens=*" %%X IN ('git rev-parse --short HEAD') DO (
|
||||||
|
set BUILD=%%X
|
||||||
|
)
|
||||||
|
|
||||||
|
REM set BRANCH=`(git symbolic-ref --short HEAD \ tr -d \/ ) \\ ""`
|
||||||
|
FOR /F "tokens=*" %%X IN ('git symbolic-ref --short HEAD') DO (
|
||||||
|
set BRANCHRAW=%%X
|
||||||
|
set BRANCH=!BRANCHRAW:/=!
|
||||||
|
)
|
||||||
|
if "%BRANCH%" == "main" set BRANCH=
|
||||||
|
|
||||||
|
set FLAGS=-X github.com/matrix-org/dendrite/internal.branch=%BRANCH% -X github.com/matrix-org/dendrite/internal.build=%BUILD%
|
||||||
|
|
||||||
|
:CHECK_BIN
|
||||||
|
if exist "bin" goto :ALL_SET
|
||||||
|
mkdir "bin"
|
||||||
|
|
||||||
|
:ALL_SET
|
||||||
|
set CGO_ENABLED=1
|
||||||
|
for /D %%P in (cmd\*) do (
|
||||||
|
go build -trimpath -ldflags "%FLAGS%" -v -o ".\bin" ".\%%P"
|
||||||
|
)
|
||||||
|
|
||||||
|
set CGO_ENABLED=0
|
||||||
|
set GOOS=js
|
||||||
|
set GOARCH=wasm
|
||||||
|
go build -trimpath -ldflags "%FLAGS%" -o bin\main.wasm .\cmd\dendritejs-pinecone
|
||||||
|
|
||||||
|
goto :DONE
|
||||||
|
|
||||||
|
:DONE
|
||||||
|
echo Done
|
||||||
|
endlocal
|
24
build.sh
Executable file
24
build.sh
Executable file
|
@ -0,0 +1,24 @@
|
||||||
|
#!/bin/sh -eu
|
||||||
|
|
||||||
|
# Put installed packages into ./bin
|
||||||
|
export GOBIN=$PWD/`dirname $0`/bin
|
||||||
|
|
||||||
|
if [ -d ".git" ]
|
||||||
|
then
|
||||||
|
export BUILD=`git rev-parse --short HEAD || ""`
|
||||||
|
export BRANCH=`(git symbolic-ref --short HEAD | tr -d \/ ) || ""`
|
||||||
|
if [ "$BRANCH" = main ]
|
||||||
|
then
|
||||||
|
export BRANCH=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
export FLAGS="-X github.com/matrix-org/dendrite/internal.branch=$BRANCH -X github.com/matrix-org/dendrite/internal.build=$BUILD"
|
||||||
|
else
|
||||||
|
export FLAGS=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p bin
|
||||||
|
|
||||||
|
CGO_ENABLED=1 go build -trimpath -ldflags "$FLAGS" -v -o "bin/" ./cmd/...
|
||||||
|
|
||||||
|
# CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -trimpath -ldflags "$FLAGS" -o bin/main.wasm ./cmd/dendritejs-pinecone
|
|
@ -1,10 +1,4 @@
|
||||||
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
FROM docker.io/golang:1.18-alpine AS base
|
||||||
FROM docker.io/golang:1.21-alpine3.18 AS base
|
|
||||||
|
|
||||||
#
|
|
||||||
# Needs to be separate from the main Dockerfile for OpenShift,
|
|
||||||
# as --target is not supported there.
|
|
||||||
#
|
|
||||||
|
|
||||||
RUN apk --update --no-cache add bash build-base
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
@ -13,12 +7,12 @@ WORKDIR /build
|
||||||
COPY . /build
|
COPY . /build
|
||||||
|
|
||||||
RUN mkdir -p bin
|
RUN mkdir -p bin
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-yggdrasil
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-monolith-server
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Yggdrasil demo)"
|
LABEL org.opencontainers.image.title="Dendrite (Monolith)"
|
||||||
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||||
|
@ -28,4 +22,4 @@ COPY --from=base /build/bin/* /usr/bin/
|
||||||
VOLUME /etc/dendrite
|
VOLUME /etc/dendrite
|
||||||
WORKDIR /etc/dendrite
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-demo-yggdrasil"]
|
ENTRYPOINT ["/usr/bin/dendrite-monolith-server"]
|
|
@ -1,10 +1,4 @@
|
||||||
# Pinned to alpine3.18 until https://github.com/mattn/go-sqlite3/issues/1164 is solved
|
FROM docker.io/golang:1.18-alpine AS base
|
||||||
FROM docker.io/golang:1.21-alpine3.18 AS base
|
|
||||||
|
|
||||||
#
|
|
||||||
# Needs to be separate from the main Dockerfile for OpenShift,
|
|
||||||
# as --target is not supported there.
|
|
||||||
#
|
|
||||||
|
|
||||||
RUN apk --update --no-cache add bash build-base
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
@ -13,13 +7,12 @@ WORKDIR /build
|
||||||
COPY . /build
|
COPY . /build
|
||||||
|
|
||||||
RUN mkdir -p bin
|
RUN mkdir -p bin
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-pinecone
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-polylith-multi
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
FROM alpine:latest
|
FROM alpine:latest
|
||||||
RUN apk --update --no-cache add curl
|
LABEL org.opencontainers.image.title="Dendrite (Polylith)"
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Pinecone demo)"
|
|
||||||
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
LABEL org.opencontainers.image.description="Next-generation Matrix homeserver written in Go"
|
||||||
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
LABEL org.opencontainers.image.source="https://github.com/matrix-org/dendrite"
|
||||||
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
LABEL org.opencontainers.image.licenses="Apache-2.0"
|
||||||
|
@ -29,4 +22,4 @@ COPY --from=base /build/bin/* /usr/bin/
|
||||||
VOLUME /etc/dendrite
|
VOLUME /etc/dendrite
|
||||||
WORKDIR /etc/dendrite
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-demo-pinecone"]
|
ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"]
|
|
@ -5,21 +5,22 @@ These are Docker images for Dendrite!
|
||||||
They can be found on Docker Hub:
|
They can be found on Docker Hub:
|
||||||
|
|
||||||
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
- [matrixdotorg/dendrite-monolith](https://hub.docker.com/r/matrixdotorg/dendrite-monolith) for monolith deployments
|
||||||
|
- [matrixdotorg/dendrite-polylith](https://hub.docker.com/r/matrixdotorg/dendrite-polylith) for polylith deployments
|
||||||
|
|
||||||
## Dockerfile
|
## Dockerfiles
|
||||||
|
|
||||||
The `Dockerfile` is a multistage file which can build Dendrite. From the root of the Dendrite
|
The `Dockerfile` builds the base image which contains all of the Dendrite
|
||||||
repository, run:
|
components. The `Dockerfile.component` file takes the given component, as
|
||||||
|
specified with `--buildarg component=` from the base image and produce
|
||||||
|
smaller component-specific images, which are substantially smaller and do
|
||||||
|
not contain the Go toolchain etc.
|
||||||
|
|
||||||
```
|
## Compose files
|
||||||
docker build . -t matrixdotorg/dendrite-monolith
|
|
||||||
```
|
|
||||||
|
|
||||||
## Compose file
|
There are three sample `docker-compose` files:
|
||||||
|
|
||||||
There is one sample `docker-compose` files:
|
- `docker-compose.monolith.yml` which runs a monolith Dendrite deployment
|
||||||
|
- `docker-compose.polylith.yml` which runs a polylith Dendrite deployment
|
||||||
- `docker-compose.yml` which runs a Dendrite deployment with Postgres
|
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
|
@ -45,14 +46,24 @@ docker run --rm --entrypoint="" \
|
||||||
|
|
||||||
The key files will now exist in your current working directory, and can be mounted into place.
|
The key files will now exist in your current working directory, and can be mounted into place.
|
||||||
|
|
||||||
## Starting Dendrite
|
## Starting Dendrite as a monolith deployment
|
||||||
|
|
||||||
Create your config based on the [`dendrite-sample.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.yaml) sample configuration file.
|
Create your config based on the [`dendrite-sample.monolith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.monolith.yaml) sample configuration file.
|
||||||
|
|
||||||
Then start the deployment:
|
Then start the deployment:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.yml up
|
docker-compose -f docker-compose.monolith.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Starting Dendrite as a polylith deployment
|
||||||
|
|
||||||
|
Create your config based on the [`dendrite-sample.polylith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.polylith.yaml) sample configuration file.
|
||||||
|
|
||||||
|
Then start the deployment:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.polylith.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the images
|
## Building the images
|
||||||
|
|
44
build/docker/docker-compose.monolith.yml
Normal file
44
build/docker/docker-compose.monolith.yml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres:14
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./postgres/create_db.sh:/docker-entrypoint-initdb.d/20-create_db.sh
|
||||||
|
# To persist your PostgreSQL databases outside of the Docker image,
|
||||||
|
# to prevent data loss, modify the following ./path_to path:
|
||||||
|
- ./path_to/postgresql:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: itsasecret
|
||||||
|
POSTGRES_USER: dendrite
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
monolith:
|
||||||
|
hostname: monolith
|
||||||
|
image: matrixdotorg/dendrite-monolith:latest
|
||||||
|
command: [
|
||||||
|
"--tls-cert=server.crt",
|
||||||
|
"--tls-key=server.key"
|
||||||
|
]
|
||||||
|
ports:
|
||||||
|
- 8008:8008
|
||||||
|
- 8448:8448
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
- ./media:/var/dendrite/media
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
143
build/docker/docker-compose.polylith.yml
Normal file
143
build/docker/docker-compose.polylith.yml
Normal file
|
@ -0,0 +1,143 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres:14
|
||||||
|
restart: always
|
||||||
|
volumes:
|
||||||
|
- ./postgres/create_db.sh:/docker-entrypoint-initdb.d/20-create_db.sh
|
||||||
|
# To persist your PostgreSQL databases outside of the Docker image,
|
||||||
|
# to prevent data loss, modify the following ./path_to path:
|
||||||
|
- ./path_to/postgresql:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: itsasecret
|
||||||
|
POSTGRES_USER: dendrite
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
jetstream:
|
||||||
|
hostname: jetstream
|
||||||
|
image: nats:latest
|
||||||
|
command: |
|
||||||
|
--jetstream
|
||||||
|
--store_dir /var/lib/nats
|
||||||
|
--cluster_name Dendrite
|
||||||
|
volumes:
|
||||||
|
# To persist your NATS JetStream streams outside of the Docker image,
|
||||||
|
# prevent data loss, modify the following ./path_to path:
|
||||||
|
- ./path_to/nats:/var/lib/nats
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
client_api:
|
||||||
|
hostname: client_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: clientapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
media_api:
|
||||||
|
hostname: media_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: mediaapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
- ./media:/var/dendrite/media
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
sync_api:
|
||||||
|
hostname: sync_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: syncapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
room_server:
|
||||||
|
hostname: room_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: roomserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
federation_api:
|
||||||
|
hostname: federation_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: federationapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
key_server:
|
||||||
|
hostname: key_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: keyserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
user_api:
|
||||||
|
hostname: user_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: userapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
appservice_api:
|
||||||
|
hostname: appservice_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: appservice
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
depends_on:
|
||||||
|
- jetstream
|
||||||
|
- postgres
|
||||||
|
- room_server
|
||||||
|
- user_api
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
|
@ -1,52 +0,0 @@
|
||||||
version: "3.4"
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
hostname: postgres
|
|
||||||
image: postgres:15-alpine
|
|
||||||
restart: always
|
|
||||||
volumes:
|
|
||||||
# This will create a docker volume to persist the database files in.
|
|
||||||
# If you prefer those files to be outside of docker, you'll need to change this.
|
|
||||||
- dendrite_postgres_data:/var/lib/postgresql/data
|
|
||||||
environment:
|
|
||||||
POSTGRES_PASSWORD: itsasecret
|
|
||||||
POSTGRES_USER: dendrite
|
|
||||||
POSTGRES_DATABASE: dendrite
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U dendrite"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
|
|
||||||
monolith:
|
|
||||||
hostname: monolith
|
|
||||||
image: matrixdotorg/dendrite-monolith:latest
|
|
||||||
ports:
|
|
||||||
- 8008:8008
|
|
||||||
- 8448:8448
|
|
||||||
volumes:
|
|
||||||
- ./config:/etc/dendrite
|
|
||||||
# The following volumes use docker volumes, change this
|
|
||||||
# if you prefer to have those files outside of docker.
|
|
||||||
- dendrite_media:/var/dendrite/media
|
|
||||||
- dendrite_jetstream:/var/dendrite/jetstream
|
|
||||||
- dendrite_search_index:/var/dendrite/searchindex
|
|
||||||
depends_on:
|
|
||||||
postgres:
|
|
||||||
condition: service_healthy
|
|
||||||
networks:
|
|
||||||
- internal
|
|
||||||
restart: unless-stopped
|
|
||||||
|
|
||||||
networks:
|
|
||||||
internal:
|
|
||||||
attachable: true
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dendrite_postgres_data:
|
|
||||||
dendrite_media:
|
|
||||||
dendrite_jetstream:
|
|
||||||
dendrite_search_index:
|
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
cd $(git rev-parse --show-toplevel)
|
cd $(git rev-parse --show-toplevel)
|
||||||
|
|
||||||
|
@ -6,6 +6,5 @@ TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Building tag '${TAG}'"
|
echo "Building tag '${TAG}'"
|
||||||
|
|
||||||
docker build . --target monolith -t matrixdotorg/dendrite-monolith:${TAG}
|
docker build -t matrixdotorg/dendrite-monolith:${TAG} -f build/docker/Dockerfile.monolith .
|
||||||
docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone:${TAG}
|
docker build -t matrixdotorg/dendrite-polylith:${TAG} -f build/docker/Dockerfile.polylith .
|
||||||
docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil:${TAG}
|
|
|
@ -1,7 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
TAG=${1:-latest}
|
TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Pulling tag '${TAG}'"
|
echo "Pulling tag '${TAG}'"
|
||||||
|
|
||||||
docker pull matrixdotorg/dendrite-monolith:${TAG}
|
docker pull matrixdotorg/dendrite-monolith:${TAG}
|
||||||
|
docker pull matrixdotorg/dendrite-polylith:${TAG}
|
|
@ -1,7 +1,8 @@
|
||||||
#!/usr/bin/env bash
|
#!/bin/bash
|
||||||
|
|
||||||
TAG=${1:-latest}
|
TAG=${1:-latest}
|
||||||
|
|
||||||
echo "Pushing tag '${TAG}'"
|
echo "Pushing tag '${TAG}'"
|
||||||
|
|
||||||
docker push matrixdotorg/dendrite-monolith:${TAG}
|
docker push matrixdotorg/dendrite-monolith:${TAG}
|
||||||
|
docker push matrixdotorg/dendrite-polylith:${TAG}
|
5
build/docker/postgres/create_db.sh
Executable file
5
build/docker/postgres/create_db.sh
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
for db in userapi_accounts mediaapi syncapi roomserver keyserver federationapi appservice mscs; do
|
||||||
|
createdb -U dendrite -O dendrite dendrite_$db
|
||||||
|
done
|
2
build/gobind-pinecone/build.sh
Executable file → Normal file
2
build/gobind-pinecone/build.sh
Executable file → Normal file
|
@ -7,7 +7,7 @@ do
|
||||||
case "$option"
|
case "$option"
|
||||||
in
|
in
|
||||||
a) gomobile bind -v -target android -trimpath -ldflags="-s -w" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
a) gomobile bind -v -target android -trimpath -ldflags="-s -w" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
||||||
i) gomobile bind -v -target ios -trimpath -ldflags="" -o ~/DendriteBindings/Gobind.xcframework . ;;
|
i) gomobile bind -v -target ios -trimpath -ldflags="" github.com/matrix-org/dendrite/build/gobind-pinecone ;;
|
||||||
*) echo "No target specified, specify -a or -i"; exit 1 ;;
|
*) echo "No target specified, specify -a or -i"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
|
@ -18,29 +18,46 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conduit"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conn"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/monolith"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/users"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/keyserver"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
|
"github.com/matrix-org/dendrite/setup"
|
||||||
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"github.com/matrix-org/dendrite/test"
|
||||||
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/pinecone/types"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"golang.org/x/net/http2"
|
||||||
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
|
pineconeConnections "github.com/matrix-org/pinecone/connections"
|
||||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||||
pineconeRouter "github.com/matrix-org/pinecone/router"
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
|
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
||||||
|
"github.com/matrix-org/pinecone/types"
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
)
|
)
|
||||||
|
@ -49,250 +66,109 @@ const (
|
||||||
PeerTypeRemote = pineconeRouter.PeerTypeRemote
|
PeerTypeRemote = pineconeRouter.PeerTypeRemote
|
||||||
PeerTypeMulticast = pineconeRouter.PeerTypeMulticast
|
PeerTypeMulticast = pineconeRouter.PeerTypeMulticast
|
||||||
PeerTypeBluetooth = pineconeRouter.PeerTypeBluetooth
|
PeerTypeBluetooth = pineconeRouter.PeerTypeBluetooth
|
||||||
PeerTypeBonjour = pineconeRouter.PeerTypeBonjour
|
|
||||||
|
|
||||||
MaxFrameSize = types.MaxFrameSize
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Re-export Conduit in this package for bindings.
|
|
||||||
type Conduit struct {
|
|
||||||
conduit.Conduit
|
|
||||||
}
|
|
||||||
|
|
||||||
type DendriteMonolith struct {
|
type DendriteMonolith struct {
|
||||||
logger logrus.Logger
|
logger logrus.Logger
|
||||||
p2pMonolith monolith.P2PMonolith
|
PineconeRouter *pineconeRouter.Router
|
||||||
StorageDirectory string
|
PineconeMulticast *pineconeMulticast.Multicast
|
||||||
CacheDirectory string
|
PineconeQUIC *pineconeSessions.Sessions
|
||||||
listener net.Listener
|
PineconeManager *pineconeConnections.ConnectionManager
|
||||||
}
|
StorageDirectory string
|
||||||
|
CacheDirectory string
|
||||||
func (m *DendriteMonolith) PublicKey() string {
|
listener net.Listener
|
||||||
return m.p2pMonolith.Router.PublicKey().String()
|
httpServer *http.Server
|
||||||
|
processContext *process.ProcessContext
|
||||||
|
userAPI userapiAPI.UserInternalAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) BaseURL() string {
|
func (m *DendriteMonolith) BaseURL() string {
|
||||||
return fmt.Sprintf("http://%s", m.p2pMonolith.Addr())
|
return fmt.Sprintf("http://%s", m.listener.Addr().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) PeerCount(peertype int) int {
|
func (m *DendriteMonolith) PeerCount(peertype int) int {
|
||||||
return m.p2pMonolith.Router.PeerCount(peertype)
|
return m.PineconeRouter.PeerCount(peertype)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SessionCount() int {
|
func (m *DendriteMonolith) SessionCount() int {
|
||||||
return len(m.p2pMonolith.Sessions.Protocol(monolith.SessionProtocol).Sessions())
|
return len(m.PineconeQUIC.Protocol("matrix").Sessions())
|
||||||
}
|
|
||||||
|
|
||||||
type InterfaceInfo struct {
|
|
||||||
Name string
|
|
||||||
Index int
|
|
||||||
Mtu int
|
|
||||||
Up bool
|
|
||||||
Broadcast bool
|
|
||||||
Loopback bool
|
|
||||||
PointToPoint bool
|
|
||||||
Multicast bool
|
|
||||||
Addrs string
|
|
||||||
}
|
|
||||||
|
|
||||||
type InterfaceRetriever interface {
|
|
||||||
CacheCurrentInterfaces() int
|
|
||||||
GetCachedInterface(index int) *InterfaceInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) RegisterNetworkCallback(intfCallback InterfaceRetriever) {
|
|
||||||
callback := func() []pineconeMulticast.InterfaceInfo {
|
|
||||||
count := intfCallback.CacheCurrentInterfaces()
|
|
||||||
intfs := []pineconeMulticast.InterfaceInfo{}
|
|
||||||
for i := 0; i < count; i++ {
|
|
||||||
iface := intfCallback.GetCachedInterface(i)
|
|
||||||
if iface != nil {
|
|
||||||
intfs = append(intfs, pineconeMulticast.InterfaceInfo{
|
|
||||||
Name: iface.Name,
|
|
||||||
Index: iface.Index,
|
|
||||||
Mtu: iface.Mtu,
|
|
||||||
Up: iface.Up,
|
|
||||||
Broadcast: iface.Broadcast,
|
|
||||||
Loopback: iface.Loopback,
|
|
||||||
PointToPoint: iface.PointToPoint,
|
|
||||||
Multicast: iface.Multicast,
|
|
||||||
Addrs: iface.Addrs,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return intfs
|
|
||||||
}
|
|
||||||
m.p2pMonolith.Multicast.RegisterNetworkCallback(callback)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
||||||
if enabled {
|
if enabled {
|
||||||
m.p2pMonolith.Multicast.Start()
|
m.PineconeMulticast.Start()
|
||||||
} else {
|
} else {
|
||||||
m.p2pMonolith.Multicast.Stop()
|
m.PineconeMulticast.Stop()
|
||||||
m.DisconnectType(int(pineconeRouter.PeerTypeMulticast))
|
m.DisconnectType(int(pineconeRouter.PeerTypeMulticast))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||||
m.p2pMonolith.ConnManager.RemovePeers()
|
m.PineconeManager.RemovePeers()
|
||||||
for _, uri := range strings.Split(uri, ",") {
|
m.PineconeManager.AddPeer(strings.TrimSpace(uri))
|
||||||
m.p2pMonolith.ConnManager.AddPeer(strings.TrimSpace(uri))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getServerKeyFromString(nodeID string) (spec.ServerName, error) {
|
|
||||||
var nodeKey spec.ServerName
|
|
||||||
if userID, err := spec.NewUserID(nodeID, false); err == nil {
|
|
||||||
hexKey, decodeErr := hex.DecodeString(string(userID.Domain()))
|
|
||||||
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
|
||||||
return "", fmt.Errorf("UserID domain is not a valid ed25519 public key: %v", userID.Domain())
|
|
||||||
} else {
|
|
||||||
nodeKey = userID.Domain()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hexKey, decodeErr := hex.DecodeString(nodeID)
|
|
||||||
if decodeErr != nil || len(hexKey) != ed25519.PublicKeySize {
|
|
||||||
return "", fmt.Errorf("Relay server uri is not a valid ed25519 public key: %v", nodeID)
|
|
||||||
} else {
|
|
||||||
nodeKey = spec.ServerName(nodeID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nodeKey, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetRelayServers(nodeID string, uris string) {
|
|
||||||
relays := []spec.ServerName{}
|
|
||||||
for _, uri := range strings.Split(uris, ",") {
|
|
||||||
uri = strings.TrimSpace(uri)
|
|
||||||
if len(uri) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeKey, err := getServerKeyFromString(uri)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf(err.Error())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
relays = append(relays, nodeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
nodeKey, err := getServerKeyFromString(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf(err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if string(nodeKey) == m.PublicKey() {
|
|
||||||
logrus.Infof("Setting own relay servers to: %v", relays)
|
|
||||||
m.p2pMonolith.RelayRetriever.SetRelayServers(relays)
|
|
||||||
} else {
|
|
||||||
relay.UpdateNodeRelayServers(
|
|
||||||
spec.ServerName(nodeKey),
|
|
||||||
relays,
|
|
||||||
m.p2pMonolith.ProcessCtx.Context(),
|
|
||||||
m.p2pMonolith.GetFederationAPI(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) GetRelayServers(nodeID string) string {
|
|
||||||
nodeKey, err := getServerKeyFromString(nodeID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf(err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
relaysString := ""
|
|
||||||
if string(nodeKey) == m.PublicKey() {
|
|
||||||
relays := m.p2pMonolith.RelayRetriever.GetRelayServers()
|
|
||||||
|
|
||||||
for i, relay := range relays {
|
|
||||||
if i != 0 {
|
|
||||||
// Append a comma to the previous entry if there is one.
|
|
||||||
relaysString += ","
|
|
||||||
}
|
|
||||||
relaysString += string(relay)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
request := api.P2PQueryRelayServersRequest{Server: spec.ServerName(nodeKey)}
|
|
||||||
response := api.P2PQueryRelayServersResponse{}
|
|
||||||
err := m.p2pMonolith.GetFederationAPI().P2PQueryRelayServers(m.p2pMonolith.ProcessCtx.Context(), &request, &response)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warnf("Failed obtaining list of this node's relay servers: %s", err.Error())
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, relay := range response.RelayServers {
|
|
||||||
if i != 0 {
|
|
||||||
// Append a comma to the previous entry if there is one.
|
|
||||||
relaysString += ","
|
|
||||||
}
|
|
||||||
relaysString += string(relay)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return relaysString
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) RelayingEnabled() bool {
|
|
||||||
return m.p2pMonolith.GetRelayAPI().RelayingEnabled()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetRelayingEnabled(enabled bool) {
|
|
||||||
m.p2pMonolith.GetRelayAPI().SetRelayingEnabled(enabled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectType(peertype int) {
|
func (m *DendriteMonolith) DisconnectType(peertype int) {
|
||||||
for _, p := range m.p2pMonolith.Router.Peers() {
|
for _, p := range m.PineconeRouter.Peers() {
|
||||||
if int(peertype) == p.PeerType {
|
if int(peertype) == p.PeerType {
|
||||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(p.Port), nil)
|
m.PineconeRouter.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectZone(zone string) {
|
func (m *DendriteMonolith) DisconnectZone(zone string) {
|
||||||
for _, p := range m.p2pMonolith.Router.Peers() {
|
for _, p := range m.PineconeRouter.Peers() {
|
||||||
if zone == p.Zone {
|
if zone == p.Zone {
|
||||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(p.Port), nil)
|
m.PineconeRouter.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) DisconnectPort(port int) {
|
func (m *DendriteMonolith) DisconnectPort(port int) {
|
||||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(port), nil)
|
m.PineconeRouter.Disconnect(types.SwitchPortID(port), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Conduit(zone string, peertype int) (*Conduit, error) {
|
func (m *DendriteMonolith) Conduit(zone string, peertype int) (*Conduit, error) {
|
||||||
l, r := net.Pipe()
|
l, r := net.Pipe()
|
||||||
newConduit := Conduit{conduit.NewConduit(r, 0)}
|
conduit := &Conduit{conn: r, port: 0}
|
||||||
go func() {
|
go func() {
|
||||||
logrus.Errorf("Attempting authenticated connect")
|
conduit.portMutex.Lock()
|
||||||
var port types.SwitchPortID
|
defer conduit.portMutex.Unlock()
|
||||||
var err error
|
loop:
|
||||||
if port, err = m.p2pMonolith.Router.Connect(
|
for i := 1; i <= 10; i++ {
|
||||||
l,
|
logrus.Errorf("Attempting authenticated connect (attempt %d)", i)
|
||||||
pineconeRouter.ConnectionZone(zone),
|
var err error
|
||||||
pineconeRouter.ConnectionPeerType(peertype),
|
conduit.port, err = m.PineconeRouter.Connect(
|
||||||
); err != nil {
|
l,
|
||||||
logrus.Errorf("Authenticated connect failed: %s", err)
|
pineconeRouter.ConnectionZone(zone),
|
||||||
_ = l.Close()
|
pineconeRouter.ConnectionPeerType(peertype),
|
||||||
_ = r.Close()
|
)
|
||||||
_ = newConduit.Close()
|
switch err {
|
||||||
return
|
case io.ErrClosedPipe:
|
||||||
|
logrus.Errorf("Authenticated connect failed due to closed pipe (attempt %d)", i)
|
||||||
|
return
|
||||||
|
case io.EOF:
|
||||||
|
logrus.Errorf("Authenticated connect failed due to EOF (attempt %d)", i)
|
||||||
|
break loop
|
||||||
|
case nil:
|
||||||
|
logrus.Errorf("Authenticated connect succeeded, connected to port %d (attempt %d)", conduit.port, i)
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
logrus.WithError(err).Errorf("Authenticated connect failed (attempt %d)", i)
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
newConduit.SetPort(port)
|
_ = l.Close()
|
||||||
logrus.Infof("Authenticated connect succeeded (port %d)", newConduit.Port())
|
_ = r.Close()
|
||||||
}()
|
}()
|
||||||
return &newConduit, nil
|
return conduit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, error) {
|
func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, error) {
|
||||||
pubkey := m.p2pMonolith.Router.PublicKey()
|
pubkey := m.PineconeRouter.PublicKey()
|
||||||
userID := userutil.MakeUserID(
|
userID := userutil.MakeUserID(
|
||||||
localpart,
|
localpart,
|
||||||
spec.ServerName(hex.EncodeToString(pubkey[:])),
|
gomatrixserverlib.ServerName(hex.EncodeToString(pubkey[:])),
|
||||||
)
|
)
|
||||||
userReq := &userapiAPI.PerformAccountCreationRequest{
|
userReq := &userapiAPI.PerformAccountCreationRequest{
|
||||||
AccountType: userapiAPI.AccountTypeUser,
|
AccountType: userapiAPI.AccountTypeUser,
|
||||||
|
@ -300,7 +176,7 @@ func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, err
|
||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
userRes := &userapiAPI.PerformAccountCreationResponse{}
|
userRes := &userapiAPI.PerformAccountCreationResponse{}
|
||||||
if err := m.p2pMonolith.GetUserAPI().PerformAccountCreation(context.Background(), userReq, userRes); err != nil {
|
if err := m.userAPI.PerformAccountCreation(context.Background(), userReq, userRes); err != nil {
|
||||||
return userID, fmt.Errorf("userAPI.PerformAccountCreation: %w", err)
|
return userID, fmt.Errorf("userAPI.PerformAccountCreation: %w", err)
|
||||||
}
|
}
|
||||||
return userID, nil
|
return userID, nil
|
||||||
|
@ -318,7 +194,7 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
AccessToken: hex.EncodeToString(accessTokenBytes[:n]),
|
AccessToken: hex.EncodeToString(accessTokenBytes[:n]),
|
||||||
}
|
}
|
||||||
loginRes := &userapiAPI.PerformDeviceCreationResponse{}
|
loginRes := &userapiAPI.PerformDeviceCreationResponse{}
|
||||||
if err := m.p2pMonolith.GetUserAPI().PerformDeviceCreation(context.Background(), loginReq, loginRes); err != nil {
|
if err := m.userAPI.PerformDeviceCreation(context.Background(), loginReq, loginRes); err != nil {
|
||||||
return "", fmt.Errorf("userAPI.PerformDeviceCreation: %w", err)
|
return "", fmt.Errorf("userAPI.PerformDeviceCreation: %w", err)
|
||||||
}
|
}
|
||||||
if !loginRes.DeviceCreated {
|
if !loginRes.DeviceCreated {
|
||||||
|
@ -327,10 +203,51 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
return loginRes.Device.AccessToken, nil
|
return loginRes.Device.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
func (m *DendriteMonolith) Start() {
|
func (m *DendriteMonolith) Start() {
|
||||||
|
var sk ed25519.PrivateKey
|
||||||
|
var pk ed25519.PublicKey
|
||||||
|
|
||||||
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
||||||
oldKeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
if _, err := os.Stat(keyfile); os.IsNotExist(err) {
|
||||||
sk, pk := monolith.GetOrCreateKey(keyfile, oldKeyfile)
|
oldkeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
||||||
|
if _, err = os.Stat(oldkeyfile); os.IsNotExist(err) {
|
||||||
|
if err = test.NewMatrixKey(keyfile); err != nil {
|
||||||
|
panic("failed to generate a new PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if _, sk, err = config.LoadMatrixKey(keyfile, os.ReadFile); err != nil {
|
||||||
|
panic("failed to load PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if sk, err = os.ReadFile(oldkeyfile); err != nil {
|
||||||
|
panic("failed to read the old private key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
if err = test.SaveMatrixKey(keyfile, sk); err != nil {
|
||||||
|
panic("failed to convert the private key to PEM format: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, sk, err = config.LoadMatrixKey(keyfile, os.ReadFile); err != nil {
|
||||||
|
panic("failed to load PEM key: " + err.Error())
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pk = sk.Public().(ed25519.PublicKey)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
m.listener, err = net.Listen("tcp", "localhost:65432")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
m.logger = logrus.Logger{
|
m.logger = logrus.Logger{
|
||||||
Out: BindLogger{},
|
Out: BindLogger{},
|
||||||
|
@ -338,29 +255,174 @@ func (m *DendriteMonolith) Start() {
|
||||||
m.logger.SetOutput(BindLogger{})
|
m.logger.SetOutput(BindLogger{})
|
||||||
logrus.SetOutput(BindLogger{})
|
logrus.SetOutput(BindLogger{})
|
||||||
|
|
||||||
m.p2pMonolith = monolith.P2PMonolith{}
|
m.PineconeRouter = pineconeRouter.NewRouter(logrus.WithField("pinecone", "router"), sk, false)
|
||||||
m.p2pMonolith.SetupPinecone(sk)
|
m.PineconeQUIC = pineconeSessions.NewSessions(logrus.WithField("pinecone", "sessions"), m.PineconeRouter, []string{"matrix"})
|
||||||
|
m.PineconeMulticast = pineconeMulticast.NewMulticast(logrus.WithField("pinecone", "multicast"), m.PineconeRouter)
|
||||||
|
m.PineconeManager = pineconeConnections.NewConnectionManager(m.PineconeRouter, nil)
|
||||||
|
|
||||||
prefix := hex.EncodeToString(pk)
|
prefix := hex.EncodeToString(pk)
|
||||||
cfg := monolith.GenerateDefaultConfig(sk, m.StorageDirectory, m.CacheDirectory, prefix)
|
cfg := &config.Dendrite{}
|
||||||
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
cfg.Defaults(config.DefaultOpts{
|
||||||
|
Generate: true,
|
||||||
|
Monolithic: true,
|
||||||
|
})
|
||||||
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
||||||
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.InMemory = false
|
cfg.Global.JetStream.InMemory = true
|
||||||
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/%s", m.StorageDirectory, prefix))
|
||||||
// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
|
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-account.db", m.StorageDirectory, prefix))
|
||||||
cfg.SyncAPI.Fulltext.Enabled = false
|
cfg.MediaAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-mediaapi.db", m.StorageDirectory))
|
||||||
|
cfg.SyncAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-syncapi.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-roomserver.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-keyserver.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-federationsender.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory))
|
||||||
|
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory))
|
||||||
|
cfg.MSCs.MSCs = []string{"msc2836", "msc2946"}
|
||||||
|
cfg.ClientAPI.RegistrationDisabled = false
|
||||||
|
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
||||||
|
if err = cfg.Derive(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
processCtx := process.NewProcessContext()
|
base := base.NewBaseDendrite(cfg, "Monolith")
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
defer base.Close() // nolint: errcheck
|
||||||
routers := httputil.NewRouters()
|
|
||||||
|
|
||||||
enableRelaying := false
|
federation := conn.CreateFederationClient(base, m.PineconeQUIC)
|
||||||
enableMetrics := false
|
|
||||||
enableWebsockets := false
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
m.p2pMonolith.StartMonolith()
|
|
||||||
|
rsAPI := roomserver.NewInternalAPI(base)
|
||||||
|
|
||||||
|
fsAPI := federationapi.NewInternalAPI(
|
||||||
|
base, federation, rsAPI, base.Caches, keyRing, true,
|
||||||
|
)
|
||||||
|
|
||||||
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
|
m.userAPI = userapi.NewInternalAPI(base, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient())
|
||||||
|
keyAPI.SetUserAPI(m.userAPI)
|
||||||
|
|
||||||
|
asAPI := appservice.NewInternalAPI(base, m.userAPI, rsAPI)
|
||||||
|
|
||||||
|
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||||
|
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
||||||
|
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
||||||
|
|
||||||
|
userProvider := users.NewPineconeUserProvider(m.PineconeRouter, m.PineconeQUIC, m.userAPI, federation)
|
||||||
|
roomProvider := rooms.NewPineconeRoomProvider(m.PineconeRouter, m.PineconeQUIC, fsAPI, federation)
|
||||||
|
|
||||||
|
monolith := setup.Monolith{
|
||||||
|
Config: base.Cfg,
|
||||||
|
Client: conn.CreateClient(base, m.PineconeQUIC),
|
||||||
|
FedClient: federation,
|
||||||
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
AppserviceAPI: asAPI,
|
||||||
|
FederationAPI: fsAPI,
|
||||||
|
RoomserverAPI: rsAPI,
|
||||||
|
UserAPI: m.userAPI,
|
||||||
|
KeyAPI: keyAPI,
|
||||||
|
ExtPublicRoomsProvider: roomProvider,
|
||||||
|
ExtUserDirectoryProvider: userProvider,
|
||||||
|
}
|
||||||
|
monolith.AddAllPublicRoutes(base)
|
||||||
|
|
||||||
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
|
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
||||||
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
||||||
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||||
|
httpRouter.HandleFunc("/pinecone", m.PineconeRouter.ManholeHandler)
|
||||||
|
|
||||||
|
pMux := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
|
pMux.PathPrefix(users.PublicURL).HandlerFunc(userProvider.FederatedUserProfiles)
|
||||||
|
pMux.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
||||||
|
pMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||||
|
|
||||||
|
pHTTP := m.PineconeQUIC.Protocol("matrix").HTTP()
|
||||||
|
pHTTP.Mux().Handle(users.PublicURL, pMux)
|
||||||
|
pHTTP.Mux().Handle(httputil.PublicFederationPathPrefix, pMux)
|
||||||
|
pHTTP.Mux().Handle(httputil.PublicMediaPathPrefix, pMux)
|
||||||
|
|
||||||
|
// Build both ends of a HTTP multiplex.
|
||||||
|
h2s := &http2.Server{}
|
||||||
|
m.httpServer = &http.Server{
|
||||||
|
Addr: ":0",
|
||||||
|
TLSNextProto: map[string]func(*http.Server, *tls.Conn, http.Handler){},
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 30 * time.Second,
|
||||||
|
BaseContext: func(_ net.Listener) context.Context {
|
||||||
|
return context.Background()
|
||||||
|
},
|
||||||
|
Handler: h2c.NewHandler(pMux, h2s),
|
||||||
|
}
|
||||||
|
|
||||||
|
m.processContext = base.ProcessContext
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
m.logger.Info("Listening on ", cfg.Global.ServerName)
|
||||||
|
|
||||||
|
switch m.httpServer.Serve(m.PineconeQUIC.Protocol("matrix")) {
|
||||||
|
case net.ErrClosed, http.ErrServerClosed:
|
||||||
|
m.logger.Info("Stopped listening on ", cfg.Global.ServerName)
|
||||||
|
default:
|
||||||
|
m.logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
logrus.Info("Listening on ", m.listener.Addr())
|
||||||
|
|
||||||
|
switch http.Serve(m.listener, httpRouter) {
|
||||||
|
case net.ErrClosed, http.ErrServerClosed:
|
||||||
|
m.logger.Info("Stopped listening on ", cfg.Global.ServerName)
|
||||||
|
default:
|
||||||
|
m.logger.Fatal(err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Stop() {
|
func (m *DendriteMonolith) Stop() {
|
||||||
m.p2pMonolith.Stop()
|
m.processContext.ShutdownDendrite()
|
||||||
|
_ = m.listener.Close()
|
||||||
|
m.PineconeMulticast.Stop()
|
||||||
|
_ = m.PineconeQUIC.Close()
|
||||||
|
_ = m.PineconeRouter.Close()
|
||||||
|
m.processContext.WaitForComponentsToFinish()
|
||||||
|
}
|
||||||
|
|
||||||
|
const MaxFrameSize = types.MaxFrameSize
|
||||||
|
|
||||||
|
type Conduit struct {
|
||||||
|
conn net.Conn
|
||||||
|
port types.SwitchPortID
|
||||||
|
portMutex sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conduit) Port() int {
|
||||||
|
c.portMutex.Lock()
|
||||||
|
defer c.portMutex.Unlock()
|
||||||
|
return int(c.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conduit) Read(b []byte) (int, error) {
|
||||||
|
return c.conn.Read(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conduit) ReadCopy() ([]byte, error) {
|
||||||
|
var buf [65535 * 2]byte
|
||||||
|
n, err := c.conn.Read(buf[:])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf[:n], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conduit) Write(b []byte) (int, error) {
|
||||||
|
return c.conn.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Conduit) Close() error {
|
||||||
|
return c.conn.Close()
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,158 +0,0 @@
|
||||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package gobind
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestMonolithStarts(t *testing.T) {
|
|
||||||
monolith := DendriteMonolith{
|
|
||||||
StorageDirectory: t.TempDir(),
|
|
||||||
CacheDirectory: t.TempDir(),
|
|
||||||
}
|
|
||||||
monolith.Start()
|
|
||||||
monolith.PublicKey()
|
|
||||||
monolith.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMonolithSetRelayServers(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
nodeID string
|
|
||||||
relays string
|
|
||||||
expectedRelays string
|
|
||||||
expectSelf bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "assorted valid, invalid, empty & self keys",
|
|
||||||
nodeID: "@valid:abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
|
||||||
expectedRelays: "123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
expectSelf: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid node key",
|
|
||||||
nodeID: "@invalid:notakey",
|
|
||||||
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
|
||||||
expectedRelays: "",
|
|
||||||
expectSelf: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "node is self",
|
|
||||||
nodeID: "self",
|
|
||||||
relays: "@valid:123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd,@invalid:notakey,,",
|
|
||||||
expectedRelays: "123456123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
expectSelf: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
monolith := DendriteMonolith{
|
|
||||||
StorageDirectory: t.TempDir(),
|
|
||||||
CacheDirectory: t.TempDir(),
|
|
||||||
}
|
|
||||||
monolith.Start()
|
|
||||||
|
|
||||||
inputRelays := tc.relays
|
|
||||||
expectedRelays := tc.expectedRelays
|
|
||||||
if tc.expectSelf {
|
|
||||||
inputRelays += "," + monolith.PublicKey()
|
|
||||||
expectedRelays += "," + monolith.PublicKey()
|
|
||||||
}
|
|
||||||
nodeID := tc.nodeID
|
|
||||||
if nodeID == "self" {
|
|
||||||
nodeID = monolith.PublicKey()
|
|
||||||
}
|
|
||||||
|
|
||||||
monolith.SetRelayServers(nodeID, inputRelays)
|
|
||||||
relays := monolith.GetRelayServers(nodeID)
|
|
||||||
monolith.Stop()
|
|
||||||
|
|
||||||
if !containSameKeys(strings.Split(relays, ","), strings.Split(expectedRelays, ",")) {
|
|
||||||
t.Fatalf("%s: expected %s got %s", tc.name, expectedRelays, relays)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func containSameKeys(expected []string, actual []string) bool {
|
|
||||||
if len(expected) != len(actual) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, expectedKey := range expected {
|
|
||||||
hasMatch := false
|
|
||||||
for _, actualKey := range actual {
|
|
||||||
if actualKey == expectedKey {
|
|
||||||
hasMatch = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !hasMatch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestParseServerKey(t *testing.T) {
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
serverKey string
|
|
||||||
expectedErr bool
|
|
||||||
expectedKey spec.ServerName
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "valid userid as key",
|
|
||||||
serverKey: "@valid:abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
expectedErr: false,
|
|
||||||
expectedKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "valid key",
|
|
||||||
serverKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
expectedErr: false,
|
|
||||||
expectedKey: "abcdef123456abcdef123456abcdef123456abcdef123456abcdef123456abcd",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid userid key",
|
|
||||||
serverKey: "@invalid:notakey",
|
|
||||||
expectedErr: true,
|
|
||||||
expectedKey: "",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid key",
|
|
||||||
serverKey: "@invalid:notakey",
|
|
||||||
expectedErr: true,
|
|
||||||
expectedKey: "",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
key, err := getServerKeyFromString(tc.serverKey)
|
|
||||||
if tc.expectedErr && err == nil {
|
|
||||||
t.Fatalf("%s: expected an error", tc.name)
|
|
||||||
} else if !tc.expectedErr && err != nil {
|
|
||||||
t.Fatalf("%s: didn't expect an error: %s", tc.name, err.Error())
|
|
||||||
}
|
|
||||||
if tc.expectedKey != key {
|
|
||||||
t.Fatalf("%s: keys not equal. expected: %s got: %s", tc.name, tc.expectedKey, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -12,7 +12,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
|
@ -20,20 +19,16 @@ import (
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
||||||
"github.com/matrix-org/dendrite/federationapi"
|
"github.com/matrix-org/dendrite/federationapi"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/keyserver"
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
"github.com/matrix-org/dendrite/roomserver"
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
basepkg "github.com/matrix-org/dendrite/setup/base"
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/test"
|
"github.com/matrix-org/dendrite/test"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
|
@ -132,10 +127,10 @@ func (m *DendriteMonolith) Start() {
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults(config.DefaultOpts{
|
cfg.Defaults(config.DefaultOpts{
|
||||||
Generate: true,
|
Generate: true,
|
||||||
SingleDatabase: true,
|
Monolithic: true,
|
||||||
})
|
})
|
||||||
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
||||||
cfg.Global.PrivateKey = sk
|
cfg.Global.PrivateKey = sk
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
||||||
|
@ -154,71 +149,26 @@ func (m *DendriteMonolith) Start() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configErrors := &config.ConfigErrors{}
|
base := base.NewBaseDendrite(cfg, "Monolith")
|
||||||
cfg.Verify(configErrors)
|
m.processContext = base.ProcessContext
|
||||||
if len(*configErrors) > 0 {
|
defer base.Close() // nolint: errcheck
|
||||||
for _, err := range *configErrors {
|
|
||||||
logrus.Errorf("Configuration error: %s", err)
|
|
||||||
}
|
|
||||||
logrus.Fatalf("Failed to start due to configuration errors")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal.SetupStdLogging()
|
federation := ygg.CreateFederationClient(base)
|
||||||
internal.SetupHookLogging(cfg.Logging)
|
|
||||||
internal.SetupPprof()
|
|
||||||
|
|
||||||
logrus.Infof("Dendrite version %s", internal.VersionString())
|
|
||||||
|
|
||||||
if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
|
|
||||||
logrus.Warn("Open registration is enabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
closer, err := cfg.SetupTracing()
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Panicf("failed to start opentracing")
|
|
||||||
}
|
|
||||||
defer closer.Close()
|
|
||||||
|
|
||||||
if cfg.Global.Sentry.Enabled {
|
|
||||||
logrus.Info("Setting up Sentry for debugging...")
|
|
||||||
err = sentry.Init(sentry.ClientOptions{
|
|
||||||
Dsn: cfg.Global.Sentry.DSN,
|
|
||||||
Environment: cfg.Global.Sentry.Environment,
|
|
||||||
Debug: true,
|
|
||||||
ServerName: string(cfg.Global.ServerName),
|
|
||||||
Release: "dendrite@" + internal.VersionString(),
|
|
||||||
AttachStacktrace: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Panic("failed to start Sentry")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
processCtx := process.NewProcessContext()
|
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
|
||||||
routers := httputil.NewRouters()
|
|
||||||
basepkg.ConfigureAdminEndpoints(processCtx, routers)
|
|
||||||
m.processContext = processCtx
|
|
||||||
defer func() {
|
|
||||||
processCtx.ShutdownDendrite()
|
|
||||||
processCtx.WaitForShutdown()
|
|
||||||
}() // nolint: errcheck
|
|
||||||
|
|
||||||
federation := ygg.CreateFederationClient(cfg)
|
|
||||||
|
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
rsAPI := roomserver.NewInternalAPI(base)
|
||||||
natsInstance := jetstream.NATSInstance{}
|
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
|
||||||
|
|
||||||
fsAPI := federationapi.NewInternalAPI(
|
fsAPI := federationapi.NewInternalAPI(
|
||||||
processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
|
base, federation, rsAPI, base.Caches, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fsAPI.IsBlacklistedOrBackingOff)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
|
userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient())
|
||||||
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
||||||
rsAPI.SetAppserviceAPI(asAPI)
|
rsAPI.SetAppserviceAPI(asAPI)
|
||||||
|
|
||||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||||
|
@ -226,8 +176,8 @@ func (m *DendriteMonolith) Start() {
|
||||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: cfg,
|
Config: base.Cfg,
|
||||||
Client: ygg.CreateClient(),
|
Client: ygg.CreateClient(base),
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
@ -235,21 +185,21 @@ func (m *DendriteMonolith) Start() {
|
||||||
FederationAPI: fsAPI,
|
FederationAPI: fsAPI,
|
||||||
RoomserverAPI: rsAPI,
|
RoomserverAPI: rsAPI,
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
|
KeyAPI: keyAPI,
|
||||||
ExtPublicRoomsProvider: yggrooms.NewYggdrasilRoomProvider(
|
ExtPublicRoomsProvider: yggrooms.NewYggdrasilRoomProvider(
|
||||||
ygg, fsAPI, federation,
|
ygg, fsAPI, federation,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
monolith.AddAllPublicRoutes(processCtx, cfg, routers, cm, &natsInstance, caches, caching.EnableMetrics)
|
monolith.AddAllPublicRoutes(base)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter()
|
httpRouter := mux.NewRouter()
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
||||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
||||||
httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
|
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||||
httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
|
|
||||||
|
|
||||||
yggRouter := mux.NewRouter()
|
yggRouter := mux.NewRouter()
|
||||||
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
|
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
||||||
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||||
|
|
||||||
// Build both ends of a HTTP multiplex.
|
// Build both ends of a HTTP multiplex.
|
||||||
m.httpServer = &http.Server{
|
m.httpServer = &http.Server{
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM golang:1.20-bullseye as build
|
FROM golang:1.18-stretch as build
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
@ -10,22 +10,18 @@ RUN mkdir /dendrite
|
||||||
|
|
||||||
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
||||||
# downloading dependencies every time.
|
# downloading dependencies every time.
|
||||||
ARG CGO
|
|
||||||
RUN --mount=target=. \
|
RUN --mount=target=. \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
go build -o /dendrite ./cmd/generate-config && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
go build -o /dendrite ./cmd/generate-keys && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
go build -o /dendrite ./cmd/dendrite-monolith-server
|
||||||
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
|
||||||
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
RUN ./generate-keys --private-key matrix_key.pem
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
ENV API=0
|
ENV API=0
|
||||||
ENV COVER=0
|
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
||||||
|
@ -33,4 +29,4 @@ EXPOSE 8008 8448
|
||||||
CMD ./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
CMD ./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \
|
||||||
./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \
|
./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
||||||
exec /complement-cmd.sh
|
exec ./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0}
|
||||||
|
|
|
@ -12,20 +12,18 @@ FROM golang:1.18-stretch
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
ENV COVER=0
|
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
WORKDIR /runtime
|
WORKDIR /runtime
|
||||||
# This script compiles Dendrite for us.
|
# This script compiles Dendrite for us.
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eux \n\
|
#!/bin/bash -eux \n\
|
||||||
if test -f "/runtime/dendrite" && test -f "/runtime/dendrite-cover"; then \n\
|
if test -f "/runtime/dendrite-monolith-server"; then \n\
|
||||||
echo "Skipping compilation; binaries exist" \n\
|
echo "Skipping compilation; binaries exist" \n\
|
||||||
exit 0 \n\
|
exit 0 \n\
|
||||||
fi \n\
|
fi \n\
|
||||||
cd /dendrite \n\
|
cd /dendrite \n\
|
||||||
go build -v -o /runtime /dendrite/cmd/dendrite \n\
|
go build -v -o /runtime /dendrite/cmd/dendrite-monolith-server \n\
|
||||||
go test -c -cover -covermode=atomic -o /runtime/dendrite-cover -coverpkg "github.com/matrix-org/..." /dendrite/cmd/dendrite \n\
|
|
||||||
' > compile.sh && chmod +x compile.sh
|
' > compile.sh && chmod +x compile.sh
|
||||||
|
|
||||||
# This script runs Dendrite for us. Must be run in the /runtime directory.
|
# This script runs Dendrite for us. Must be run in the /runtime directory.
|
||||||
|
@ -35,8 +33,7 @@ RUN echo '\
|
||||||
./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key \n\
|
./generate-keys -keysize 1024 --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key \n\
|
||||||
./generate-config -server $SERVER_NAME --ci > dendrite.yaml \n\
|
./generate-config -server $SERVER_NAME --ci > dendrite.yaml \n\
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates \n\
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates \n\
|
||||||
[ ${COVER} -eq 1 ] && exec ./dendrite-cover --test.coverprofile=integrationcover.log --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
exec ./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
||||||
exec ./dendrite --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
|
||||||
' > run.sh && chmod +x run.sh
|
' > run.sh && chmod +x run.sh
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
#syntax=docker/dockerfile:1.2
|
||||||
|
|
||||||
FROM golang:1.20-bullseye as build
|
FROM golang:1.18-stretch as build
|
||||||
RUN apt-get update && apt-get install -y postgresql
|
RUN apt-get update && apt-get install -y postgresql
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# No password when connecting over localhost
|
# No password when connecting over localhost
|
||||||
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/13/main/pg_hba.conf && \
|
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/9.6/main/pg_hba.conf && \
|
||||||
# Bump up max conns for moar concurrency
|
# Bump up max conns for moar concurrency
|
||||||
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/13/main/postgresql.conf
|
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/9.6/main/postgresql.conf
|
||||||
|
|
||||||
# This entry script starts postgres, waits for it to be up then starts dendrite
|
# This entry script starts postgres, waits for it to be up then starts dendrite
|
||||||
RUN echo '\
|
RUN echo '\
|
||||||
#!/bin/bash -eu \n\
|
#!/bin/bash -eu \n\
|
||||||
pg_lsclusters \n\
|
pg_lsclusters \n\
|
||||||
pg_ctlcluster 13 main start \n\
|
pg_ctlcluster 9.6 main start \n\
|
||||||
\n\
|
\n\
|
||||||
until pg_isready \n\
|
until pg_isready \n\
|
||||||
do \n\
|
do \n\
|
||||||
|
@ -28,22 +28,18 @@ RUN mkdir /dendrite
|
||||||
|
|
||||||
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
||||||
# downloading dependencies every time.
|
# downloading dependencies every time.
|
||||||
ARG CGO
|
|
||||||
RUN --mount=target=. \
|
RUN --mount=target=. \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
--mount=type=cache,target=/root/.cache/go-build \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
go build -o /dendrite ./cmd/generate-config && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
go build -o /dendrite ./cmd/generate-keys && \
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
go build -o /dendrite ./cmd/dendrite-monolith-server
|
||||||
CGO_ENABLED=${CGO} go build -cover -covermode=atomic -o /dendrite/dendrite-cover -coverpkg "github.com/matrix-org/..." ./cmd/dendrite && \
|
|
||||||
cp build/scripts/complement-cmd.sh /complement-cmd.sh
|
|
||||||
|
|
||||||
WORKDIR /dendrite
|
WORKDIR /dendrite
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
RUN ./generate-keys --private-key matrix_key.pem
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
ENV API=0
|
ENV API=0
|
||||||
ENV COVER=0
|
|
||||||
EXPOSE 8008 8448
|
EXPOSE 8008 8448
|
||||||
|
|
||||||
|
|
||||||
|
@ -54,4 +50,4 @@ CMD /build/run_postgres.sh && ./generate-keys --keysize 1024 --server $SERVER_NA
|
||||||
# Bump max_open_conns up here in the global database config
|
# Bump max_open_conns up here in the global database config
|
||||||
sed -i 's/max_open_conns:.*$/max_open_conns: 1990/g' dendrite.yaml && \
|
sed -i 's/max_open_conns:.*$/max_open_conns: 1990/g' dendrite.yaml && \
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
||||||
exec /complement-cmd.sh
|
exec ./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0}
|
|
@ -1,21 +0,0 @@
|
||||||
#!/bin/bash -e
|
|
||||||
|
|
||||||
# This script is intended to be used inside a docker container for Complement
|
|
||||||
|
|
||||||
export GOCOVERDIR=/tmp/covdatafiles
|
|
||||||
mkdir -p "${GOCOVERDIR}"
|
|
||||||
if [[ "${COVER}" -eq 1 ]]; then
|
|
||||||
echo "Running with coverage"
|
|
||||||
exec /dendrite/dendrite-cover \
|
|
||||||
--really-enable-open-registration \
|
|
||||||
--tls-cert server.crt \
|
|
||||||
--tls-key server.key \
|
|
||||||
--config dendrite.yaml
|
|
||||||
else
|
|
||||||
echo "Not running with coverage"
|
|
||||||
exec /dendrite/dendrite \
|
|
||||||
--really-enable-open-registration \
|
|
||||||
--tls-cert server.crt \
|
|
||||||
--tls-key server.key \
|
|
||||||
--config dendrite.yaml
|
|
||||||
fi
|
|
|
@ -15,5 +15,5 @@ tar -xzf master.tar.gz
|
||||||
|
|
||||||
# Run the tests!
|
# Run the tests!
|
||||||
cd complement-master
|
cd complement-master
|
||||||
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests ./tests/csapi
|
COMPLEMENT_BASE_IMAGE=complement-dendrite:latest go test -v -count=1 ./tests
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -14,18 +14,10 @@
|
||||||
|
|
||||||
package api
|
package api
|
||||||
|
|
||||||
import "github.com/matrix-org/gomatrixserverlib/fclient"
|
import "github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
// ExtraPublicRoomsProvider provides a way to inject extra published rooms into /publicRooms requests.
|
||||||
type ExtraPublicRoomsProvider interface {
|
type ExtraPublicRoomsProvider interface {
|
||||||
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
||||||
Rooms() []fclient.PublicRoom
|
Rooms() []gomatrixserverlib.PublicRoom
|
||||||
}
|
|
||||||
|
|
||||||
type RegistrationToken struct {
|
|
||||||
Token *string `json:"token"`
|
|
||||||
UsesAllowed *int32 `json:"uses_allowed"`
|
|
||||||
Pending *int32 `json:"pending"`
|
|
||||||
Completed *int32 `json:"completed"`
|
|
||||||
ExpiryTime *int64 `json:"expiry_time"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ func VerifyUserFromRequest(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.MissingToken(err.Error()),
|
JSON: jsonerror.MissingToken(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var res api.QueryAccessTokenResponse
|
var res api.QueryAccessTokenResponse
|
||||||
|
@ -68,23 +68,21 @@ func VerifyUserFromRequest(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
||||||
return nil, &util.JSONResponse{
|
jsonErr := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return nil, &jsonErr
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if res.Err != "" {
|
if res.Err != "" {
|
||||||
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden(res.Err),
|
JSON: jsonerror.Forbidden(res.Err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if res.Device == nil {
|
if res.Device == nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.UnknownToken("Unknown token"),
|
JSON: jsonerror.UnknownToken("Unknown token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.Device, nil
|
return res.Device, nil
|
||||||
|
|
|
@ -16,8 +16,6 @@ package authtypes
|
||||||
|
|
||||||
// ThreePID represents a third-party identifier
|
// ThreePID represents a third-party identifier
|
||||||
type ThreePID struct {
|
type ThreePID struct {
|
||||||
Address string `json:"address"`
|
Address string `json:"address"`
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
AddedAt int64 `json:"added_at"`
|
|
||||||
ValidatedAt int64 `json:"validated_at"`
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,14 +15,15 @@
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,17 +32,12 @@ import (
|
||||||
// called after authorization has completed, with the result of the authorization.
|
// called after authorization has completed, with the result of the authorization.
|
||||||
// If the final return value is non-nil, an error occurred and the cleanup function
|
// If the final return value is non-nil, an error occurred and the cleanup function
|
||||||
// is nil.
|
// is nil.
|
||||||
func LoginFromJSONReader(
|
func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
||||||
req *http.Request,
|
reqBytes, err := io.ReadAll(r)
|
||||||
useraccountAPI uapi.UserLoginAPI,
|
|
||||||
userAPI UserInternalAPIForLogin,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
|
||||||
reqBytes, err := io.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -52,7 +48,7 @@ func LoginFromJSONReader(
|
||||||
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
||||||
err := &util.JSONResponse{
|
err := &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
JSON: jsonerror.BadJSON("Reading request body failed: " + err.Error()),
|
||||||
}
|
}
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
@ -61,37 +57,23 @@ func LoginFromJSONReader(
|
||||||
switch header.Type {
|
switch header.Type {
|
||||||
case authtypes.LoginTypePassword:
|
case authtypes.LoginTypePassword:
|
||||||
typ = &LoginTypePassword{
|
typ = &LoginTypePassword{
|
||||||
UserAPI: useraccountAPI,
|
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeToken:
|
case authtypes.LoginTypeToken:
|
||||||
typ = &LoginTypeToken{
|
typ = &LoginTypeToken{
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
case authtypes.LoginTypeApplicationService:
|
|
||||||
token, err := ExtractAccessToken(req)
|
|
||||||
if err != nil {
|
|
||||||
err := &util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.MissingToken(err.Error()),
|
|
||||||
}
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
typ = &LoginTypeApplicationService{
|
|
||||||
Config: cfg,
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
err := util.JSONResponse{
|
err := util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam("unhandled login type: " + header.Type),
|
JSON: jsonerror.InvalidArgumentValue("unhandled login type: " + header.Type),
|
||||||
}
|
}
|
||||||
return nil, nil, &err
|
return nil, nil, &err
|
||||||
}
|
}
|
||||||
|
|
||||||
return typ.LoginFromJSON(req.Context(), reqBytes)
|
return typ.LoginFromJSON(ctx, reqBytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
// Copyright 2023 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/internal"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginTypeApplicationService describes how to authenticate as an
|
|
||||||
// application service
|
|
||||||
type LoginTypeApplicationService struct {
|
|
||||||
Config *config.ClientAPI
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements Type
|
|
||||||
func (t *LoginTypeApplicationService) Name() string {
|
|
||||||
return authtypes.LoginTypeApplicationService
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginFromJSON implements Type
|
|
||||||
func (t *LoginTypeApplicationService) LoginFromJSON(
|
|
||||||
ctx context.Context, reqBytes []byte,
|
|
||||||
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
|
||||||
var r Login
|
|
||||||
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := internal.ValidateApplicationServiceRequest(t.Config, r.Identifier.User, t.Token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup := func(ctx context.Context, j *util.JSONResponse) {}
|
|
||||||
return &r, cleanup, nil
|
|
||||||
}
|
|
|
@ -17,17 +17,13 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,9 +31,8 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
Token string
|
|
||||||
|
|
||||||
WantUsername string
|
WantUsername string
|
||||||
WantDeviceID string
|
WantDeviceID string
|
||||||
|
@ -51,7 +46,7 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
"password": "herpassword",
|
"password": "herpassword",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantUsername: "@alice:example.com",
|
WantUsername: "alice",
|
||||||
WantDeviceID: "adevice",
|
WantDeviceID: "adevice",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,69 +60,19 @@ func TestLoginFromJSONReader(t *testing.T) {
|
||||||
WantDeviceID: "adevice",
|
WantDeviceID: "adevice",
|
||||||
WantDeletedTokens: []string{"atoken"},
|
WantDeletedTokens: []string{"atoken"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "appServiceWorksUserID",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
Token: "astoken",
|
|
||||||
|
|
||||||
WantUsername: "@alice:example.com",
|
|
||||||
WantDeviceID: "adevice",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "appServiceWorksLocalpart",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "alice" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
Token: "astoken",
|
|
||||||
|
|
||||||
WantUsername: "alice",
|
|
||||||
WantDeviceID: "adevice",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
t.Run(tst.Name, func(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: fclient.SigningIdentity{
|
ServerName: serverName,
|
||||||
ServerName: serverName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Derived: &config.Derived{
|
|
||||||
ApplicationServices: []config.ApplicationService{
|
|
||||||
{
|
|
||||||
ID: "anapplicationservice",
|
|
||||||
ASToken: "astoken",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {
|
|
||||||
{
|
|
||||||
Exclusive: true,
|
|
||||||
Regex: "@alice:example.com",
|
|
||||||
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
login, cleanup, err := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
if err != nil {
|
||||||
if tst.Token != "" {
|
t.Fatalf("LoginFromJSONReader failed: %+v", err)
|
||||||
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
login, cleanup, jsonErr := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
|
||||||
if jsonErr != nil {
|
|
||||||
t.Fatalf("LoginFromJSONReader failed: %+v", jsonErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
cleanup(ctx, &util.JSONResponse{Code: http.StatusOK})
|
||||||
|
|
||||||
if login.Username() != tst.WantUsername {
|
if login.Username() != tst.WantUsername {
|
||||||
|
@ -155,17 +100,16 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
tsts := []struct {
|
tsts := []struct {
|
||||||
Name string
|
Name string
|
||||||
Body string
|
Body string
|
||||||
Token string
|
|
||||||
|
|
||||||
WantErrCode spec.MatrixErrorCode
|
WantErrCode string
|
||||||
}{
|
}{
|
||||||
{Name: "empty", WantErrCode: spec.ErrorBadJSON},
|
{Name: "empty", WantErrCode: "M_BAD_JSON"},
|
||||||
{
|
{
|
||||||
Name: "badUnmarshal",
|
Name: "badUnmarshal",
|
||||||
Body: `badsyntaxJSON`,
|
Body: `badsyntaxJSON`,
|
||||||
WantErrCode: spec.ErrorBadJSON,
|
WantErrCode: "M_BAD_JSON",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badPassword",
|
Name: "badPassword",
|
||||||
|
@ -175,7 +119,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"password": "invalidpassword",
|
"password": "invalidpassword",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: spec.ErrorForbidden,
|
WantErrCode: "M_FORBIDDEN",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badToken",
|
Name: "badToken",
|
||||||
|
@ -184,7 +128,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"token": "invalidtoken",
|
"token": "invalidtoken",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: spec.ErrorForbidden,
|
WantErrCode: "M_FORBIDDEN",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "badType",
|
Name: "badType",
|
||||||
|
@ -192,46 +136,7 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
"type": "m.login.invalid",
|
"type": "m.login.invalid",
|
||||||
"device_id": "adevice"
|
"device_id": "adevice"
|
||||||
}`,
|
}`,
|
||||||
WantErrCode: spec.ErrorInvalidParam,
|
WantErrCode: "M_INVALID_ARGUMENT_VALUE",
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "noASToken",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: "M_MISSING_TOKEN",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badASToken",
|
|
||||||
Token: "badastoken",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "@alice:example.com" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: "M_UNKNOWN_TOKEN",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badASNamespace",
|
|
||||||
Token: "astoken",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "@bob:example.com" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: "M_EXCLUSIVE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badASUserID",
|
|
||||||
Token: "astoken",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.application_service",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "@alice:wrong.example.com" },
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: "M_INVALID_USERNAME",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tst := range tsts {
|
for _, tst := range tsts {
|
||||||
|
@ -239,38 +144,14 @@ func TestBadLoginFromJSONReader(t *testing.T) {
|
||||||
var userAPI fakeUserInternalAPI
|
var userAPI fakeUserInternalAPI
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: fclient.SigningIdentity{
|
ServerName: serverName,
|
||||||
ServerName: serverName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Derived: &config.Derived{
|
|
||||||
ApplicationServices: []config.ApplicationService{
|
|
||||||
{
|
|
||||||
ID: "anapplicationservice",
|
|
||||||
ASToken: "astoken",
|
|
||||||
NamespaceMap: map[string][]config.ApplicationServiceNamespace{
|
|
||||||
"users": {
|
|
||||||
{
|
|
||||||
Exclusive: true,
|
|
||||||
Regex: "@alice:example.com",
|
|
||||||
RegexpObject: regexp.MustCompile("@alice:example.com"),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader(tst.Body))
|
_, cleanup, errRes := LoginFromJSONReader(ctx, strings.NewReader(tst.Body), &userAPI, &userAPI, cfg)
|
||||||
if tst.Token != "" {
|
|
||||||
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
|
||||||
if errRes == nil {
|
if errRes == nil {
|
||||||
cleanup(ctx, nil)
|
cleanup(ctx, nil)
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
} else if merr, ok := errRes.JSON.(spec.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
} else if merr, ok := errRes.JSON.(*jsonerror.MatrixError); ok && merr.ErrCode != tst.WantErrCode {
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -288,15 +169,7 @@ func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
res.Exists = true
|
res.Exists = true
|
||||||
res.Account = &uapi.Account{UserID: userutil.MakeUserID(req.Localpart, req.ServerName)}
|
res.Account = &uapi.Account{}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) QueryAccountByLocalpart(ctx context.Context, req *uapi.QueryAccountByLocalpartRequest, res *uapi.QueryAccountByLocalpartResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) PerformAccountCreation(ctx context.Context, req *uapi.PerformAccountCreationRequest, res *uapi.PerformAccountCreationResponse) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -48,15 +48,13 @@ func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*L
|
||||||
var res uapi.QueryLoginTokenResponse
|
var res uapi.QueryLoginTokenResponse
|
||||||
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
||||||
return nil, nil, &util.JSONResponse{
|
jsonErr := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return nil, nil, &jsonErr
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if res.Data == nil {
|
if res.Data == nil {
|
||||||
return nil, nil, &util.JSONResponse{
|
return nil, nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("invalid login token"),
|
JSON: jsonerror.Forbidden("invalid login token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,21 +16,20 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"github.com/go-ldap/ldap/v3"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GetAccountByPassword func(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error
|
||||||
|
|
||||||
type PasswordRequest struct {
|
type PasswordRequest struct {
|
||||||
Login
|
Login
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
|
@ -38,8 +37,8 @@ type PasswordRequest struct {
|
||||||
|
|
||||||
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
// LoginTypePassword implements https://matrix.org/docs/spec/client_server/r0.6.1#password-based
|
||||||
type LoginTypePassword struct {
|
type LoginTypePassword struct {
|
||||||
Config *config.ClientAPI
|
GetAccountByPassword GetAccountByPassword
|
||||||
UserAPI api.UserLoginAPI
|
Config *config.ClientAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Name() string {
|
func (t *LoginTypePassword) Name() string {
|
||||||
|
@ -60,227 +59,51 @@ func (t *LoginTypePassword) LoginFromJSON(ctx context.Context, reqBytes []byte)
|
||||||
return login, func(context.Context, *util.JSONResponse) {}, nil
|
return login, func(context.Context, *util.JSONResponse) {}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Login(ctx context.Context, request *PasswordRequest) (*Login, *util.JSONResponse) {
|
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
||||||
fullUsername := request.Username()
|
r := req.(*PasswordRequest)
|
||||||
if fullUsername == "" {
|
username := strings.ToLower(r.Username())
|
||||||
|
if username == "" {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.BadJSON("A username must be supplied."),
|
JSON: jsonerror.BadJSON("A username must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(request.Password) == 0 {
|
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.BadJSON("A password must be supplied."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
username, domain, err := userutil.ParseUsernameParam(fullUsername, t.Config.Matrix)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.InvalidUsername(err.Error()),
|
JSON: jsonerror.InvalidUsername(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !t.Config.Matrix.IsLocalServerName(domain) {
|
// Squash username to all lowercase letters
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername("The server name is not known."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var account *api.Account
|
|
||||||
if t.Config.Ldap.Enabled {
|
|
||||||
isAdmin, err := t.authenticateLdap(username, request.Password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
acc, err := t.getOrCreateAccount(ctx, username, domain, isAdmin)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
account = acc
|
|
||||||
} else {
|
|
||||||
acc, err := t.authenticateDb(ctx, username, domain, request.Password)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
account = acc
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the user, so login.Username() can do the right thing
|
|
||||||
request.Identifier.User = account.UserID
|
|
||||||
request.User = account.UserID
|
|
||||||
return &request.Login, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *LoginTypePassword) authenticateDb(ctx context.Context, username string, domain spec.ServerName, password string) (*api.Account, *util.JSONResponse) {
|
|
||||||
res := &api.QueryAccountByPasswordResponse{}
|
res := &api.QueryAccountByPasswordResponse{}
|
||||||
err := t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{Localpart: strings.ToLower(localpart), PlaintextPassword: r.Password}, res)
|
||||||
Localpart: strings.ToLower(username),
|
|
||||||
ServerName: domain,
|
|
||||||
PlaintextPassword: password,
|
|
||||||
}, res)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("Unable to fetch account by password."),
|
JSON: jsonerror.Unknown("unable to fetch account by password"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
err = t.UserAPI.QueryAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
||||||
Localpart: username,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
PlaintextPassword: r.Password,
|
||||||
PlaintextPassword: password,
|
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("Unable to fetch account by password."),
|
JSON: jsonerror.Unknown("unable to fetch account by password"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
||||||
|
// but that would leak the existence of the user.
|
||||||
if !res.Exists {
|
if !res.Exists {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("The username or password was incorrect or the account does not exist."),
|
JSON: jsonerror.Forbidden("The username or password was incorrect or the account does not exist."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res.Account, nil
|
return &r.Login, nil
|
||||||
}
|
|
||||||
func (t *LoginTypePassword) authenticateLdap(username, password string) (bool, *util.JSONResponse) {
|
|
||||||
var conn *ldap.Conn
|
|
||||||
conn, err := ldap.DialURL(t.Config.Ldap.Uri)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("unable to connect to ldap: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
if t.Config.Ldap.AdminBindEnabled {
|
|
||||||
err = conn.Bind(t.Config.Ldap.AdminBindDn, t.Config.Ldap.AdminBindPassword)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("unable to bind to ldap: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
filter := strings.ReplaceAll(t.Config.Ldap.SearchFilter, "{username}", username)
|
|
||||||
searchRequest := ldap.NewSearchRequest(
|
|
||||||
t.Config.Ldap.BaseDn, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
|
||||||
0, 0, false, filter, []string{t.Config.Ldap.SearchAttribute}, nil,
|
|
||||||
)
|
|
||||||
result, err := conn.Search(searchRequest)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("unable to bind to search ldap: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result.Entries) > 1 {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.BadJSON("'user' must be duplicated."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(result.Entries) < 1 {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.BadJSON("'user' not found."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
userDN := result.Entries[0].DN
|
|
||||||
err = conn.Bind(userDN, password)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bindDn := strings.ReplaceAll(t.Config.Ldap.UserBindDn, "{username}", username)
|
|
||||||
err = conn.Bind(bindDn, password)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isAdmin, err := t.isLdapAdmin(conn, username)
|
|
||||||
if err != nil {
|
|
||||||
return false, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return isAdmin, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *LoginTypePassword) isLdapAdmin(conn *ldap.Conn, username string) (bool, error) {
|
|
||||||
searchRequest := ldap.NewSearchRequest(
|
|
||||||
t.Config.Ldap.AdminGroupDn,
|
|
||||||
ldap.ScopeWholeSubtree, ldap.DerefAlways, 0, 0, false,
|
|
||||||
strings.ReplaceAll(t.Config.Ldap.AdminGroupFilter, "{username}", username),
|
|
||||||
[]string{t.Config.Ldap.AdminGroupAttribute},
|
|
||||||
nil)
|
|
||||||
|
|
||||||
sr, err := conn.Search(searchRequest)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(sr.Entries) < 1 {
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *LoginTypePassword) getOrCreateAccount(ctx context.Context, username string, domain spec.ServerName, admin bool) (*api.Account, *util.JSONResponse) {
|
|
||||||
var existing api.QueryAccountByLocalpartResponse
|
|
||||||
err := t.UserAPI.QueryAccountByLocalpart(ctx, &api.QueryAccountByLocalpartRequest{
|
|
||||||
Localpart: username,
|
|
||||||
ServerName: domain,
|
|
||||||
}, &existing)
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
return existing.Account, nil
|
|
||||||
}
|
|
||||||
if err != sql.ErrNoRows {
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
accountType := api.AccountTypeUser
|
|
||||||
if admin {
|
|
||||||
accountType = api.AccountTypeAdmin
|
|
||||||
}
|
|
||||||
var created api.PerformAccountCreationResponse
|
|
||||||
err = t.UserAPI.PerformAccountCreation(ctx, &api.PerformAccountCreationRequest{
|
|
||||||
AppServiceID: "ldap",
|
|
||||||
Localpart: username,
|
|
||||||
Password: uuid.New().String(),
|
|
||||||
AccountType: accountType,
|
|
||||||
OnConflict: api.ConflictAbort,
|
|
||||||
}, &created)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if _, ok := err.(*api.ErrorConflict); ok {
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.UserInUse("Desired user ID is already taken."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return created.Account, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
@ -55,7 +55,7 @@ type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
||||||
type LoginIdentifier struct {
|
type LoginIdentifier struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// when type = m.id.user or m.id.application_service
|
// when type = m.id.user
|
||||||
User string `json:"user"`
|
User string `json:"user"`
|
||||||
// when type = m.id.thirdparty
|
// when type = m.id.thirdparty
|
||||||
Medium string `json:"medium"`
|
Medium string `json:"medium"`
|
||||||
|
@ -113,8 +113,8 @@ type UserInteractive struct {
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||||
typePassword := &LoginTypePassword{
|
typePassword := &LoginTypePassword{
|
||||||
UserAPI: userAccountAPI,
|
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
return &UserInteractive{
|
return &UserInteractive{
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{
|
||||||
|
@ -178,10 +178,8 @@ func (u *UserInteractive) NewSession() *util.JSONResponse {
|
||||||
sessionID, err := GenerateAccessToken()
|
sessionID, err := GenerateAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Error("failed to generate session ID")
|
logrus.WithError(err).Error("failed to generate session ID")
|
||||||
return &util.JSONResponse{
|
res := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &res
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
u.Lock()
|
u.Lock()
|
||||||
u.Sessions[sessionID] = []string{}
|
u.Sessions[sessionID] = []string{}
|
||||||
|
@ -195,19 +193,15 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
|
||||||
mixedObjects := make(map[string]interface{})
|
mixedObjects := make(map[string]interface{})
|
||||||
b, err := json.Marshal(response)
|
b, err := json.Marshal(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
ise := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &ise
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
challenge := u.challenge(sessionID)
|
challenge := u.challenge(sessionID)
|
||||||
b, err = json.Marshal(challenge.JSON)
|
b, err = json.Marshal(challenge.JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
ise := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &ise
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ = json.Unmarshal(b, &mixedObjects)
|
_ = json.Unmarshal(b, &mixedObjects)
|
||||||
|
|
||||||
|
@ -240,7 +234,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("Unknown auth.type: " + authType),
|
JSON: jsonerror.BadJSON("Unknown auth.type: " + authType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -256,7 +250,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !u.IsSingleStageFlow(authType) {
|
if !u.IsSingleStageFlow(authType) {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown("The auth.session is missing or unknown."),
|
JSON: jsonerror.Unknown("The auth.session is missing or unknown."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,13 @@ import (
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
serverName = spec.ServerName("example.com")
|
serverName = gomatrixserverlib.ServerName("example.com")
|
||||||
// space separated localpart+password -> account
|
// space separated localpart+password -> account
|
||||||
lookup = make(map[string]*api.Account)
|
lookup = make(map[string]*api.Account)
|
||||||
device = &api.Device{
|
device = &api.Device{
|
||||||
|
@ -45,20 +44,10 @@ func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *a
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) QueryAccountByLocalpart(ctx context.Context, req *api.QueryAccountByLocalpartRequest, res *api.QueryAccountByLocalpartResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) PerformAccountCreation(ctx context.Context, req *api.PerformAccountCreationRequest, res *api.PerformAccountCreationResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func setup() *UserInteractive {
|
func setup() *UserInteractive {
|
||||||
cfg := &config.ClientAPI{
|
cfg := &config.ClientAPI{
|
||||||
Matrix: &config.Global{
|
Matrix: &config.Global{
|
||||||
SigningIdentity: fclient.SigningIdentity{
|
ServerName: serverName,
|
||||||
ServerName: serverName,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
||||||
|
|
|
@ -15,54 +15,55 @@
|
||||||
package clientapi
|
package clientapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/clientapi/routing"
|
"github.com/matrix-org/dendrite/clientapi/routing"
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
|
keyserverAPI "github.com/matrix-org/dendrite/keyserver/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/base"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
||||||
func AddPublicRoutes(
|
func AddPublicRoutes(
|
||||||
processContext *process.ProcessContext,
|
base *base.BaseDendrite,
|
||||||
routers httputil.Routers,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
cfg *config.Dendrite,
|
|
||||||
natsInstance *jetstream.NATSInstance,
|
|
||||||
federation fclient.FederationClient,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
transactionsCache *transactions.Cache,
|
transactionsCache *transactions.Cache,
|
||||||
fsAPI federationAPI.ClientFederationAPI,
|
fsAPI federationAPI.ClientFederationAPI,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
|
keyAPI keyserverAPI.ClientKeyAPI,
|
||||||
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
) {
|
) {
|
||||||
js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
cfg := &base.Cfg.ClientAPI
|
||||||
|
mscCfg := &base.Cfg.MSCs
|
||||||
|
js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream)
|
||||||
|
|
||||||
syncProducer := &producers.SyncAPIProducer{
|
syncProducer := &producers.SyncAPIProducer{
|
||||||
JetStream: js,
|
JetStream: js,
|
||||||
TopicReceiptEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
||||||
TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
TopicSendToDeviceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
||||||
TopicTypingEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
TopicTypingEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
||||||
TopicPresenceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
TopicPresenceEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
||||||
UserAPI: userAPI,
|
UserAPI: userAPI,
|
||||||
ServerName: cfg.Global.ServerName,
|
ServerName: cfg.Matrix.ServerName,
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
routers,
|
base.PublicClientAPIMux,
|
||||||
|
base.PublicWellKnownAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
|
base.DendriteAdminMux,
|
||||||
cfg, rsAPI, asAPI,
|
cfg, rsAPI, asAPI,
|
||||||
userAPI, userDirectoryProvider, federation,
|
userAPI, userDirectoryProvider, federation,
|
||||||
syncProducer, transactionsCache, fsAPI,
|
syncProducer, transactionsCache, fsAPI, keyAPI,
|
||||||
extRoomsProvider, natsClient, enableMetrics,
|
extRoomsProvider, mscCfg, natsClient,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -20,7 +20,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,10 +32,8 @@ func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONRespon
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
return &util.JSONResponse{
|
resp := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &resp
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnmarshalJSON(body, iface)
|
return UnmarshalJSON(body, iface)
|
||||||
|
@ -45,7 +43,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
if !utf8.Valid(body) {
|
if !utf8.Valid(body) {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.NotJSON("Body contains invalid UTF-8"),
|
JSON: jsonerror.NotJSON("Body contains invalid UTF-8"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,7 +53,7 @@ func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||||
// valid JSON with incorrect types for values.
|
// valid JSON with incorrect types for values.
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
229
clientapi/jsonerror/jsonerror.go
Normal file
229
clientapi/jsonerror/jsonerror.go
Normal file
|
@ -0,0 +1,229 @@
|
||||||
|
// Copyright 2017 Vector Creations Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package jsonerror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MatrixError represents the "standard error response" in Matrix.
|
||||||
|
// http://matrix.org/docs/spec/client_server/r0.2.0.html#api-standards
|
||||||
|
type MatrixError struct {
|
||||||
|
ErrCode string `json:"errcode"`
|
||||||
|
Err string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e MatrixError) Error() string {
|
||||||
|
return fmt.Sprintf("%s: %s", e.ErrCode, e.Err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalServerError returns a 500 Internal Server Error in a matrix-compliant
|
||||||
|
// format.
|
||||||
|
func InternalServerError() util.JSONResponse {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: Unknown("Internal Server Error"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown is an unexpected error
|
||||||
|
func Unknown(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_UNKNOWN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forbidden is an error when the client tries to access a resource
|
||||||
|
// they are not allowed to access.
|
||||||
|
func Forbidden(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_FORBIDDEN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadJSON is an error when the client supplies malformed JSON.
|
||||||
|
func BadJSON(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_BAD_JSON", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BadAlias is an error when the client supplies a bad alias.
|
||||||
|
func BadAlias(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_BAD_ALIAS", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotJSON is an error when the client supplies something that is not JSON
|
||||||
|
// to a JSON endpoint.
|
||||||
|
func NotJSON(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_NOT_JSON", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotFound is an error when the client tries to access an unknown resource.
|
||||||
|
func NotFound(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_NOT_FOUND", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissingArgument is an error when the client tries to access a resource
|
||||||
|
// without providing an argument that is required.
|
||||||
|
func MissingArgument(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_MISSING_ARGUMENT", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidArgumentValue is an error when the client tries to provide an
|
||||||
|
// invalid value for a valid argument
|
||||||
|
func InvalidArgumentValue(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_INVALID_ARGUMENT_VALUE", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissingToken is an error when the client tries to access a resource which
|
||||||
|
// requires authentication without supplying credentials.
|
||||||
|
func MissingToken(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_MISSING_TOKEN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnknownToken is an error when the client tries to access a resource which
|
||||||
|
// requires authentication and supplies an unrecognised token
|
||||||
|
func UnknownToken(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_UNKNOWN_TOKEN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WeakPassword is an error which is returned when the client tries to register
|
||||||
|
// using a weak password. http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
|
||||||
|
func WeakPassword(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_WEAK_PASSWORD", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidUsername is an error returned when the client tries to register an
|
||||||
|
// invalid username
|
||||||
|
func InvalidUsername(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_INVALID_USERNAME", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserInUse is an error returned when the client tries to register an
|
||||||
|
// username that already exists
|
||||||
|
func UserInUse(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_USER_IN_USE", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoomInUse is an error returned when the client tries to make a room
|
||||||
|
// that already exists
|
||||||
|
func RoomInUse(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_ROOM_IN_USE", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASExclusive is an error returned when an application service tries to
|
||||||
|
// register an username that is outside of its registered namespace, or if a
|
||||||
|
// user attempts to register a username or room alias within an exclusive
|
||||||
|
// namespace.
|
||||||
|
func ASExclusive(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_EXCLUSIVE", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GuestAccessForbidden is an error which is returned when the client is
|
||||||
|
// forbidden from accessing a resource as a guest.
|
||||||
|
func GuestAccessForbidden(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_GUEST_ACCESS_FORBIDDEN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidSignature is an error which is returned when the client tries
|
||||||
|
// to upload invalid signatures.
|
||||||
|
func InvalidSignature(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_INVALID_SIGNATURE", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InvalidParam is an error that is returned when a parameter was invalid,
|
||||||
|
// traditionally with cross-signing.
|
||||||
|
func InvalidParam(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_INVALID_PARAM", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MissingParam is an error that is returned when a parameter was incorrect,
|
||||||
|
// traditionally with cross-signing.
|
||||||
|
func MissingParam(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_MISSING_PARAM", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnableToAuthoriseJoin is an error that is returned when a server can't
|
||||||
|
// determine whether to allow a restricted join or not.
|
||||||
|
func UnableToAuthoriseJoin(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_UNABLE_TO_AUTHORISE_JOIN", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LeaveServerNoticeError is an error returned when trying to reject an invite
|
||||||
|
// for a server notice room.
|
||||||
|
func LeaveServerNoticeError() *MatrixError {
|
||||||
|
return &MatrixError{
|
||||||
|
ErrCode: "M_CANNOT_LEAVE_SERVER_NOTICE_ROOM",
|
||||||
|
Err: "You cannot reject this invite",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type IncompatibleRoomVersionError struct {
|
||||||
|
RoomVersion string `json:"room_version"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
Code string `json:"errcode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncompatibleRoomVersion is an error which is returned when the client
|
||||||
|
// requests a room with a version that is unsupported.
|
||||||
|
func IncompatibleRoomVersion(roomVersion gomatrixserverlib.RoomVersion) *IncompatibleRoomVersionError {
|
||||||
|
return &IncompatibleRoomVersionError{
|
||||||
|
Code: "M_INCOMPATIBLE_ROOM_VERSION",
|
||||||
|
RoomVersion: string(roomVersion),
|
||||||
|
Error: "Your homeserver does not support the features required to join this room",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnsupportedRoomVersion is an error which is returned when the client
|
||||||
|
// requests a room with a version that is unsupported.
|
||||||
|
func UnsupportedRoomVersion(msg string) *MatrixError {
|
||||||
|
return &MatrixError{"M_UNSUPPORTED_ROOM_VERSION", msg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitExceededError is a rate-limiting error.
|
||||||
|
type LimitExceededError struct {
|
||||||
|
MatrixError
|
||||||
|
RetryAfterMS int64 `json:"retry_after_ms,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LimitExceeded is an error when the client tries to send events too quickly.
|
||||||
|
func LimitExceeded(msg string, retryAfterMS int64) *LimitExceededError {
|
||||||
|
return &LimitExceededError{
|
||||||
|
MatrixError: MatrixError{"M_LIMIT_EXCEEDED", msg},
|
||||||
|
RetryAfterMS: retryAfterMS,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotTrusted is an error which is returned when the client asks the server to
|
||||||
|
// proxy a request (e.g. 3PID association) to a server that isn't trusted
|
||||||
|
func NotTrusted(serverName string) *MatrixError {
|
||||||
|
return &MatrixError{
|
||||||
|
ErrCode: "M_SERVER_NOT_TRUSTED",
|
||||||
|
Err: fmt.Sprintf("Untrusted server '%s'", serverName),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InternalAPIError is returned when Dendrite failed to reach an internal API.
|
||||||
|
func InternalAPIError(ctx context.Context, err error) util.JSONResponse {
|
||||||
|
logrus.WithContext(ctx).WithError(err).Error("Error reaching an internal API")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: &MatrixError{
|
||||||
|
ErrCode: "M_INTERNAL_SERVER_ERROR",
|
||||||
|
Err: "Dendrite encountered an error reaching an internal API.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
44
clientapi/jsonerror/jsonerror_test.go
Normal file
44
clientapi/jsonerror/jsonerror_test.go
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright 2017 Vector Creations Ltd
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package jsonerror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLimitExceeded(t *testing.T) {
|
||||||
|
e := LimitExceeded("too fast", 5000)
|
||||||
|
jsonBytes, err := json.Marshal(&e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TestLimitExceeded: Failed to marshal LimitExceeded error. %s", err.Error())
|
||||||
|
}
|
||||||
|
want := `{"errcode":"M_LIMIT_EXCEEDED","error":"too fast","retry_after_ms":5000}`
|
||||||
|
if string(jsonBytes) != want {
|
||||||
|
t.Errorf("TestLimitExceeded: want %s, got %s", want, string(jsonBytes))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestForbidden(t *testing.T) {
|
||||||
|
e := Forbidden("you shall not pass")
|
||||||
|
jsonBytes, err := json.Marshal(&e)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("TestForbidden: Failed to marshal Forbidden error. %s", err.Error())
|
||||||
|
}
|
||||||
|
want := `{"errcode":"M_FORBIDDEN","error":"you shall not pass"}`
|
||||||
|
if string(jsonBytes) != want {
|
||||||
|
t.Errorf("TestForbidden: want %s, got %s", want, string(jsonBytes))
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,14 +21,12 @@ import (
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncAPIProducer produces events for the sync API server to consume
|
// SyncAPIProducer produces events for the sync API server to consume
|
||||||
|
@ -38,13 +36,13 @@ type SyncAPIProducer struct {
|
||||||
TopicTypingEvent string
|
TopicTypingEvent string
|
||||||
TopicPresenceEvent string
|
TopicPresenceEvent string
|
||||||
JetStream nats.JetStreamContext
|
JetStream nats.JetStreamContext
|
||||||
ServerName spec.ServerName
|
ServerName gomatrixserverlib.ServerName
|
||||||
UserAPI userapi.ClientUserAPI
|
UserAPI userapi.ClientUserAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendReceipt(
|
func (p *SyncAPIProducer) SendReceipt(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID, roomID, eventID, receiptType string, timestamp spec.Timestamp,
|
userID, roomID, eventID, receiptType string, timestamp gomatrixserverlib.Timestamp,
|
||||||
) error {
|
) error {
|
||||||
m := &nats.Msg{
|
m := &nats.Msg{
|
||||||
Subject: p.TopicReceiptEvent,
|
Subject: p.TopicReceiptEvent,
|
||||||
|
@ -63,7 +61,7 @@ func (p *SyncAPIProducer) SendReceipt(
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendToDevice(
|
func (p *SyncAPIProducer) SendToDevice(
|
||||||
ctx context.Context, sender, userID, deviceID, eventType string,
|
ctx context.Context, sender, userID, deviceID, eventType string,
|
||||||
message json.RawMessage,
|
message interface{},
|
||||||
) error {
|
) error {
|
||||||
devices := []string{}
|
devices := []string{}
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
|
@ -91,19 +89,24 @@ func (p *SyncAPIProducer) SendToDevice(
|
||||||
devices = append(devices, deviceID)
|
devices = append(devices, deviceID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
js, err := json.Marshal(message)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"num_devices": len(devices),
|
"num_devices": len(devices),
|
||||||
"type": eventType,
|
"type": eventType,
|
||||||
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
||||||
for i, device := range devices {
|
for _, device := range devices {
|
||||||
ote := &types.OutputSendToDeviceEvent{
|
ote := &types.OutputSendToDeviceEvent{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
DeviceID: device,
|
DeviceID: device,
|
||||||
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
||||||
Sender: sender,
|
Sender: sender,
|
||||||
Type: eventType,
|
Type: eventType,
|
||||||
Content: message,
|
Content: js,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,17 +115,15 @@ func (p *SyncAPIProducer) SendToDevice(
|
||||||
log.WithError(err).Error("sendToDevice failed json.Marshal")
|
log.WithError(err).Error("sendToDevice failed json.Marshal")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
m := nats.NewMsg(p.TopicSendToDeviceEvent)
|
m := &nats.Msg{
|
||||||
m.Data = eventJSON
|
Subject: p.TopicSendToDeviceEvent,
|
||||||
|
Data: eventJSON,
|
||||||
|
Header: nats.Header{},
|
||||||
|
}
|
||||||
m.Header.Set("sender", sender)
|
m.Header.Set("sender", sender)
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
m.Header.Set(jetstream.UserID, userID)
|
||||||
|
|
||||||
if _, err = p.JetStream.PublishMsg(m, nats.Context(ctx)); err != nil {
|
if _, err = p.JetStream.PublishMsg(m, nats.Context(ctx)); err != nil {
|
||||||
if i < len(devices)-1 {
|
log.WithError(err).Error("sendToDevice failed t.Producer.SendMessage")
|
||||||
log.WithError(err).Warn("sendToDevice failed to PublishMsg, trying further devices")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
log.WithError(err).Error("sendToDevice failed to PublishMsg for all devices")
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -155,7 +156,7 @@ func (p *SyncAPIProducer) SendPresence(
|
||||||
m.Header.Set("status_msg", *statusMsg)
|
m.Header.Set("status_msg", *statusMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.Header.Set("last_active_ts", strconv.Itoa(int(spec.AsTimestamp(time.Now()))))
|
m.Header.Set("last_active_ts", strconv.Itoa(int(gomatrixserverlib.AsTimestamp(time.Now()))))
|
||||||
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -21,11 +21,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -38,7 +38,7 @@ func GetAccountData(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID does not match the current user"),
|
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ func GetAccountData(
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound("data not found"),
|
JSON: jsonerror.NotFound("data not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ func SaveAccountData(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID does not match the current user"),
|
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,30 +90,27 @@ func SaveAccountData(
|
||||||
if req.Body == http.NoBody {
|
if req.Body == http.NoBody {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.NotJSON("Content not JSON"),
|
JSON: jsonerror.NotJSON("Content not JSON"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
if dataType == "m.fully_read" || dataType == "m.push_rules" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
JSON: jsonerror.Forbidden(fmt.Sprintf("Unable to modify %q using this API", dataType)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := io.ReadAll(req.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !json.Valid(body) {
|
if !json.Valid(body) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("Bad JSON content"),
|
JSON: jsonerror.BadJSON("Bad JSON content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,16 +142,8 @@ func SaveReadMarker(
|
||||||
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("userID for this device is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the user is a member of this room
|
// Verify that the user is a member of this room
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -165,34 +154,33 @@ func SaveReadMarker(
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.FullyRead != "" {
|
if r.FullyRead == "" {
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
return util.JSONResponse{
|
||||||
if err != nil {
|
Code: http.StatusBadRequest,
|
||||||
return util.JSONResponse{
|
JSON: jsonerror.BadJSON("Missing m.fully_read mandatory field"),
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataReq := api.InputAccountDataRequest{
|
|
||||||
UserID: device.UserID,
|
|
||||||
DataType: "m.fully_read",
|
|
||||||
RoomID: roomID,
|
|
||||||
AccountData: data,
|
|
||||||
}
|
|
||||||
dataRes := api.InputAccountDataResponse{}
|
|
||||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the read receipts that may be included in the read marker.
|
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||||
|
if err != nil {
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
dataReq := api.InputAccountDataRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
DataType: "m.fully_read",
|
||||||
|
RoomID: roomID,
|
||||||
|
AccountData: data,
|
||||||
|
}
|
||||||
|
dataRes := api.InputAccountDataResponse{}
|
||||||
|
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle the read receipt that may be included in the read marker
|
||||||
if r.Read != "" {
|
if r.Read != "" {
|
||||||
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read", r.Read)
|
return SetReceipt(req, syncProducer, device, roomID, "m.read", r.Read)
|
||||||
}
|
|
||||||
if r.ReadPrivate != "" {
|
|
||||||
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read.private", r.ReadPrivate)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -1,401 +1,132 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"golang.org/x/exp/constraints"
|
|
||||||
|
|
||||||
clientapi "github.com/matrix-org/dendrite/clientapi/api"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
var validRegistrationTokenRegex = regexp.MustCompile("^[[:ascii:][:digit:]_]*$")
|
func AdminEvacuateRoom(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
if err != nil {
|
||||||
if !cfg.RegistrationRequiresToken {
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
roomID, ok := vars["roomID"]
|
||||||
|
if !ok {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Forbidden("Registration via tokens is not enabled on this homeserver"),
|
JSON: jsonerror.MissingArgument("Expecting room ID."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := &roomserverAPI.PerformAdminEvacuateRoomResponse{}
|
||||||
|
if err := rsAPI.PerformAdminEvacuateRoom(
|
||||||
|
req.Context(),
|
||||||
|
&roomserverAPI.PerformAdminEvacuateRoomRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
},
|
||||||
|
res,
|
||||||
|
); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
if err := res.Error; err != nil {
|
||||||
|
return err.JSONResponse()
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"affected": res.Affected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminEvacuateUser(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
userID, ok := vars["userID"]
|
||||||
|
if !ok {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.MissingArgument("Expecting user ID."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
|
if err != nil {
|
||||||
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
|
}
|
||||||
|
if domain != cfg.Matrix.ServerName {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.MissingArgument("User ID must belong to this server."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
res := &roomserverAPI.PerformAdminEvacuateUserResponse{}
|
||||||
|
if err := rsAPI.PerformAdminEvacuateUser(
|
||||||
|
req.Context(),
|
||||||
|
&roomserverAPI.PerformAdminEvacuateUserRequest{
|
||||||
|
UserID: userID,
|
||||||
|
},
|
||||||
|
res,
|
||||||
|
); err != nil {
|
||||||
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
|
}
|
||||||
|
if err := res.Error; err != nil {
|
||||||
|
return err.JSONResponse()
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: map[string]interface{}{
|
||||||
|
"affected": res.Affected,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
localpart, ok := vars["localpart"]
|
||||||
|
if !ok {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.MissingArgument("Expecting user localpart."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request := struct {
|
request := struct {
|
||||||
Token string `json:"token"`
|
Password string `json:"password"`
|
||||||
UsesAllowed *int32 `json:"uses_allowed,omitempty"`
|
|
||||||
ExpiryTime *int64 `json:"expiry_time,omitempty"`
|
|
||||||
Length int32 `json:"length"`
|
|
||||||
}{}
|
}{}
|
||||||
|
|
||||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
JSON: jsonerror.Unknown("Failed to decode request body: " + err.Error()),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
token := request.Token
|
|
||||||
usesAllowed := request.UsesAllowed
|
|
||||||
expiryTime := request.ExpiryTime
|
|
||||||
length := request.Length
|
|
||||||
|
|
||||||
if len(token) == 0 {
|
|
||||||
if length == 0 {
|
|
||||||
// length not provided in request. Assign default value of 16.
|
|
||||||
length = 16
|
|
||||||
}
|
|
||||||
// token not present in request body. Hence, generate a random token.
|
|
||||||
if length <= 0 || length > 64 {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("length must be greater than zero and not greater than 64"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
token = util.RandomString(int(length))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(token) > 64 {
|
|
||||||
//Token present in request body, but is too long.
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("token must not be longer than 64"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isTokenValid := validRegistrationTokenRegex.Match([]byte(token))
|
|
||||||
if !isTokenValid {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("token must consist only of characters matched by the regex [A-Za-z0-9-_]"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// At this point, we have a valid token, either through request body or through random generation.
|
|
||||||
if usesAllowed != nil && *usesAllowed < 0 {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pending := int32(0)
|
|
||||||
completed := int32(0)
|
|
||||||
// If usesAllowed or expiryTime is 0, it means they are not present in the request. NULL (indicating unlimited uses / no expiration will be persisted in DB)
|
|
||||||
registrationToken := &clientapi.RegistrationToken{
|
|
||||||
Token: &token,
|
|
||||||
UsesAllowed: usesAllowed,
|
|
||||||
Pending: &pending,
|
|
||||||
Completed: &completed,
|
|
||||||
ExpiryTime: expiryTime,
|
|
||||||
}
|
|
||||||
created, err := userAPI.PerformAdminCreateRegistrationToken(req.Context(), registrationToken)
|
|
||||||
if !created {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusConflict,
|
|
||||||
JSON: map[string]string{
|
|
||||||
"error": fmt.Sprintf("token: %s already exists", token),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: map[string]interface{}{
|
|
||||||
"token": token,
|
|
||||||
"uses_allowed": getReturnValue(usesAllowed),
|
|
||||||
"pending": pending,
|
|
||||||
"completed": completed,
|
|
||||||
"expiry_time": getReturnValue(expiryTime),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getReturnValue[t constraints.Integer](in *t) any {
|
|
||||||
if in == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return *in
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminListRegistrationTokens(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
|
||||||
queryParams := req.URL.Query()
|
|
||||||
returnAll := true
|
|
||||||
valid := true
|
|
||||||
validQuery, ok := queryParams["valid"]
|
|
||||||
if ok {
|
|
||||||
returnAll = false
|
|
||||||
validValue, err := strconv.ParseBool(validQuery[0])
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("invalid 'valid' query parameter"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
valid = validValue
|
|
||||||
}
|
|
||||||
tokens, err := userAPI.PerformAdminListRegistrationTokens(req.Context(), returnAll, valid)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.ErrorUnknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: map[string]interface{}{
|
|
||||||
"registration_tokens": tokens,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminGetRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
tokenText := vars["token"]
|
|
||||||
token, err := userAPI.PerformAdminGetRegistrationToken(req.Context(), tokenText)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: token,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminDeleteRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
tokenText := vars["token"]
|
|
||||||
err = userAPI.PerformAdminDeleteRegistrationToken(req.Context(), tokenText)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: map[string]interface{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminUpdateRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
tokenText := vars["token"]
|
|
||||||
request := make(map[string]*int64)
|
|
||||||
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newAttributes := make(map[string]interface{})
|
|
||||||
usesAllowed, ok := request["uses_allowed"]
|
|
||||||
if ok {
|
|
||||||
// Only add usesAllowed to newAtrributes if it is present and valid
|
|
||||||
if usesAllowed != nil && *usesAllowed < 0 {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("uses_allowed must be a non-negative integer or null"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newAttributes["usesAllowed"] = usesAllowed
|
|
||||||
}
|
|
||||||
expiryTime, ok := request["expiry_time"]
|
|
||||||
if ok {
|
|
||||||
// Only add expiryTime to newAtrributes if it is present and valid
|
|
||||||
if expiryTime != nil && spec.Timestamp(*expiryTime).Time().Before(time.Now()) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("expiry_time must not be in the past"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newAttributes["expiryTime"] = expiryTime
|
|
||||||
}
|
|
||||||
if len(newAttributes) == 0 {
|
|
||||||
// No attributes to update. Return existing token
|
|
||||||
return AdminGetRegistrationToken(req, cfg, userAPI)
|
|
||||||
}
|
|
||||||
updatedToken, err := userAPI.PerformAdminUpdateRegistrationToken(req.Context(), tokenText, newAttributes)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound(fmt.Sprintf("token: %s not found", tokenText)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: *updatedToken,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminEvacuateRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
affected, err := rsAPI.PerformAdminEvacuateRoom(req.Context(), vars["roomID"])
|
|
||||||
switch err.(type) {
|
|
||||||
case nil:
|
|
||||||
case eventutil.ErrRoomNoExists:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound(err.Error()),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
logrus.WithError(err).WithField("roomID", vars["roomID"]).Error("Failed to evacuate room")
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: map[string]interface{}{
|
|
||||||
"affected": affected,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminEvacuateUser(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
affected, err := rsAPI.PerformAdminEvacuateUser(req.Context(), vars["userID"])
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).WithField("userID", vars["userID"]).Error("Failed to evacuate user")
|
|
||||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: map[string]interface{}{
|
|
||||||
"affected": affected,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminPurgeRoom(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = rsAPI.PerformAdminPurgeRoom(context.Background(), vars["roomID"]); err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.Device, userAPI api.ClientUserAPI) util.JSONResponse {
|
|
||||||
if req.Body == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown("Missing request body"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
var localpart string
|
|
||||||
userID := vars["userID"]
|
|
||||||
localpart, serverName, err := cfg.Matrix.SplitLocalID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
accAvailableResp := &api.QueryAccountAvailabilityResponse{}
|
|
||||||
if err = userAPI.QueryAccountAvailability(req.Context(), &api.QueryAccountAvailabilityRequest{
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
}, accAvailableResp); err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if accAvailableResp.Available {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.Unknown("User does not exist"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request := struct {
|
|
||||||
Password string `json:"password"`
|
|
||||||
LogoutDevices bool `json:"logout_devices"`
|
|
||||||
}{}
|
|
||||||
if err = json.NewDecoder(req.Body).Decode(&request); err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown("Failed to decode request body: " + err.Error()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if request.Password == "" {
|
if request.Password == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam("Expecting non-empty password."),
|
JSON: jsonerror.MissingArgument("Expecting non-empty password."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
updateReq := &userapi.PerformPasswordUpdateRequest{
|
||||||
if err = internal.ValidatePassword(request.Password); err != nil {
|
|
||||||
return *internal.PasswordResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateReq := &api.PerformPasswordUpdateRequest{
|
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: serverName,
|
|
||||||
Password: request.Password,
|
Password: request.Password,
|
||||||
LogoutDevices: request.LogoutDevices,
|
LogoutDevices: true,
|
||||||
}
|
}
|
||||||
updateRes := &api.PerformPasswordUpdateResponse{}
|
updateRes := &userapi.PerformPasswordUpdateResponse{}
|
||||||
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown("Failed to perform password update: " + err.Error()),
|
JSON: jsonerror.Unknown("Failed to perform password update: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -407,181 +138,3 @@ func AdminResetPassword(req *http.Request, cfg *config.ClientAPI, device *api.De
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func AdminReindex(req *http.Request, cfg *config.ClientAPI, device *api.Device, natsClient *nats.Conn) util.JSONResponse {
|
|
||||||
_, err := natsClient.RequestMsg(nats.NewMsg(cfg.Matrix.JetStream.Prefixed(jetstream.InputFulltextReindex)), time.Second*10)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to publish nats message")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminMarkAsStale(req *http.Request, cfg *config.ClientAPI, keyAPI api.ClientKeyAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
userID := vars["userID"]
|
|
||||||
|
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
if cfg.Matrix.IsLocalServerName(domain) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("Can not mark local device list as stale"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = keyAPI.PerformMarkAsStaleIfNeeded(req.Context(), &api.PerformMarkAsStaleRequest{
|
|
||||||
UserID: userID,
|
|
||||||
Domain: domain,
|
|
||||||
}, &struct{}{})
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown(fmt.Sprintf("Failed to mark device list as stale: %s", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func AdminDownloadState(req *http.Request, device *api.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
|
||||||
if err != nil {
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
roomID, ok := vars["roomID"]
|
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.MissingParam("Expecting room ID."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
serverName, ok := vars["serverName"]
|
|
||||||
if !ok {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.MissingParam("Expecting remote server name."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = rsAPI.PerformAdminDownloadState(req.Context(), roomID, device.UserID, spec.ServerName(serverName)); err != nil {
|
|
||||||
if errors.Is(err, eventutil.ErrRoomNoExists{}) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: spec.NotFound(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logrus.WithError(err).WithFields(logrus.Fields{
|
|
||||||
"userID": device.UserID,
|
|
||||||
"serverName": serverName,
|
|
||||||
"roomID": roomID,
|
|
||||||
}).Error("failed to download state")
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetEventReports returns reported events for a given user/room.
|
|
||||||
func GetEventReports(
|
|
||||||
req *http.Request,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
||||||
from, limit uint64,
|
|
||||||
backwards bool,
|
|
||||||
userID, roomID string,
|
|
||||||
) util.JSONResponse {
|
|
||||||
|
|
||||||
eventReports, count, err := rsAPI.QueryAdminEventReports(req.Context(), from, limit, backwards, userID, roomID)
|
|
||||||
if err != nil {
|
|
||||||
logrus.WithError(err).Error("failed to query event reports")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := map[string]any{
|
|
||||||
"event_reports": eventReports,
|
|
||||||
"total": count,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a next_token if there are still reports
|
|
||||||
if int64(from+limit) < count {
|
|
||||||
resp["next_token"] = int(from) + len(eventReports)
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: resp,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
|
||||||
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
// Given this is an admin endpoint, let them know what didn't work.
|
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
report, err := rsAPI.QueryAdminEventReport(req.Context(), parsedReportID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: report,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteEventReport(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, reportID string) util.JSONResponse {
|
|
||||||
parsedReportID, err := strconv.ParseUint(reportID, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
// Given this is an admin endpoint, let them know what didn't work.
|
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = rsAPI.PerformAdminDeleteEventReport(req.Context(), parsedReportID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseUint64OrDefault(input string, defaultValue uint64) uint64 {
|
|
||||||
v, err := strconv.ParseUint(input, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
|
@ -17,8 +17,8 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -51,7 +51,7 @@ func GetAdminWhois(
|
||||||
if !allowed {
|
if !allowed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID does not match the current user"),
|
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,10 +61,7 @@ func GetAdminWhois(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
devices := make(map[string]deviceInfo)
|
devices := make(map[string]deviceInfo)
|
||||||
|
|
|
@ -15,14 +15,14 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ func GetAliases(
|
||||||
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
stateTuple := gomatrixserverlib.StateKeyTuple{
|
stateTuple := gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: spec.MRoomHistoryVisibility,
|
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}
|
}
|
||||||
stateReq := &api.QueryCurrentStateRequest{
|
stateReq := &api.QueryCurrentStateRequest{
|
||||||
|
@ -47,37 +47,26 @@ func GetAliases(
|
||||||
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
||||||
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
||||||
var err error
|
var err error
|
||||||
var content gomatrixserverlib.HistoryVisibilityContent
|
visibility, err = historyVisEvent.HistoryVisibility()
|
||||||
if err = json.Unmarshal(historyVisEvent.Content(), &content); err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
||||||
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
return util.ErrorResponse(fmt.Errorf("historyVisEvent.HistoryVisibility: %w", err))
|
||||||
}
|
}
|
||||||
visibility = content.HistoryVisibility
|
|
||||||
}
|
}
|
||||||
if visibility != spec.WorldReadable {
|
if visibility != gomatrixserverlib.WorldReadable {
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
queryReq := api.QueryMembershipForUserRequest{
|
queryReq := api.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: *deviceUserID,
|
UserID: device.UserID,
|
||||||
}
|
}
|
||||||
var queryRes api.QueryMembershipForUserResponse
|
var queryRes api.QueryMembershipForUserResponse
|
||||||
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !queryRes.IsInRoom {
|
if !queryRes.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("You aren't a member of this room."),
|
JSON: jsonerror.Forbidden("You aren't a member of this room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
@ -31,7 +31,8 @@ const recaptchaTemplate = `
|
||||||
<title>Authentication</title>
|
<title>Authentication</title>
|
||||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||||
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'>
|
||||||
<script src="{{.apiJsUrl}}" async defer></script>
|
<script src="https://www.google.com/recaptcha/api.js"
|
||||||
|
async defer></script>
|
||||||
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
|
<script src="//code.jquery.com/jquery-1.11.2.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
function captchaDone() {
|
function captchaDone() {
|
||||||
|
@ -50,8 +51,8 @@ function captchaDone() {
|
||||||
Please verify that you're not a robot.
|
Please verify that you're not a robot.
|
||||||
</p>
|
</p>
|
||||||
<input type="hidden" name="session" value="{{.session}}" />
|
<input type="hidden" name="session" value="{{.session}}" />
|
||||||
<div class="{{.sitekeyClass}}"
|
<div class="g-recaptcha"
|
||||||
data-sitekey="{{.sitekey}}"
|
data-sitekey="{{.siteKey}}"
|
||||||
data-callback="captchaDone">
|
data-callback="captchaDone">
|
||||||
</div>
|
</div>
|
||||||
<noscript>
|
<noscript>
|
||||||
|
@ -101,38 +102,21 @@ func serveTemplate(w http.ResponseWriter, templateHTML string, data map[string]s
|
||||||
func AuthFallback(
|
func AuthFallback(
|
||||||
w http.ResponseWriter, req *http.Request, authType string,
|
w http.ResponseWriter, req *http.Request, authType string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) {
|
) *util.JSONResponse {
|
||||||
// We currently only support "m.login.recaptcha", so fail early if that's not requested
|
|
||||||
if authType == authtypes.LoginTypeRecaptcha {
|
|
||||||
if !cfg.RecaptchaEnabled {
|
|
||||||
writeHTTPMessage(w, req,
|
|
||||||
"Recaptcha login is disabled on this Homeserver",
|
|
||||||
http.StatusBadRequest,
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
writeHTTPMessage(w, req, fmt.Sprintf("Unknown authtype %q", authType), http.StatusNotImplemented)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionID := req.URL.Query().Get("session")
|
sessionID := req.URL.Query().Get("session")
|
||||||
|
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
writeHTTPMessage(w, req,
|
return writeHTTPMessage(w, req,
|
||||||
"Session ID not provided",
|
"Session ID not provided",
|
||||||
http.StatusBadRequest,
|
http.StatusBadRequest,
|
||||||
)
|
)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
serveRecaptcha := func() {
|
serveRecaptcha := func() {
|
||||||
data := map[string]string{
|
data := map[string]string{
|
||||||
"myUrl": req.URL.String(),
|
"myUrl": req.URL.String(),
|
||||||
"session": sessionID,
|
"session": sessionID,
|
||||||
"apiJsUrl": cfg.RecaptchaApiJsUrl,
|
"siteKey": cfg.RecaptchaPublicKey,
|
||||||
"sitekey": cfg.RecaptchaPublicKey,
|
|
||||||
"sitekeyClass": cfg.RecaptchaSitekeyClass,
|
|
||||||
"formField": cfg.RecaptchaFormField,
|
|
||||||
}
|
}
|
||||||
serveTemplate(w, recaptchaTemplate, data)
|
serveTemplate(w, recaptchaTemplate, data)
|
||||||
}
|
}
|
||||||
|
@ -144,44 +128,70 @@ func AuthFallback(
|
||||||
|
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
// Handle Recaptcha
|
// Handle Recaptcha
|
||||||
serveRecaptcha()
|
if authType == authtypes.LoginTypeRecaptcha {
|
||||||
return
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
serveRecaptcha()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
||||||
|
}
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
// Handle Recaptcha
|
// Handle Recaptcha
|
||||||
clientIP := req.RemoteAddr
|
if authType == authtypes.LoginTypeRecaptcha {
|
||||||
err := req.ParseForm()
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
}
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
serveRecaptcha()
|
clientIP := req.RemoteAddr
|
||||||
return
|
err := req.ParseForm()
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
||||||
|
res := jsonerror.InternalServerError()
|
||||||
|
return &res
|
||||||
|
}
|
||||||
|
|
||||||
|
response := req.Form.Get("g-recaptcha-response")
|
||||||
|
if err := validateRecaptcha(cfg, response, clientIP); err != nil {
|
||||||
|
util.GetLogger(req.Context()).Error(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success. Add recaptcha as a completed login flow
|
||||||
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||||
|
|
||||||
|
serveSuccess()
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
response := req.Form.Get(cfg.RecaptchaFormField)
|
return &util.JSONResponse{
|
||||||
err = validateRecaptcha(cfg, response, clientIP)
|
Code: http.StatusNotFound,
|
||||||
switch err {
|
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
||||||
case ErrMissingResponse:
|
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
|
||||||
serveRecaptcha() // serve the initial page again, instead of nothing
|
|
||||||
return
|
|
||||||
case ErrInvalidCaptcha:
|
|
||||||
w.WriteHeader(http.StatusUnauthorized)
|
|
||||||
serveRecaptcha()
|
|
||||||
return
|
|
||||||
case nil:
|
|
||||||
default: // something else failed
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
|
||||||
serveRecaptcha()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Success. Add recaptcha as a completed login flow
|
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
|
||||||
|
|
||||||
serveSuccess()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
writeHTTPMessage(w, req, "Bad method", http.StatusMethodNotAllowed)
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusMethodNotAllowed,
|
||||||
|
JSON: jsonerror.NotFound("Bad method"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRecaptchaEnabled creates an error response if recaptcha is not usable on homeserver.
|
||||||
|
func checkRecaptchaEnabled(
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
w http.ResponseWriter,
|
||||||
|
req *http.Request,
|
||||||
|
) *util.JSONResponse {
|
||||||
|
if !cfg.RecaptchaEnabled {
|
||||||
|
return writeHTTPMessage(w, req,
|
||||||
|
"Recaptcha login is disabled on this Homeserver",
|
||||||
|
http.StatusBadRequest,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeHTTPMessage writes the given header and message to the HTTP response writer.
|
// writeHTTPMessage writes the given header and message to the HTTP response writer.
|
||||||
|
@ -189,10 +199,13 @@ func AuthFallback(
|
||||||
func writeHTTPMessage(
|
func writeHTTPMessage(
|
||||||
w http.ResponseWriter, req *http.Request,
|
w http.ResponseWriter, req *http.Request,
|
||||||
message string, header int,
|
message string, header int,
|
||||||
) {
|
) *util.JSONResponse {
|
||||||
w.WriteHeader(header)
|
w.WriteHeader(header)
|
||||||
_, err := w.Write([]byte(message))
|
_, err := w.Write([]byte(message))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("w.Write failed")
|
util.GetLogger(req.Context()).WithError(err).Error("w.Write failed")
|
||||||
|
res := jsonerror.InternalServerError()
|
||||||
|
return &res
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Test_AuthFallback(t *testing.T) {
|
|
||||||
cfg := config.Dendrite{}
|
|
||||||
cfg.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
|
||||||
for _, useHCaptcha := range []bool{false, true} {
|
|
||||||
for _, recaptchaEnabled := range []bool{false, true} {
|
|
||||||
for _, wantErr := range []bool{false, true} {
|
|
||||||
t.Run(fmt.Sprintf("useHCaptcha(%v) - recaptchaEnabled(%v) - wantErr(%v)", useHCaptcha, recaptchaEnabled, wantErr), func(t *testing.T) {
|
|
||||||
// Set the defaults for each test
|
|
||||||
cfg.ClientAPI.Defaults(config.DefaultOpts{Generate: true, SingleDatabase: true})
|
|
||||||
cfg.ClientAPI.RecaptchaEnabled = recaptchaEnabled
|
|
||||||
cfg.ClientAPI.RecaptchaPublicKey = "pub"
|
|
||||||
cfg.ClientAPI.RecaptchaPrivateKey = "priv"
|
|
||||||
if useHCaptcha {
|
|
||||||
cfg.ClientAPI.RecaptchaSiteVerifyAPI = "https://hcaptcha.com/siteverify"
|
|
||||||
cfg.ClientAPI.RecaptchaApiJsUrl = "https://js.hcaptcha.com/1/api.js"
|
|
||||||
cfg.ClientAPI.RecaptchaFormField = "h-captcha-response"
|
|
||||||
cfg.ClientAPI.RecaptchaSitekeyClass = "h-captcha"
|
|
||||||
}
|
|
||||||
cfgErrs := &config.ConfigErrors{}
|
|
||||||
cfg.ClientAPI.Verify(cfgErrs)
|
|
||||||
if len(*cfgErrs) > 0 {
|
|
||||||
t.Fatalf("(hCaptcha=%v) unexpected config errors: %s", useHCaptcha, cfgErrs.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/?session=1337", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if !recaptchaEnabled {
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
if rec.Body.String() != "Recaptcha login is disabled on this Homeserver" {
|
|
||||||
t.Fatalf("unexpected response body: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if !strings.Contains(rec.Body.String(), cfg.ClientAPI.RecaptchaSitekeyClass) {
|
|
||||||
t.Fatalf("body does not contain %s: %s", cfg.ClientAPI.RecaptchaSitekeyClass, rec.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if wantErr {
|
|
||||||
_, _ = w.Write([]byte(`{"success":false}`))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
_, _ = w.Write([]byte(`{"success":true}`))
|
|
||||||
}))
|
|
||||||
defer srv.Close() // nolint: errcheck
|
|
||||||
|
|
||||||
cfg.ClientAPI.RecaptchaSiteVerifyAPI = srv.URL
|
|
||||||
|
|
||||||
// check the result after sending the captcha
|
|
||||||
req = httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
|
||||||
req.Form = url.Values{}
|
|
||||||
req.Form.Add(cfg.ClientAPI.RecaptchaFormField, "someRandomValue")
|
|
||||||
rec = httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if recaptchaEnabled {
|
|
||||||
if !wantErr {
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusOK)
|
|
||||||
}
|
|
||||||
if rec.Body.String() != successTemplate {
|
|
||||||
t.Fatalf("unexpected response: %s, want %s", rec.Body.String(), successTemplate)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if rec.Code != http.StatusUnauthorized {
|
|
||||||
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusUnauthorized)
|
|
||||||
}
|
|
||||||
wantString := "Authentication"
|
|
||||||
if !strings.Contains(rec.Body.String(), wantString) {
|
|
||||||
t.Fatalf("expected response to contain '%s', but didn't: %s", wantString, rec.Body.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("unexpected response code: %d, want %d", rec.Code, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
if rec.Body.String() != "Recaptcha login is disabled on this Homeserver" {
|
|
||||||
t.Fatalf("unexpected response: %s, want %s", rec.Body.String(), "successTemplate")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("unknown fallbacks are handled correctly", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, "DoesNotExist", &cfg.ClientAPI)
|
|
||||||
if rec.Code != http.StatusNotImplemented {
|
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusNotImplemented)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("unknown methods are handled correctly", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/?session=1337", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if rec.Code != http.StatusMethodNotAllowed {
|
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusMethodNotAllowed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("missing session parameter is handled correctly", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("missing 'response' is handled correctly", func(t *testing.T) {
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/?session=1337", nil)
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
AuthFallback(rec, req, authtypes.LoginTypeRecaptcha, &cfg.ClientAPI)
|
|
||||||
if rec.Code != http.StatusBadRequest {
|
|
||||||
t.Fatalf("unexpected http status: %d, want %d", rec.Code, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -17,22 +17,26 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/version"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetCapabilities returns information about the server's supported feature set
|
// GetCapabilities returns information about the server's supported feature set
|
||||||
// and other relevant capabilities to an authenticated user.
|
// and other relevant capabilities to an authenticated user.
|
||||||
func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
func GetCapabilities(
|
||||||
versionsMap := map[gomatrixserverlib.RoomVersion]string{}
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
for v, desc := range version.SupportedRoomVersions() {
|
) util.JSONResponse {
|
||||||
if desc.Stable() {
|
roomVersionsQueryReq := roomserverAPI.QueryRoomVersionCapabilitiesRequest{}
|
||||||
versionsMap[v] = "stable"
|
roomVersionsQueryRes := roomserverAPI.QueryRoomVersionCapabilitiesResponse{}
|
||||||
} else {
|
if err := rsAPI.QueryRoomVersionCapabilities(
|
||||||
versionsMap[v] = "unstable"
|
req.Context(),
|
||||||
}
|
&roomVersionsQueryReq,
|
||||||
|
&roomVersionsQueryRes,
|
||||||
|
); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryRoomVersionCapabilities failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
response := map[string]interface{}{
|
response := map[string]interface{}{
|
||||||
|
@ -40,10 +44,7 @@ func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse
|
||||||
"m.change_password": map[string]bool{
|
"m.change_password": map[string]bool{
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
},
|
},
|
||||||
"m.room_versions": map[string]interface{}{
|
"m.room_versions": roomVersionsQueryRes,
|
||||||
"default": rsAPI.DefaultRoomVersion(),
|
|
||||||
"available": versionsMap,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,9 +26,10 @@ import (
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
@ -37,19 +38,33 @@ import (
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||||
type createRoomRequest struct {
|
type createRoomRequest struct {
|
||||||
Invite []string `json:"invite"`
|
Invite []string `json:"invite"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Preset string `json:"preset"`
|
Preset string `json:"preset"`
|
||||||
CreationContent json.RawMessage `json:"creation_content"`
|
CreationContent json.RawMessage `json:"creation_content"`
|
||||||
InitialState []gomatrixserverlib.FledglingEvent `json:"initial_state"`
|
InitialState []fledglingEvent `json:"initial_state"`
|
||||||
RoomAliasName string `json:"room_alias_name"`
|
RoomAliasName string `json:"room_alias_name"`
|
||||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
GuestCanJoin bool `json:"guest_can_join"`
|
||||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||||
IsDirect bool `json:"is_direct"`
|
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
||||||
|
IsDirect bool `json:"is_direct"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
presetPrivateChat = "private_chat"
|
||||||
|
presetTrustedPrivateChat = "trusted_private_chat"
|
||||||
|
presetPublicChat = "public_chat"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
historyVisibilityShared = "shared"
|
||||||
|
// TODO: These should be implemented once history visibility is implemented
|
||||||
|
// historyVisibilityWorldReadable = "world_readable"
|
||||||
|
// historyVisibilityInvited = "invited"
|
||||||
|
)
|
||||||
|
|
||||||
func (r createRoomRequest) Validate() *util.JSONResponse {
|
func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
|
||||||
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
|
||||||
|
@ -57,23 +72,28 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
JSON: jsonerror.BadJSON("room_alias_name cannot contain whitespace or ':'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, userID := range r.Invite {
|
for _, userID := range r.Invite {
|
||||||
if _, err := spec.NewUserID(userID, true); err != nil {
|
// TODO: We should put user ID parsing code into gomatrixserverlib and use that instead
|
||||||
|
// (see https://github.com/matrix-org/gomatrixserverlib/blob/3394e7c7003312043208aa73727d2256eea3d1f6/eventcontent.go#L347 )
|
||||||
|
// It should be a struct (with pointers into a single string to avoid copying) and
|
||||||
|
// we should update all refs to use UserID types rather than strings.
|
||||||
|
// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/types.py#L92
|
||||||
|
if _, _, err := gomatrixserverlib.SplitID('@', userID); err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("user id must be in the form @localpart:domain"),
|
JSON: jsonerror.BadJSON("user id must be in the form @localpart:domain"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
switch r.Preset {
|
switch r.Preset {
|
||||||
case spec.PresetPrivateChat, spec.PresetTrustedPrivateChat, spec.PresetPublicChat, "":
|
case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
|
||||||
default:
|
default:
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
JSON: jsonerror.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +105,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("malformed creation_content"),
|
JSON: jsonerror.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,7 +114,7 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("malformed creation_content"),
|
JSON: jsonerror.BadJSON("malformed creation_content"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +127,13 @@ type createRoomResponse struct {
|
||||||
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fledglingEvent is a helper representation of an event used when creating many events in succession.
|
||||||
|
type fledglingEvent struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
StateKey string `json:"state_key"`
|
||||||
|
Content interface{} `json:"content"`
|
||||||
|
}
|
||||||
|
|
||||||
// CreateRoom implements /createRoom
|
// CreateRoom implements /createRoom
|
||||||
func CreateRoom(
|
func CreateRoom(
|
||||||
req *http.Request, device *api.Device,
|
req *http.Request, device *api.Device,
|
||||||
|
@ -114,124 +141,444 @@ func CreateRoom(
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var createRequest createRoomRequest
|
var r createRoomRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &createRequest)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if resErr = createRequest.Validate(); resErr != nil {
|
if resErr = r.Validate(); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return createRoom(req.Context(), createRequest, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
return createRoom(req.Context(), r, device, cfg, profileAPI, rsAPI, asAPI, evTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// createRoom implements /createRoom
|
// createRoom implements /createRoom
|
||||||
|
// nolint: gocyclo
|
||||||
func createRoom(
|
func createRoom(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
createRequest createRoomRequest, device *api.Device,
|
r createRoomRequest, device *api.Device,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
evTime time.Time,
|
evTime time.Time,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
// TODO (#267): Check room ID doesn't clash with an existing one, and we
|
||||||
if err != nil {
|
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
||||||
util.GetLogger(ctx).WithError(err).Error("invalid userID")
|
roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName)
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !cfg.Matrix.IsLocalServerName(userID.Domain()) {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden(fmt.Sprintf("User domain %q not configured locally", userID.Domain())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := util.GetLogger(ctx)
|
logger := util.GetLogger(ctx)
|
||||||
|
userID := device.UserID
|
||||||
// TODO: Check room ID doesn't clash with an existing one, and we
|
|
||||||
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
|
||||||
roomID, err := spec.NewRoomID(fmt.Sprintf("!%s:%s", util.RandomString(16), userID.Domain()))
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("invalid roomID")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clobber keys: creator, room_version
|
// Clobber keys: creator, room_version
|
||||||
|
|
||||||
roomVersion := rsAPI.DefaultRoomVersion()
|
roomVersion := roomserverVersion.DefaultRoomVersion()
|
||||||
if createRequest.RoomVersion != "" {
|
if r.RoomVersion != "" {
|
||||||
candidateVersion := gomatrixserverlib.RoomVersion(createRequest.RoomVersion)
|
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
||||||
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
||||||
if roomVersionError != nil {
|
if roomVersionError != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.UnsupportedRoomVersion(roomVersionError.Error()),
|
JSON: jsonerror.UnsupportedRoomVersion(roomVersionError.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
roomVersion = candidateVersion
|
roomVersion = candidateVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: visibility/presets/raw initial state
|
||||||
|
// TODO: Create room alias association
|
||||||
|
// Make sure this doesn't fall into an application service's namespace though!
|
||||||
|
|
||||||
logger.WithFields(log.Fields{
|
logger.WithFields(log.Fields{
|
||||||
"userID": userID.String(),
|
"userID": userID,
|
||||||
"roomID": roomID.String(),
|
"roomID": roomID,
|
||||||
"roomVersion": roomVersion,
|
"roomVersion": roomVersion,
|
||||||
}).Info("Creating new room")
|
}).Info("Creating new room")
|
||||||
|
|
||||||
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID.String(), asAPI, profileAPI)
|
profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
}
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
|
createContent := map[string]interface{}{}
|
||||||
|
if len(r.CreationContent) > 0 {
|
||||||
|
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("invalid create content"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createContent["creator"] = userID
|
||||||
|
createContent["room_version"] = roomVersion
|
||||||
|
powerLevelContent := eventutil.InitialPowerLevelsContent(userID)
|
||||||
|
joinRuleContent := gomatrixserverlib.JoinRuleContent{
|
||||||
|
JoinRule: gomatrixserverlib.Invite,
|
||||||
|
}
|
||||||
|
historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{
|
||||||
|
HistoryVisibility: historyVisibilityShared,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.PowerLevelContentOverride != nil {
|
||||||
|
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
||||||
|
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userDisplayName := profile.DisplayName
|
switch r.Preset {
|
||||||
userAvatarURL := profile.AvatarURL
|
case presetPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
keyID := cfg.Matrix.KeyID
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
privateKey := cfg.Matrix.PrivateKey
|
case presetTrustedPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
req := roomserverAPI.PerformCreateRoomRequest{
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
InvitedUsers: createRequest.Invite,
|
for _, invitee := range r.Invite {
|
||||||
RoomName: createRequest.Name,
|
powerLevelContent.Users[invitee] = 100
|
||||||
Visibility: createRequest.Visibility,
|
}
|
||||||
Topic: createRequest.Topic,
|
case presetPublicChat:
|
||||||
StatePreset: createRequest.Preset,
|
joinRuleContent.JoinRule = gomatrixserverlib.Public
|
||||||
CreationContent: createRequest.CreationContent,
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
InitialState: createRequest.InitialState,
|
|
||||||
RoomAliasName: createRequest.RoomAliasName,
|
|
||||||
RoomVersion: roomVersion,
|
|
||||||
PowerLevelContentOverride: createRequest.PowerLevelContentOverride,
|
|
||||||
IsDirect: createRequest.IsDirect,
|
|
||||||
|
|
||||||
UserDisplayName: userDisplayName,
|
|
||||||
UserAvatarURL: userAvatarURL,
|
|
||||||
KeyID: keyID,
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
EventTime: evTime,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
roomAlias, createRes := rsAPI.PerformCreateRoom(ctx, *userID, *roomID, &req)
|
createEvent := fledglingEvent{
|
||||||
if createRes != nil {
|
Type: gomatrixserverlib.MRoomCreate,
|
||||||
return *createRes
|
Content: createContent,
|
||||||
|
}
|
||||||
|
powerLevelEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomPowerLevels,
|
||||||
|
Content: powerLevelContent,
|
||||||
|
}
|
||||||
|
joinRuleEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomJoinRules,
|
||||||
|
Content: joinRuleContent,
|
||||||
|
}
|
||||||
|
historyVisibilityEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomHistoryVisibility,
|
||||||
|
Content: historyVisibilityContent,
|
||||||
|
}
|
||||||
|
membershipEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomMember,
|
||||||
|
StateKey: userID,
|
||||||
|
Content: gomatrixserverlib.MemberContent{
|
||||||
|
Membership: gomatrixserverlib.Join,
|
||||||
|
DisplayName: profile.DisplayName,
|
||||||
|
AvatarURL: profile.AvatarURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameEvent *fledglingEvent
|
||||||
|
var topicEvent *fledglingEvent
|
||||||
|
var guestAccessEvent *fledglingEvent
|
||||||
|
var aliasEvent *fledglingEvent
|
||||||
|
|
||||||
|
if r.Name != "" {
|
||||||
|
nameEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomName,
|
||||||
|
Content: eventutil.NameContent{
|
||||||
|
Name: r.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Topic != "" {
|
||||||
|
topicEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomTopic,
|
||||||
|
Content: eventutil.TopicContent{
|
||||||
|
Topic: r.Topic,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.GuestCanJoin {
|
||||||
|
guestAccessEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomGuestAccess,
|
||||||
|
Content: eventutil.GuestAccessContent{
|
||||||
|
GuestAccess: "can_join",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var roomAlias string
|
||||||
|
if r.RoomAliasName != "" {
|
||||||
|
roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, cfg.Matrix.ServerName)
|
||||||
|
// check it's free TODO: This races but is better than nothing
|
||||||
|
hasAliasReq := roomserverAPI.GetRoomIDForAliasRequest{
|
||||||
|
Alias: roomAlias,
|
||||||
|
IncludeAppservices: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliasResp roomserverAPI.GetRoomIDForAliasResponse
|
||||||
|
err = rsAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
if aliasResp.RoomID != "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.RoomInUse("Room ID already exists."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomCanonicalAlias,
|
||||||
|
Content: eventutil.CanonicalAlias{
|
||||||
|
Alias: roomAlias,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var initialStateEvents []fledglingEvent
|
||||||
|
for i := range r.InitialState {
|
||||||
|
if r.InitialState[i].StateKey != "" {
|
||||||
|
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.InitialState[i].Type {
|
||||||
|
case gomatrixserverlib.MRoomCreate:
|
||||||
|
continue
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomPowerLevels:
|
||||||
|
powerLevelEvent = r.InitialState[i]
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomJoinRules:
|
||||||
|
joinRuleEvent = r.InitialState[i]
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomHistoryVisibility:
|
||||||
|
historyVisibilityEvent = r.InitialState[i]
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomGuestAccess:
|
||||||
|
guestAccessEvent = &r.InitialState[i]
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomName:
|
||||||
|
nameEvent = &r.InitialState[i]
|
||||||
|
|
||||||
|
case gomatrixserverlib.MRoomTopic:
|
||||||
|
topicEvent = &r.InitialState[i]
|
||||||
|
|
||||||
|
default:
|
||||||
|
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// send events into the room in order of:
|
||||||
|
// 1- m.room.create
|
||||||
|
// 2- room creator join member
|
||||||
|
// 3- m.room.power_levels
|
||||||
|
// 4- m.room.join_rules
|
||||||
|
// 5- m.room.history_visibility
|
||||||
|
// 6- m.room.canonical_alias (opt)
|
||||||
|
// 7- m.room.guest_access (opt)
|
||||||
|
// 8- other initial state items
|
||||||
|
// 9- m.room.name (opt)
|
||||||
|
// 10- m.room.topic (opt)
|
||||||
|
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
||||||
|
// 12- 3pid invite events (opt) TODO
|
||||||
|
// This differs from Synapse slightly. Synapse would vary the ordering of 3-7
|
||||||
|
// depending on if those events were in "initial_state" or not. This made it
|
||||||
|
// harder to reason about, hence sticking to a strict static ordering.
|
||||||
|
// TODO: Synapse has txn/token ID on each event. Do we need to do this here?
|
||||||
|
eventsToMake := []fledglingEvent{
|
||||||
|
createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent,
|
||||||
|
}
|
||||||
|
if guestAccessEvent != nil {
|
||||||
|
eventsToMake = append(eventsToMake, *guestAccessEvent)
|
||||||
|
}
|
||||||
|
eventsToMake = append(eventsToMake, initialStateEvents...)
|
||||||
|
if nameEvent != nil {
|
||||||
|
eventsToMake = append(eventsToMake, *nameEvent)
|
||||||
|
}
|
||||||
|
if topicEvent != nil {
|
||||||
|
eventsToMake = append(eventsToMake, *topicEvent)
|
||||||
|
}
|
||||||
|
if aliasEvent != nil {
|
||||||
|
// TODO: bit of a chicken and egg problem here as the alias doesn't exist and cannot until we have made the room.
|
||||||
|
// This means we might fail creating the alias but say the canonical alias is something that doesn't exist.
|
||||||
|
eventsToMake = append(eventsToMake, *aliasEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: invite events
|
||||||
|
// TODO: 3pid invite events
|
||||||
|
|
||||||
|
var builtEvents []*gomatrixserverlib.HeaderedEvent
|
||||||
|
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
||||||
|
for i, e := range eventsToMake {
|
||||||
|
depth := i + 1 // depth starts at 1
|
||||||
|
|
||||||
|
builder := gomatrixserverlib.EventBuilder{
|
||||||
|
Sender: userID,
|
||||||
|
RoomID: roomID,
|
||||||
|
Type: e.Type,
|
||||||
|
StateKey: &e.StateKey,
|
||||||
|
Depth: int64(depth),
|
||||||
|
}
|
||||||
|
err = builder.SetContent(e.Content)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
if i > 0 {
|
||||||
|
builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()}
|
||||||
|
}
|
||||||
|
var ev *gomatrixserverlib.Event
|
||||||
|
ev, err = buildEvent(&builder, &authEvents, cfg, evTime, roomVersion)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("buildEvent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the event to the list of auth events
|
||||||
|
builtEvents = append(builtEvents, ev.Headered(roomVersion))
|
||||||
|
err = authEvents.AddEvent(ev)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inputs := make([]roomserverAPI.InputRoomEvent, 0, len(builtEvents))
|
||||||
|
for _, event := range builtEvents {
|
||||||
|
inputs = append(inputs, roomserverAPI.InputRoomEvent{
|
||||||
|
Kind: roomserverAPI.KindNew,
|
||||||
|
Event: event,
|
||||||
|
Origin: cfg.Matrix.ServerName,
|
||||||
|
SendAsServer: roomserverAPI.DoNotSendToOtherServers,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if err = roomserverAPI.SendInputRoomEvents(ctx, rsAPI, inputs, false); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(#269): Reserve room alias while we create the room. This stops us
|
||||||
|
// from creating the room but still failing due to the alias having already
|
||||||
|
// been taken.
|
||||||
|
if roomAlias != "" {
|
||||||
|
aliasReq := roomserverAPI.SetRoomAliasRequest{
|
||||||
|
Alias: roomAlias,
|
||||||
|
RoomID: roomID,
|
||||||
|
UserID: userID,
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliasResp roomserverAPI.SetRoomAliasResponse
|
||||||
|
err = rsAPI.SetRoomAlias(ctx, &aliasReq, &aliasResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliasResp.AliasExists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.RoomInUse("Room alias already exists."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is a direct message then we should invite the participants.
|
||||||
|
if len(r.Invite) > 0 {
|
||||||
|
// Build some stripped state for the invite.
|
||||||
|
var globalStrippedState []gomatrixserverlib.InviteV2StrippedState
|
||||||
|
for _, event := range builtEvents {
|
||||||
|
// Chosen events from the spec:
|
||||||
|
// https://spec.matrix.org/v1.3/client-server-api/#stripped-state
|
||||||
|
switch event.Type() {
|
||||||
|
case gomatrixserverlib.MRoomCreate:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomName:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomAvatar:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomTopic:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomCanonicalAlias:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomEncryption:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomMember:
|
||||||
|
fallthrough
|
||||||
|
case gomatrixserverlib.MRoomJoinRules:
|
||||||
|
ev := event.Event
|
||||||
|
globalStrippedState = append(
|
||||||
|
globalStrippedState,
|
||||||
|
gomatrixserverlib.NewInviteV2StrippedState(ev),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the invites.
|
||||||
|
for _, invitee := range r.Invite {
|
||||||
|
// Build the invite event.
|
||||||
|
inviteEvent, err := buildMembershipEvent(
|
||||||
|
ctx, invitee, "", profileAPI, device, gomatrixserverlib.Invite,
|
||||||
|
roomID, r.IsDirect, cfg, evTime, rsAPI, asAPI,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inviteStrippedState := append(
|
||||||
|
globalStrippedState,
|
||||||
|
gomatrixserverlib.NewInviteV2StrippedState(inviteEvent.Event),
|
||||||
|
)
|
||||||
|
// Send the invite event to the roomserver.
|
||||||
|
var inviteRes roomserverAPI.PerformInviteResponse
|
||||||
|
event := inviteEvent.Headered(roomVersion)
|
||||||
|
if err := rsAPI.PerformInvite(ctx, &roomserverAPI.PerformInviteRequest{
|
||||||
|
Event: event,
|
||||||
|
InviteRoomState: inviteStrippedState,
|
||||||
|
RoomVersion: event.RoomVersion,
|
||||||
|
SendAsServer: string(cfg.Matrix.ServerName),
|
||||||
|
}, &inviteRes); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.InternalServerError(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inviteRes.Error != nil {
|
||||||
|
return inviteRes.Error.JSONResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Visibility == "public" {
|
||||||
|
// expose this room in the published room list
|
||||||
|
var pubRes roomserverAPI.PerformPublishResponse
|
||||||
|
if err := rsAPI.PerformPublish(ctx, &roomserverAPI.PerformPublishRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
Visibility: "public",
|
||||||
|
}, &pubRes); err != nil {
|
||||||
|
return jsonerror.InternalAPIError(ctx, err)
|
||||||
|
}
|
||||||
|
if pubRes.Error != nil {
|
||||||
|
// treat as non-fatal since the room is already made by this point
|
||||||
|
util.GetLogger(ctx).WithError(pubRes.Error).Error("failed to visibility:public")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
response := createRoomResponse{
|
response := createRoomResponse{
|
||||||
RoomID: roomID.String(),
|
RoomID: roomID,
|
||||||
RoomAlias: roomAlias,
|
RoomAlias: roomAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,3 +587,30 @@ func createRoom(
|
||||||
JSON: response,
|
JSON: response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildEvent fills out auth_events for the builder then builds the event
|
||||||
|
func buildEvent(
|
||||||
|
builder *gomatrixserverlib.EventBuilder,
|
||||||
|
provider gomatrixserverlib.AuthEventProvider,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
evTime time.Time,
|
||||||
|
roomVersion gomatrixserverlib.RoomVersion,
|
||||||
|
) (*gomatrixserverlib.Event, error) {
|
||||||
|
eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
refs, err := eventsNeeded.AuthEventReferences(provider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
builder.AuthEvents = refs
|
||||||
|
event, err := builder.Build(
|
||||||
|
evTime, cfg.Matrix.ServerName, cfg.Matrix.KeyID,
|
||||||
|
cfg.Matrix.PrivateKey, roomVersion,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %w", builder.Type, err)
|
||||||
|
}
|
||||||
|
return event, nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,9 +5,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ func Deactivate(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,26 +33,19 @@ func Deactivate(
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, serverName, err := gomatrixserverlib.SplitID('@', login.Username())
|
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var res api.PerformAccountDeactivationResponse
|
var res api.PerformAccountDeactivationResponse
|
||||||
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
err = accountAPI.PerformAccountDeactivation(ctx, &api.PerformAccountDeactivationRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: serverName,
|
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformAccountDeactivation failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -15,16 +15,15 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
)
|
)
|
||||||
|
@ -60,10 +59,7 @@ func GetDeviceByID(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
var targetDevice *api.Device
|
var targetDevice *api.Device
|
||||||
for _, device := range queryRes.Devices {
|
for _, device := range queryRes.Devices {
|
||||||
|
@ -75,7 +71,7 @@ func GetDeviceByID(
|
||||||
if targetDevice == nil {
|
if targetDevice == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound("Unknown device"),
|
JSON: jsonerror.NotFound("Unknown device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,10 +96,7 @@ func GetDevicesByLocalpart(
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryDevices failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res := devicesJSON{}
|
res := devicesJSON{}
|
||||||
|
@ -145,15 +138,18 @@ func UpdateDeviceByID(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceUpdate failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !performRes.DeviceExists {
|
if !performRes.DeviceExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.Forbidden("device does not exist"),
|
JSON: jsonerror.Forbidden("device does not exist"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if performRes.Forbidden {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("device not owned by current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,7 +179,7 @@ func DeleteDeviceById(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -193,7 +189,7 @@ func DeleteDeviceById(
|
||||||
if dev != deviceID {
|
if dev != deviceID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("session and device mismatch"),
|
JSON: jsonerror.Forbidden("session & device mismatch"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -215,10 +211,7 @@ func DeleteDeviceById(
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
// make sure that the access token being used matches the login creds used for user interactive auth, else
|
||||||
|
@ -226,7 +219,7 @@ func DeleteDeviceById(
|
||||||
if login.Username() != localpart && login.Username() != device.UserID {
|
if login.Username() != localpart && login.Username() != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: spec.Forbidden("Cannot delete another user's device"),
|
JSON: jsonerror.Forbidden("Cannot delete another user's device"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,10 +229,7 @@ func DeleteDeviceById(
|
||||||
DeviceIDs: []string{deviceID},
|
DeviceIDs: []string{deviceID},
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteOK = true
|
deleteOK = true
|
||||||
|
@ -252,51 +242,24 @@ func DeleteDeviceById(
|
||||||
|
|
||||||
// DeleteDevices handles POST requests to /delete_devices
|
// DeleteDevices handles POST requests to /delete_devices
|
||||||
func DeleteDevices(
|
func DeleteDevices(
|
||||||
req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
|
|
||||||
bodyBytes, err := io.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("The request body could not be read: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defer req.Body.Close() // nolint:errcheck
|
|
||||||
|
|
||||||
// initiate UIA
|
|
||||||
login, errRes := userInteractiveAuth.Verify(ctx, bodyBytes, device)
|
|
||||||
if errRes != nil {
|
|
||||||
return *errRes
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.Username() != device.UserID {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("unable to delete devices for other user"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
payload := devicesDeleteJSON{}
|
payload := devicesDeleteJSON{}
|
||||||
if err = json.Unmarshal(bodyBytes, &payload); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("unable to unmarshal device deletion request")
|
if resErr := httputil.UnmarshalJSONRequest(req, &payload); resErr != nil {
|
||||||
return util.JSONResponse{
|
return *resErr
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer req.Body.Close() // nolint: errcheck
|
||||||
|
|
||||||
var res api.PerformDeviceDeletionResponse
|
var res api.PerformDeviceDeletionResponse
|
||||||
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
if err := userAPI.PerformDeviceDeletion(ctx, &api.PerformDeviceDeletionRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
DeviceIDs: payload.Devices,
|
DeviceIDs: payload.Devices,
|
||||||
}, &res); err != nil {
|
}, &res); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformDeviceDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -18,16 +18,14 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type roomDirectoryResponse struct {
|
type roomDirectoryResponse struct {
|
||||||
|
@ -35,7 +33,7 @@ type roomDirectoryResponse struct {
|
||||||
Servers []string `json:"servers"`
|
Servers []string `json:"servers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *roomDirectoryResponse) fillServers(servers []spec.ServerName) {
|
func (r *roomDirectoryResponse) fillServers(servers []gomatrixserverlib.ServerName) {
|
||||||
r.Servers = make([]string, len(servers))
|
r.Servers = make([]string, len(servers))
|
||||||
for i, s := range servers {
|
for i, s := range servers {
|
||||||
r.Servers[i] = string(s)
|
r.Servers[i] = string(s)
|
||||||
|
@ -46,7 +44,7 @@ func (r *roomDirectoryResponse) fillServers(servers []spec.ServerName) {
|
||||||
func DirectoryRoom(
|
func DirectoryRoom(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
roomAlias string,
|
roomAlias string,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
fedSenderAPI federationAPI.ClientFederationAPI,
|
fedSenderAPI federationAPI.ClientFederationAPI,
|
||||||
|
@ -55,7 +53,7 @@ func DirectoryRoom(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,10 +67,7 @@ func DirectoryRoom(
|
||||||
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
queryRes := &roomserverAPI.GetRoomIDForAliasResponse{}
|
||||||
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
if err = rsAPI.GetRoomIDForAlias(req.Context(), queryReq, queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetRoomIDForAlias failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.RoomID = queryRes.RoomID
|
res.RoomID = queryRes.RoomID
|
||||||
|
@ -80,16 +75,13 @@ func DirectoryRoom(
|
||||||
if res.RoomID == "" {
|
if res.RoomID == "" {
|
||||||
// If we don't know it locally, do a federation query.
|
// If we don't know it locally, do a federation query.
|
||||||
// But don't send the query to ourselves.
|
// But don't send the query to ourselves.
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if domain != cfg.Matrix.ServerName {
|
||||||
fedRes, fedErr := federation.LookupRoomAlias(req.Context(), cfg.Matrix.ServerName, domain, roomAlias)
|
fedRes, fedErr := federation.LookupRoomAlias(req.Context(), domain, roomAlias)
|
||||||
if fedErr != nil {
|
if fedErr != nil {
|
||||||
// TODO: Return 502 if the remote server errored.
|
// TODO: Return 502 if the remote server errored.
|
||||||
// TODO: Return 504 if the remote server timed out.
|
// TODO: Return 504 if the remote server timed out.
|
||||||
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(fedErr).Error("federation.LookupRoomAlias failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res.RoomID = fedRes.RoomID
|
res.RoomID = fedRes.RoomID
|
||||||
res.fillServers(fedRes.Servers)
|
res.fillServers(fedRes.Servers)
|
||||||
|
@ -98,7 +90,7 @@ func DirectoryRoom(
|
||||||
if res.RoomID == "" {
|
if res.RoomID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound(
|
JSON: jsonerror.NotFound(
|
||||||
fmt.Sprintf("Room alias %s not found", roomAlias),
|
fmt.Sprintf("Room alias %s not found", roomAlias),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
@ -108,10 +100,7 @@ func DirectoryRoom(
|
||||||
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
var joinedHostsRes federationAPI.QueryJoinedHostServerNamesInRoomResponse
|
||||||
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
if err = fedSenderAPI.QueryJoinedHostServerNamesInRoom(req.Context(), &joinedHostsReq, &joinedHostsRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
util.GetLogger(req.Context()).WithError(err).Error("fedSenderAPI.QueryJoinedHostServerNamesInRoom failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
res.fillServers(joinedHostsRes.ServerNames)
|
res.fillServers(joinedHostsRes.ServerNames)
|
||||||
}
|
}
|
||||||
|
@ -134,14 +123,14 @@ func SetLocalAlias(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam("Room alias must be in the form '#localpart:domain'"),
|
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if domain != cfg.Matrix.ServerName {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("Alias must be on local homeserver"),
|
JSON: jsonerror.Forbidden("Alias must be on local homeserver"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -154,7 +143,7 @@ func SetLocalAlias(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON("User ID must be in the form '@localpart:domain'"),
|
JSON: jsonerror.BadJSON("User ID must be in the form '@localpart:domain'"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, appservice := range cfg.Derived.ApplicationServices {
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
|
@ -166,7 +155,7 @@ func SetLocalAlias(
|
||||||
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.ASExclusive("Alias is reserved by an application service"),
|
JSON: jsonerror.ASExclusive("Alias is reserved by an application service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -181,50 +170,21 @@ func SetLocalAlias(
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
roomID, err := spec.NewRoomID(r.RoomID)
|
queryReq := roomserverAPI.SetRoomAliasRequest{
|
||||||
if err != nil {
|
UserID: device.UserID,
|
||||||
return util.JSONResponse{
|
RoomID: r.RoomID,
|
||||||
Code: http.StatusBadRequest,
|
Alias: alias,
|
||||||
JSON: spec.InvalidParam("invalid room ID"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var queryRes roomserverAPI.SetRoomAliasResponse
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
if err := rsAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *roomID, *userID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QuerySenderIDForUser failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
} else if senderID == nil {
|
|
||||||
util.GetLogger(req.Context()).WithField("roomID", *roomID).WithField("userID", *userID).Error("Sender ID not found")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasAlreadyExists, err := rsAPI.SetRoomAlias(req.Context(), *senderID, *roomID, alias)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if aliasAlreadyExists {
|
if queryRes.AliasExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusConflict,
|
Code: http.StatusConflict,
|
||||||
JSON: spec.Unknown("The alias " + alias + " already exists."),
|
JSON: jsonerror.Unknown("The alias " + alias + " already exists."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,91 +201,27 @@ func RemoveLocalAlias(
|
||||||
alias string,
|
alias string,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
queryReq := roomserverAPI.RemoveRoomAliasRequest{
|
||||||
if err != nil {
|
Alias: alias,
|
||||||
return util.JSONResponse{
|
UserID: device.UserID,
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{Err: "UserID for device is invalid"},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var queryRes roomserverAPI.RemoveRoomAliasResponse
|
||||||
roomIDReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: alias}
|
if err := rsAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
roomIDRes := roomserverAPI.GetRoomIDForAliasResponse{}
|
|
||||||
err = rsAPI.GetRoomIDForAlias(req.Context(), &roomIDReq, &roomIDRes)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound("The alias does not exist."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
validRoomID, err := spec.NewRoomID(roomIDRes.RoomID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound("The alias does not exist."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This seems like the kind of auth check that should be done in the roomserver, but
|
|
||||||
// if this check fails (user is not in the room), then there will be no SenderID for the user
|
|
||||||
// for pseudo-ID rooms - it will just return "". However, we can't use lack of a sender ID
|
|
||||||
// as meaning they are not in the room, since lacking a sender ID could be caused by other bugs.
|
|
||||||
// TODO: maybe have QuerySenderIDForUser return richer errors?
|
|
||||||
var queryResp roomserverAPI.QueryMembershipForUserResponse
|
|
||||||
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
||||||
RoomID: validRoomID.String(),
|
|
||||||
UserID: *userID,
|
|
||||||
}, &queryResp)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.QueryMembershipForUser failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !queryResp.IsInRoom {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceSenderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *userID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound("The alias does not exist."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// TODO: how to handle this case? missing user/room keys seem to be a whole new class of errors
|
|
||||||
if deviceSenderID == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
aliasFound, aliasRemoved, err := rsAPI.RemoveRoomAlias(req.Context(), *deviceSenderID, alias)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !aliasFound {
|
if !queryRes.Found {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound("The alias does not exist."),
|
JSON: jsonerror.NotFound("The alias does not exist."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !aliasRemoved {
|
if !queryRes.Removed {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("You do not have permission to remove this alias."),
|
JSON: jsonerror.Forbidden("You do not have permission to remove this alias."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -350,15 +246,12 @@ func GetVisibility(
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var v roomVisibility
|
var v roomVisibility
|
||||||
if len(res.RoomIDs) == 1 {
|
if len(res.RoomIDs) == 1 {
|
||||||
v.Visibility = spec.Public
|
v.Visibility = gomatrixserverlib.Public
|
||||||
} else {
|
} else {
|
||||||
v.Visibility = "private"
|
v.Visibility = "private"
|
||||||
}
|
}
|
||||||
|
@ -375,30 +268,7 @@ func SetVisibility(
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
deviceUserID, err := spec.NewUserID(dev.UserID, true)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, dev.UserID, roomID)
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("userID for this device is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("roomID is invalid")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("RoomID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
|
||||||
if err != nil || senderID == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown("failed to find senderID for this user"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
@ -406,26 +276,23 @@ func SetVisibility(
|
||||||
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
queryEventsReq := roomserverAPI.QueryLatestEventsAndStateRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
||||||
EventType: spec.MRoomPowerLevels,
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
}},
|
}},
|
||||||
}
|
}
|
||||||
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
var queryEventsRes roomserverAPI.QueryLatestEventsAndStateResponse
|
||||||
err = rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
err := rsAPI.QueryLatestEventsAndState(req.Context(), &queryEventsReq, &queryEventsRes)
|
||||||
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
if err != nil || len(queryEventsRes.StateEvents) == 0 {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
util.GetLogger(req.Context()).WithError(err).Error("could not query events from room")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
||||||
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].PDU)
|
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].Event)
|
||||||
if power.UserLevel(*senderID) < power.EventLevel(spec.MRoomCanonicalAlias, true) {
|
if power.UserLevel(dev.UserID) < power.EventLevel(gomatrixserverlib.MRoomCanonicalAlias, true) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID doesn't have power level to change visibility"),
|
JSON: jsonerror.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -434,54 +301,16 @@ func SetVisibility(
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
var publishRes roomserverAPI.PerformPublishResponse
|
||||||
|
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Visibility: v.Visibility,
|
Visibility: v.Visibility,
|
||||||
}); err != nil {
|
}, &publishRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
return util.JSONResponse{
|
}
|
||||||
Code: http.StatusInternalServerError,
|
if publishRes.Error != nil {
|
||||||
JSON: spec.InternalServerError{},
|
util.GetLogger(req.Context()).WithError(publishRes.Error).Error("PerformPublish failed")
|
||||||
}
|
return publishRes.Error.JSONResponse()
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetVisibilityAS(
|
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device,
|
|
||||||
networkID, roomID string,
|
|
||||||
) util.JSONResponse {
|
|
||||||
if dev.AccountType != userapi.AccountTypeAppService {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("Only appservice may use this endpoint"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var v roomVisibility
|
|
||||||
|
|
||||||
// If the method is delete, we simply mark the visibility as private
|
|
||||||
if req.Method == http.MethodDelete {
|
|
||||||
v.Visibility = "private"
|
|
||||||
} else {
|
|
||||||
if reqErr := httputil.UnmarshalJSONRequest(req, &v); reqErr != nil {
|
|
||||||
return *reqErr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
Visibility: v.Visibility,
|
|
||||||
NetworkID: networkID,
|
|
||||||
AppserviceID: dev.AppserviceID,
|
|
||||||
}); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to publish room")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -23,40 +23,37 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/api"
|
"github.com/matrix-org/dendrite/clientapi/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cacheMu sync.Mutex
|
cacheMu sync.Mutex
|
||||||
publicRoomsCache []fclient.PublicRoom
|
publicRoomsCache []gomatrixserverlib.PublicRoom
|
||||||
)
|
)
|
||||||
|
|
||||||
type PublicRoomReq struct {
|
type PublicRoomReq struct {
|
||||||
Since string `json:"since,omitempty"`
|
Since string `json:"since,omitempty"`
|
||||||
Limit int64 `json:"limit,omitempty"`
|
Limit int16 `json:"limit,omitempty"`
|
||||||
Filter filter `json:"filter,omitempty"`
|
Filter filter `json:"filter,omitempty"`
|
||||||
Server string `json:"server,omitempty"`
|
Server string `json:"server,omitempty"`
|
||||||
IncludeAllNetworks bool `json:"include_all_networks,omitempty"`
|
|
||||||
NetworkID string `json:"third_party_instance_id,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type filter struct {
|
type filter struct {
|
||||||
SearchTerms string `json:"generic_search_term,omitempty"`
|
SearchTerms string `json:"generic_search_term,omitempty"`
|
||||||
RoomTypes []string `json:"room_types,omitempty"` // TODO: Implement filter on this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostPublicRooms implements GET and POST /publicRooms
|
// GetPostPublicRooms implements GET and POST /publicRooms
|
||||||
func GetPostPublicRooms(
|
func GetPostPublicRooms(
|
||||||
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var request PublicRoomReq
|
var request PublicRoomReq
|
||||||
|
@ -64,27 +61,18 @@ func GetPostPublicRooms(
|
||||||
return *fillErr
|
return *fillErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if request.IncludeAllNetworks && request.NetworkID != "" {
|
serverName := gomatrixserverlib.ServerName(request.Server)
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("include_all_networks and third_party_instance_id can not be used together"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
serverName := spec.ServerName(request.Server)
|
if serverName != "" && serverName != cfg.Matrix.ServerName {
|
||||||
if serverName != "" && !cfg.Matrix.IsLocalServerName(serverName) {
|
|
||||||
res, err := federation.GetPublicRoomsFiltered(
|
res, err := federation.GetPublicRoomsFiltered(
|
||||||
req.Context(), cfg.Matrix.ServerName, serverName,
|
req.Context(), serverName,
|
||||||
int(request.Limit), request.Since,
|
int(request.Limit), request.Since,
|
||||||
request.Filter.SearchTerms, false,
|
request.Filter.SearchTerms, false,
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
util.GetLogger(req.Context()).WithError(err).Error("failed to get public rooms")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -95,10 +83,7 @@ func GetPostPublicRooms(
|
||||||
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
response, err := publicRooms(req.Context(), request, rsAPI, extRoomsProvider)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
util.GetLogger(req.Context()).WithError(err).Errorf("failed to work out public rooms")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -108,12 +93,12 @@ func GetPostPublicRooms(
|
||||||
|
|
||||||
func publicRooms(
|
func publicRooms(
|
||||||
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
) (*fclient.RespPublicRooms, error) {
|
) (*gomatrixserverlib.RespPublicRooms, error) {
|
||||||
|
|
||||||
response := fclient.RespPublicRooms{
|
response := gomatrixserverlib.RespPublicRooms{
|
||||||
Chunk: []fclient.PublicRoom{},
|
Chunk: []gomatrixserverlib.PublicRoom{},
|
||||||
}
|
}
|
||||||
var limit int64
|
var limit int16
|
||||||
var offset int64
|
var offset int64
|
||||||
limit = request.Limit
|
limit = request.Limit
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
|
@ -128,9 +113,9 @@ func publicRooms(
|
||||||
}
|
}
|
||||||
err = nil
|
err = nil
|
||||||
|
|
||||||
var rooms []fclient.PublicRoom
|
var rooms []gomatrixserverlib.PublicRoom
|
||||||
if request.Since == "" {
|
if request.Since == "" {
|
||||||
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider, request)
|
rooms = refreshPublicRoomCache(ctx, rsAPI, extRoomsProvider)
|
||||||
} else {
|
} else {
|
||||||
rooms = getPublicRoomsFromCache()
|
rooms = getPublicRoomsFromCache()
|
||||||
}
|
}
|
||||||
|
@ -152,14 +137,14 @@ func publicRooms(
|
||||||
return &response, err
|
return &response, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterRooms(rooms []fclient.PublicRoom, searchTerm string) []fclient.PublicRoom {
|
func filterRooms(rooms []gomatrixserverlib.PublicRoom, searchTerm string) []gomatrixserverlib.PublicRoom {
|
||||||
if searchTerm == "" {
|
if searchTerm == "" {
|
||||||
return rooms
|
return rooms
|
||||||
}
|
}
|
||||||
|
|
||||||
normalizedTerm := strings.ToLower(searchTerm)
|
normalizedTerm := strings.ToLower(searchTerm)
|
||||||
|
|
||||||
result := make([]fclient.PublicRoom, 0)
|
result := make([]gomatrixserverlib.PublicRoom, 0)
|
||||||
for _, room := range rooms {
|
for _, room := range rooms {
|
||||||
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
if strings.Contains(strings.ToLower(room.Name), normalizedTerm) ||
|
||||||
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
strings.Contains(strings.ToLower(room.Topic), normalizedTerm) ||
|
||||||
|
@ -178,7 +163,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
if httpReq.Method != "GET" && httpReq.Method != "POST" {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: spec.NotFound("Bad method"),
|
JSON: jsonerror.NotFound("Bad method"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if httpReq.Method == "GET" {
|
if httpReq.Method == "GET" {
|
||||||
|
@ -189,10 +174,10 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
util.GetLogger(httpReq.Context()).WithError(err).Error("strconv.Atoi failed")
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: spec.BadJSON("limit param is not a number"),
|
JSON: jsonerror.BadJSON("limit param is not a number"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
request.Limit = int64(limit)
|
request.Limit = int16(limit)
|
||||||
request.Since = httpReq.FormValue("since")
|
request.Since = httpReq.FormValue("since")
|
||||||
request.Server = httpReq.FormValue("server")
|
request.Server = httpReq.FormValue("server")
|
||||||
} else {
|
} else {
|
||||||
|
@ -220,7 +205,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO
|
||||||
// limit=3&since=6 => G (prev='3', next='')
|
// limit=3&since=6 => G (prev='3', next='')
|
||||||
//
|
//
|
||||||
// A value of '-1' for prev/next indicates no position.
|
// A value of '-1' for prev/next indicates no position.
|
||||||
func sliceInto(slice []fclient.PublicRoom, since int64, limit int64) (subset []fclient.PublicRoom, prev, next int) {
|
func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int16) (subset []gomatrixserverlib.PublicRoom, prev, next int) {
|
||||||
prev = -1
|
prev = -1
|
||||||
next = -1
|
next = -1
|
||||||
|
|
||||||
|
@ -246,26 +231,16 @@ func sliceInto(slice []fclient.PublicRoom, since int64, limit int64) (subset []f
|
||||||
|
|
||||||
func refreshPublicRoomCache(
|
func refreshPublicRoomCache(
|
||||||
ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
request PublicRoomReq,
|
) []gomatrixserverlib.PublicRoom {
|
||||||
) []fclient.PublicRoom {
|
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
var extraRooms []fclient.PublicRoom
|
var extraRooms []gomatrixserverlib.PublicRoom
|
||||||
if extRoomsProvider != nil {
|
if extRoomsProvider != nil {
|
||||||
extraRooms = extRoomsProvider.Rooms()
|
extraRooms = extRoomsProvider.Rooms()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is only here to make Sytest happy, for now.
|
|
||||||
ns := strings.Split(request.NetworkID, "|")
|
|
||||||
if len(ns) == 2 {
|
|
||||||
request.NetworkID = ns[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryPublishedRoomsResponse
|
var queryRes roomserverAPI.QueryPublishedRoomsResponse
|
||||||
err := rsAPI.QueryPublishedRooms(ctx, &roomserverAPI.QueryPublishedRoomsRequest{
|
err := rsAPI.QueryPublishedRooms(ctx, &roomserverAPI.QueryPublishedRoomsRequest{}, &queryRes)
|
||||||
NetworkID: request.NetworkID,
|
|
||||||
IncludeAllNetworks: request.IncludeAllNetworks,
|
|
||||||
}, &queryRes)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryPublishedRooms failed")
|
util.GetLogger(ctx).WithError(err).Error("QueryPublishedRooms failed")
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
|
@ -275,7 +250,7 @@ func refreshPublicRoomCache(
|
||||||
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
util.GetLogger(ctx).WithError(err).Error("PopulatePublicRooms failed")
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
publicRoomsCache = []fclient.PublicRoom{}
|
publicRoomsCache = []gomatrixserverlib.PublicRoom{}
|
||||||
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
publicRoomsCache = append(publicRoomsCache, pubRooms...)
|
||||||
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
publicRoomsCache = append(publicRoomsCache, extraRooms...)
|
||||||
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
publicRoomsCache = dedupeAndShuffle(publicRoomsCache)
|
||||||
|
@ -287,16 +262,16 @@ func refreshPublicRoomCache(
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublicRoomsFromCache() []fclient.PublicRoom {
|
func getPublicRoomsFromCache() []gomatrixserverlib.PublicRoom {
|
||||||
cacheMu.Lock()
|
cacheMu.Lock()
|
||||||
defer cacheMu.Unlock()
|
defer cacheMu.Unlock()
|
||||||
return publicRoomsCache
|
return publicRoomsCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func dedupeAndShuffle(in []fclient.PublicRoom) []fclient.PublicRoom {
|
func dedupeAndShuffle(in []gomatrixserverlib.PublicRoom) []gomatrixserverlib.PublicRoom {
|
||||||
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
// de-duplicate rooms with the same room ID. We can join the room via any of these aliases as we know these servers
|
||||||
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
// are alive and well, so we arbitrarily pick one (purposefully shuffling them to spread the load a bit)
|
||||||
var publicRooms []fclient.PublicRoom
|
var publicRooms []gomatrixserverlib.PublicRoom
|
||||||
haveRoomIDs := make(map[string]bool)
|
haveRoomIDs := make(map[string]bool)
|
||||||
rand.Shuffle(len(in), func(i, j int) {
|
rand.Shuffle(len(in), func(i, j int) {
|
||||||
in[i], in[j] = in[j], in[i]
|
in[i], in[j] = in[j], in[i]
|
||||||
|
|
|
@ -4,25 +4,25 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pubRoom(name string) fclient.PublicRoom {
|
func pubRoom(name string) gomatrixserverlib.PublicRoom {
|
||||||
return fclient.PublicRoom{
|
return gomatrixserverlib.PublicRoom{
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSliceInto(t *testing.T) {
|
func TestSliceInto(t *testing.T) {
|
||||||
slice := []fclient.PublicRoom{
|
slice := []gomatrixserverlib.PublicRoom{
|
||||||
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
pubRoom("a"), pubRoom("b"), pubRoom("c"), pubRoom("d"), pubRoom("e"), pubRoom("f"), pubRoom("g"),
|
||||||
}
|
}
|
||||||
limit := int64(3)
|
limit := int16(3)
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
since int64
|
since int64
|
||||||
wantPrev int
|
wantPrev int
|
||||||
wantNext int
|
wantNext int
|
||||||
wantSubset []fclient.PublicRoom
|
wantSubset []gomatrixserverlib.PublicRoom
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
since: 0,
|
since: 0,
|
||||||
|
|
138
clientapi/routing/getevent.go
Normal file
138
clientapi/routing/getevent.go
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
// Copyright 2019 Alex Chen
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type getEventRequest struct {
|
||||||
|
req *http.Request
|
||||||
|
device *userapi.Device
|
||||||
|
roomID string
|
||||||
|
eventID string
|
||||||
|
cfg *config.ClientAPI
|
||||||
|
requestedEvent *gomatrixserverlib.Event
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEvent implements GET /_matrix/client/r0/rooms/{roomId}/event/{eventId}
|
||||||
|
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-rooms-roomid-event-eventid
|
||||||
|
func GetEvent(
|
||||||
|
req *http.Request,
|
||||||
|
device *userapi.Device,
|
||||||
|
roomID string,
|
||||||
|
eventID string,
|
||||||
|
cfg *config.ClientAPI,
|
||||||
|
rsAPI api.ClientRoomserverAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
eventsReq := api.QueryEventsByIDRequest{
|
||||||
|
EventIDs: []string{eventID},
|
||||||
|
}
|
||||||
|
var eventsResp api.QueryEventsByIDResponse
|
||||||
|
err := rsAPI.QueryEventsByID(req.Context(), &eventsReq, &eventsResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryEventsByID failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(eventsResp.Events) == 0 {
|
||||||
|
// Event not found locally
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestedEvent := eventsResp.Events[0].Event
|
||||||
|
|
||||||
|
r := getEventRequest{
|
||||||
|
req: req,
|
||||||
|
device: device,
|
||||||
|
roomID: roomID,
|
||||||
|
eventID: eventID,
|
||||||
|
cfg: cfg,
|
||||||
|
requestedEvent: requestedEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
stateReq := api.QueryStateAfterEventsRequest{
|
||||||
|
RoomID: r.requestedEvent.RoomID(),
|
||||||
|
PrevEventIDs: r.requestedEvent.PrevEventIDs(),
|
||||||
|
StateToFetch: []gomatrixserverlib.StateKeyTuple{{
|
||||||
|
EventType: gomatrixserverlib.MRoomMember,
|
||||||
|
StateKey: device.UserID,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
var stateResp api.QueryStateAfterEventsResponse
|
||||||
|
if err := rsAPI.QueryStateAfterEvents(req.Context(), &stateReq, &stateResp); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryStateAfterEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stateResp.RoomExists {
|
||||||
|
util.GetLogger(req.Context()).Errorf("Expected to find room for event %s but failed", r.requestedEvent.EventID())
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !stateResp.PrevEventsExist {
|
||||||
|
// Missing some events locally; stateResp.StateEvents unavailable.
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var appService *config.ApplicationService
|
||||||
|
if device.AppserviceID != "" {
|
||||||
|
for _, as := range cfg.Derived.ApplicationServices {
|
||||||
|
if as.ID == device.AppserviceID {
|
||||||
|
appService = &as
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, stateEvent := range stateResp.StateEvents {
|
||||||
|
if appService != nil {
|
||||||
|
if !appService.IsInterestedInUserID(*stateEvent.StateKey()) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else if !stateEvent.StateKeyEquals(device.UserID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
membership, err := stateEvent.Membership()
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("stateEvent.Membership failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
if membership == gomatrixserverlib.Join {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: gomatrixserverlib.ToClientEvent(r.requestedEvent, gomatrixserverlib.FormatAll),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The event was not found or you do not have permission to read this event"),
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,68 +0,0 @@
|
||||||
// Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
)
|
|
||||||
|
|
||||||
type getJoinedRoomsResponse struct {
|
|
||||||
JoinedRooms []string `json:"joined_rooms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetJoinedRooms(
|
|
||||||
req *http.Request,
|
|
||||||
device *userapi.Device,
|
|
||||||
rsAPI api.ClientRoomserverAPI,
|
|
||||||
) util.JSONResponse {
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("Invalid device user ID")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
rooms, err := rsAPI.QueryRoomsForUser(req.Context(), *deviceUserID, "join")
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var roomIDStrs []string
|
|
||||||
if rooms == nil {
|
|
||||||
roomIDStrs = []string{}
|
|
||||||
} else {
|
|
||||||
roomIDStrs = make([]string, len(rooms))
|
|
||||||
for i, roomID := range rooms {
|
|
||||||
roomIDStrs[i] = roomID.String()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: getJoinedRoomsResponse{roomIDStrs},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -15,17 +15,14 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,9 +37,9 @@ func JoinRoomByIDOrAlias(
|
||||||
joinReq := roomserverAPI.PerformJoinRequest{
|
joinReq := roomserverAPI.PerformJoinRequest{
|
||||||
RoomIDOrAlias: roomIDOrAlias,
|
RoomIDOrAlias: roomIDOrAlias,
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
IsGuest: device.AccountType == api.AccountTypeGuest,
|
|
||||||
Content: map[string]interface{}{},
|
Content: map[string]interface{}{},
|
||||||
}
|
}
|
||||||
|
joinRes := roomserverAPI.PerformJoinResponse{}
|
||||||
|
|
||||||
// Check to see if any ?server_name= query parameters were
|
// Check to see if any ?server_name= query parameters were
|
||||||
// given in the request.
|
// given in the request.
|
||||||
|
@ -50,7 +47,7 @@ func JoinRoomByIDOrAlias(
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
joinReq.ServerNames = append(
|
joinReq.ServerNames = append(
|
||||||
joinReq.ServerNames,
|
joinReq.ServerNames,
|
||||||
spec.ServerName(serverName),
|
gomatrixserverlib.ServerName(serverName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -63,84 +60,51 @@ func JoinRoomByIDOrAlias(
|
||||||
// Work out our localpart for the client profile request.
|
// Work out our localpart for the client profile request.
|
||||||
|
|
||||||
// Request our profile content to populate the request content with.
|
// Request our profile content to populate the request content with.
|
||||||
profile, err := profileAPI.QueryProfile(req.Context(), device.UserID)
|
res := &api.QueryProfileResponse{}
|
||||||
|
err := profileAPI.QueryProfile(req.Context(), &api.QueryProfileRequest{UserID: device.UserID}, res)
|
||||||
switch err {
|
if err != nil || !res.UserExists {
|
||||||
case nil:
|
if !res.UserExists {
|
||||||
joinReq.Content["displayname"] = profile.DisplayName
|
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
||||||
joinReq.Content["avatar_url"] = profile.AvatarURL
|
return util.JSONResponse{
|
||||||
case appserviceAPI.ErrProfileNotExists:
|
Code: http.StatusInternalServerError,
|
||||||
util.GetLogger(req.Context()).Error("Unable to query user profile, no profile found.")
|
JSON: jsonerror.Unknown("Unable to query user profile, no profile found."),
|
||||||
return util.JSONResponse{
|
}
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("Unable to query user profile, no profile found."),
|
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("UserProfileAPI.QueryProfile failed")
|
||||||
|
} else {
|
||||||
|
joinReq.Content["displayname"] = res.DisplayName
|
||||||
|
joinReq.Content["avatar_url"] = res.AvatarURL
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the roomserver to perform the join.
|
// Ask the roomserver to perform the join.
|
||||||
done := make(chan util.JSONResponse, 1)
|
done := make(chan util.JSONResponse, 1)
|
||||||
go func() {
|
go func() {
|
||||||
defer close(done)
|
defer close(done)
|
||||||
roomID, _, err := rsAPI.PerformJoin(req.Context(), &joinReq)
|
if err := rsAPI.PerformJoin(req.Context(), &joinReq, &joinRes); err != nil {
|
||||||
var response util.JSONResponse
|
done <- jsonerror.InternalAPIError(req.Context(), err)
|
||||||
|
} else if joinRes.Error != nil {
|
||||||
switch e := err.(type) {
|
done <- joinRes.Error.JSONResponse()
|
||||||
case nil: // success case
|
} else {
|
||||||
response = util.JSONResponse{
|
done <- util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
// TODO: Put the response struct somewhere internal.
|
// TODO: Put the response struct somewhere internal.
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}{roomID},
|
}{joinRes.RoomID},
|
||||||
}
|
|
||||||
case roomserverAPI.ErrInvalidID:
|
|
||||||
response = util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown(e.Error()),
|
|
||||||
}
|
|
||||||
case roomserverAPI.ErrNotAllowed:
|
|
||||||
jsonErr := spec.Forbidden(e.Error())
|
|
||||||
if device.AccountType == api.AccountTypeGuest {
|
|
||||||
jsonErr = spec.GuestAccessForbidden(e.Error())
|
|
||||||
}
|
|
||||||
response = util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonErr,
|
|
||||||
}
|
|
||||||
case *gomatrix.HTTPError: // this ensures we proxy responses over federation to the client
|
|
||||||
response = util.JSONResponse{
|
|
||||||
Code: e.Code,
|
|
||||||
JSON: json.RawMessage(e.Message),
|
|
||||||
}
|
|
||||||
case eventutil.ErrRoomNoExists:
|
|
||||||
response = util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: spec.NotFound(e.Error()),
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
response = util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
done <- response
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Wait either for the join to finish, or for us to hit a reasonable
|
// Wait either for the join to finish, or for us to hit a reasonable
|
||||||
// timeout, at which point we'll just return a 200 to placate clients.
|
// timeout, at which point we'll just return a 200 to placate clients.
|
||||||
timer := time.NewTimer(time.Second * 20)
|
|
||||||
select {
|
select {
|
||||||
case <-timer.C:
|
case <-time.After(time.Second * 20):
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusAccepted,
|
Code: http.StatusAccepted,
|
||||||
JSON: spec.Unknown("The room join will continue in the background."),
|
JSON: jsonerror.Unknown("The room join will continue in the background."),
|
||||||
}
|
}
|
||||||
case result := <-done:
|
case result := <-done:
|
||||||
// Stop and drain the timer
|
|
||||||
if !timer.Stop() {
|
|
||||||
<-timer.C
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"net/http"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/federationapi/statistics"
|
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/appservice"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
|
||||||
"github.com/matrix-org/dendrite/test"
|
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
var testIsBlacklistedOrBackingOff = func(s spec.ServerName) (*statistics.ServerStatistics, error) {
|
|
||||||
return &statistics.ServerStatistics{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJoinRoomByIDOrAlias(t *testing.T) {
|
|
||||||
alice := test.NewUser(t)
|
|
||||||
bob := test.NewUser(t)
|
|
||||||
charlie := test.NewUser(t, test.WithAccountType(uapi.AccountTypeGuest))
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
|
||||||
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
|
||||||
defer close()
|
|
||||||
|
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
natsInstance := jetstream.NATSInstance{}
|
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil) // creates the rs.Inputer etc
|
|
||||||
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
|
||||||
|
|
||||||
// Create the users in the userapi
|
|
||||||
for _, u := range []*test.User{alice, bob, charlie} {
|
|
||||||
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
|
||||||
userRes := &uapi.PerformAccountCreationResponse{}
|
|
||||||
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
|
||||||
AccountType: u.AccountType,
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: "someRandomPassword",
|
|
||||||
}, userRes); err != nil {
|
|
||||||
t.Errorf("failed to create account: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
aliceDev := &uapi.Device{UserID: alice.ID}
|
|
||||||
bobDev := &uapi.Device{UserID: bob.ID}
|
|
||||||
charlieDev := &uapi.Device{UserID: charlie.ID, AccountType: uapi.AccountTypeGuest}
|
|
||||||
|
|
||||||
// create a room with disabled guest access and invite Bob
|
|
||||||
resp := createRoom(ctx, createRoomRequest{
|
|
||||||
Name: "testing",
|
|
||||||
IsDirect: true,
|
|
||||||
Topic: "testing",
|
|
||||||
Visibility: "public",
|
|
||||||
Preset: spec.PresetPublicChat,
|
|
||||||
RoomAliasName: "alias",
|
|
||||||
Invite: []string{bob.ID},
|
|
||||||
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
|
||||||
crResp, ok := resp.JSON.(createRoomResponse)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// create a room with guest access enabled and invite Charlie
|
|
||||||
resp = createRoom(ctx, createRoomRequest{
|
|
||||||
Name: "testing",
|
|
||||||
IsDirect: true,
|
|
||||||
Topic: "testing",
|
|
||||||
Visibility: "public",
|
|
||||||
Preset: spec.PresetPublicChat,
|
|
||||||
Invite: []string{charlie.ID},
|
|
||||||
}, aliceDev, &cfg.ClientAPI, userAPI, rsAPI, asAPI, time.Now())
|
|
||||||
crRespWithGuestAccess, ok := resp.JSON.(createRoomResponse)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("response is not a createRoomResponse: %+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dummy request
|
|
||||||
body := &bytes.Buffer{}
|
|
||||||
req, err := http.NewRequest(http.MethodPost, "/?server_name=test", body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
device *uapi.Device
|
|
||||||
roomID string
|
|
||||||
wantHTTP200 bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "User can join successfully by alias",
|
|
||||||
device: bobDev,
|
|
||||||
roomID: crResp.RoomAlias,
|
|
||||||
wantHTTP200: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "User can join successfully by roomID",
|
|
||||||
device: bobDev,
|
|
||||||
roomID: crResp.RoomID,
|
|
||||||
wantHTTP200: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "join is forbidden if user is guest",
|
|
||||||
device: charlieDev,
|
|
||||||
roomID: crResp.RoomID,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "room does not exist",
|
|
||||||
device: aliceDev,
|
|
||||||
roomID: "!doesnotexist:test",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user from different server",
|
|
||||||
device: &uapi.Device{UserID: "@wrong:server"},
|
|
||||||
roomID: crResp.RoomAlias,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "user doesn't exist locally",
|
|
||||||
device: &uapi.Device{UserID: "@doesnotexist:test"},
|
|
||||||
roomID: crResp.RoomAlias,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "invalid room ID",
|
|
||||||
device: aliceDev,
|
|
||||||
roomID: "invalidRoomID",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "roomAlias does not exist",
|
|
||||||
device: aliceDev,
|
|
||||||
roomID: "#doesnotexist:test",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "room with guest_access event",
|
|
||||||
device: charlieDev,
|
|
||||||
roomID: crRespWithGuestAccess.RoomID,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
joinResp := JoinRoomByIDOrAlias(req, tc.device, rsAPI, userAPI, tc.roomID)
|
|
||||||
if tc.wantHTTP200 && !joinResp.Is2xx() {
|
|
||||||
t.Fatalf("expected join room to succeed, but didn't: %+v", joinResp)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -20,8 +20,8 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -61,26 +61,28 @@ func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if len(kb.AuthData) == 0 {
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
return util.JSONResponse{
|
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("missing auth_data"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
version, err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: "",
|
Version: "",
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
})
|
}, &performKeyBackupResp); err != nil {
|
||||||
if err != nil {
|
return jsonerror.InternalServerError()
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", err))
|
}
|
||||||
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
Version: version,
|
Version: performKeyBackupResp.Version,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,17 +90,20 @@ func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
||||||
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
||||||
func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
var queryResp userapi.QueryKeyBackupResponse
|
||||||
|
if err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
})
|
}, &queryResp); err != nil {
|
||||||
if err != nil {
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", err))
|
}
|
||||||
|
if queryResp.Error != "" {
|
||||||
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("version not found"),
|
JSON: jsonerror.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -121,29 +126,31 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUse
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
AuthData: kb.AuthData,
|
AuthData: kb.AuthData,
|
||||||
Algorithm: kb.Algorithm,
|
Algorithm: kb.Algorithm,
|
||||||
})
|
}, &performKeyBackupResp); err != nil {
|
||||||
switch e := err.(type) {
|
return jsonerror.InternalServerError()
|
||||||
case spec.ErrRoomKeysVersion:
|
}
|
||||||
return util.JSONResponse{
|
if performKeyBackupResp.Error != "" {
|
||||||
Code: http.StatusForbidden,
|
if performKeyBackupResp.BadInput {
|
||||||
JSON: e,
|
return util.JSONResponse{
|
||||||
}
|
Code: 400,
|
||||||
case nil:
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
default:
|
}
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("backup version not found"),
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Unclear what the 200 body should be
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: keyBackupVersionCreateResponse{
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
@ -155,19 +162,35 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUse
|
||||||
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
||||||
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
||||||
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
exists, err := userAPI.DeleteKeyBackup(req.Context(), device.UserID, version)
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
if err != nil {
|
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
return util.ErrorResponse(fmt.Errorf("DeleteKeyBackup: %s", err))
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
DeleteBackup: true,
|
||||||
|
}, &performKeyBackupResp); err != nil {
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
if !exists {
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
|
}
|
||||||
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("backup version not found"),
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Unclear what the 200 body should be
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: struct{}{},
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
Version: performKeyBackupResp.Version,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,26 +198,27 @@ func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, de
|
||||||
func UploadBackupKeys(
|
func UploadBackupKeys(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
performKeyBackupResp, err := userAPI.UpdateBackupKeyAuthData(req.Context(), &userapi.PerformKeyBackupRequest{
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
Keys: *keys,
|
Keys: *keys,
|
||||||
})
|
}, &performKeyBackupResp); err != nil && performKeyBackupResp.Error == "" {
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
switch e := err.(type) {
|
}
|
||||||
case spec.ErrRoomKeysVersion:
|
if performKeyBackupResp.Error != "" {
|
||||||
return util.JSONResponse{
|
if performKeyBackupResp.BadInput {
|
||||||
Code: http.StatusForbidden,
|
return util.JSONResponse{
|
||||||
JSON: e,
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case nil:
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
default:
|
|
||||||
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %w", e))
|
|
||||||
}
|
}
|
||||||
if !performKeyBackupResp.Exists {
|
if !performKeyBackupResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("backup version not found"),
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -210,20 +234,23 @@ func UploadBackupKeys(
|
||||||
func GetBackupKeys(
|
func GetBackupKeys(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string,
|
req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
queryResp, err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
var queryResp userapi.QueryKeyBackupResponse
|
||||||
|
if err := userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
Version: version,
|
Version: version,
|
||||||
ReturnKeys: true,
|
ReturnKeys: true,
|
||||||
KeysForRoomID: roomID,
|
KeysForRoomID: roomID,
|
||||||
KeysForSessionID: sessionID,
|
KeysForSessionID: sessionID,
|
||||||
})
|
}, &queryResp); err != nil {
|
||||||
if err != nil {
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %w", err))
|
}
|
||||||
|
if queryResp.Error != "" {
|
||||||
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
||||||
}
|
}
|
||||||
if !queryResp.Exists {
|
if !queryResp.Exists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("version not found"),
|
JSON: jsonerror.NotFound("version not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if sessionID != "" {
|
if sessionID != "" {
|
||||||
|
@ -240,20 +267,17 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
} else if roomID != "" {
|
} else if roomID != "" {
|
||||||
roomData, ok := queryResp.Keys[roomID]
|
roomData, ok := queryResp.Keys[roomID]
|
||||||
if !ok {
|
if ok {
|
||||||
// If no keys are found, then an object with an empty sessions property will be returned
|
// wrap response in "sessions"
|
||||||
roomData = make(map[string]userapi.KeyBackupSession)
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: roomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// wrap response in "sessions"
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: 200,
|
|
||||||
JSON: struct {
|
|
||||||
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
|
||||||
}{
|
|
||||||
Sessions: roomData,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// response is the same as the upload request
|
// response is the same as the upload request
|
||||||
var resp keyBackupSessionRequest
|
var resp keyBackupSessionRequest
|
||||||
|
@ -274,6 +298,6 @@ func GetBackupKeys(
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 404,
|
Code: 404,
|
||||||
JSON: spec.NotFound("keys not found"),
|
JSON: jsonerror.NotFound("keys not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,9 +20,10 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/keyserver/api"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,9 +33,9 @@ type crossSigningRequest struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceKeys(
|
func UploadCrossSigningDeviceKeys(
|
||||||
req *http.Request,
|
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
||||||
keyserverAPI api.ClientKeyAPI, device *api.Device,
|
keyserverAPI api.ClientKeyAPI, device *userapi.Device,
|
||||||
accountAPI api.ClientUserAPI, cfg *config.ClientAPI,
|
accountAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
uploadReq := &crossSigningRequest{}
|
uploadReq := &crossSigningRequest{}
|
||||||
uploadRes := &api.PerformUploadDeviceKeysResponse{}
|
uploadRes := &api.PerformUploadDeviceKeysResponse{}
|
||||||
|
@ -62,8 +63,8 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
UserAPI: accountAPI,
|
GetAccountByPassword: accountAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &uploadReq.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
@ -71,29 +72,31 @@ func UploadCrossSigningDeviceKeys(
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
||||||
|
|
||||||
uploadReq.UserID = device.UserID
|
uploadReq.UserID = device.UserID
|
||||||
keyserverAPI.PerformUploadDeviceKeys(req.Context(), &uploadReq.PerformUploadDeviceKeysRequest, uploadRes)
|
if err := keyserverAPI.PerformUploadDeviceKeys(req.Context(), &uploadReq.PerformUploadDeviceKeysRequest, uploadRes); err != nil {
|
||||||
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := uploadRes.Error; err != nil {
|
if err := uploadRes.Error; err != nil {
|
||||||
switch {
|
switch {
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidSignature(err.Error()),
|
JSON: jsonerror.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam(err.Error()),
|
JSON: jsonerror.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown(err.Error()),
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -104,7 +107,7 @@ func UploadCrossSigningDeviceKeys(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse {
|
||||||
uploadReq := &api.PerformUploadDeviceSignaturesRequest{}
|
uploadReq := &api.PerformUploadDeviceSignaturesRequest{}
|
||||||
uploadRes := &api.PerformUploadDeviceSignaturesResponse{}
|
uploadRes := &api.PerformUploadDeviceSignaturesResponse{}
|
||||||
|
|
||||||
|
@ -113,29 +116,31 @@ func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.Clie
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadReq.UserID = device.UserID
|
uploadReq.UserID = device.UserID
|
||||||
keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes)
|
if err := keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes); err != nil {
|
||||||
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := uploadRes.Error; err != nil {
|
if err := uploadRes.Error; err != nil {
|
||||||
switch {
|
switch {
|
||||||
case err.IsInvalidSignature:
|
case err.IsInvalidSignature:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidSignature(err.Error()),
|
JSON: jsonerror.InvalidSignature(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsMissingParam:
|
case err.IsMissingParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam(err.Error()),
|
JSON: jsonerror.MissingParam(err.Error()),
|
||||||
}
|
}
|
||||||
case err.IsInvalidParam:
|
case err.IsInvalidParam:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidParam(err.Error()),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown(err.Error()),
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,11 +19,11 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/dendrite/keyserver/api"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type uploadKeysRequest struct {
|
type uploadKeysRequest struct {
|
||||||
|
@ -31,7 +31,7 @@ type uploadKeysRequest struct {
|
||||||
OneTimeKeys map[string]json.RawMessage `json:"one_time_keys"`
|
OneTimeKeys map[string]json.RawMessage `json:"one_time_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse {
|
||||||
var r uploadKeysRequest
|
var r uploadKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
@ -67,10 +67,7 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device)
|
||||||
}
|
}
|
||||||
if uploadRes.Error != nil {
|
if uploadRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
util.GetLogger(req.Context()).WithError(uploadRes.Error).Error("Failed to PerformUploadKeys")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(uploadRes.KeyErrors) > 0 {
|
if len(uploadRes.KeyErrors) > 0 {
|
||||||
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
util.GetLogger(req.Context()).WithField("key_errors", uploadRes.KeyErrors).Error("Failed to upload one or more keys")
|
||||||
|
@ -80,6 +77,7 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keyCount := make(map[string]int)
|
keyCount := make(map[string]int)
|
||||||
|
// we only return key counts when the client uploads OTKs
|
||||||
if len(uploadRes.OneTimeKeyCounts) > 0 {
|
if len(uploadRes.OneTimeKeyCounts) > 0 {
|
||||||
keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount
|
keyCount = uploadRes.OneTimeKeyCounts[0].KeyCount
|
||||||
}
|
}
|
||||||
|
@ -93,6 +91,7 @@ func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device)
|
||||||
|
|
||||||
type queryKeysRequest struct {
|
type queryKeysRequest struct {
|
||||||
Timeout int `json:"timeout"`
|
Timeout int `json:"timeout"`
|
||||||
|
Token string `json:"token"`
|
||||||
DeviceKeys map[string][]string `json:"device_keys"`
|
DeviceKeys map[string][]string `json:"device_keys"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,25 +99,24 @@ func (r *queryKeysRequest) GetTimeout() time.Duration {
|
||||||
if r.Timeout == 0 {
|
if r.Timeout == 0 {
|
||||||
return 10 * time.Second
|
return 10 * time.Second
|
||||||
}
|
}
|
||||||
timeout := time.Duration(r.Timeout) * time.Millisecond
|
return time.Duration(r.Timeout) * time.Millisecond
|
||||||
if timeout > time.Second*20 {
|
|
||||||
timeout = time.Second * 20
|
|
||||||
}
|
|
||||||
return timeout
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *api.Device) util.JSONResponse {
|
func QueryKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse {
|
||||||
var r queryKeysRequest
|
var r queryKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
queryRes := api.QueryKeysResponse{}
|
queryRes := api.QueryKeysResponse{}
|
||||||
keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
if err := keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
UserToDevices: r.DeviceKeys,
|
UserToDevices: r.DeviceKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
}, &queryRes)
|
// TODO: Token?
|
||||||
|
}, &queryRes); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{
|
JSON: map[string]interface{}{
|
||||||
|
@ -151,16 +149,15 @@ func ClaimKeys(req *http.Request, keyAPI api.ClientKeyAPI) util.JSONResponse {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
claimRes := api.PerformClaimKeysResponse{}
|
claimRes := api.PerformClaimKeysResponse{}
|
||||||
keyAPI.PerformClaimKeys(req.Context(), &api.PerformClaimKeysRequest{
|
if err := keyAPI.PerformClaimKeys(req.Context(), &api.PerformClaimKeysRequest{
|
||||||
OneTimeKeys: r.OneTimeKeys,
|
OneTimeKeys: r.OneTimeKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
}, &claimRes)
|
}, &claimRes); err != nil {
|
||||||
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
|
}
|
||||||
if claimRes.Error != nil {
|
if claimRes.Error != nil {
|
||||||
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
util.GetLogger(req.Context()).WithError(claimRes.Error).Error("failed to PerformClaimKeys")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 200,
|
Code: 200,
|
||||||
|
|
|
@ -17,9 +17,9 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -29,18 +29,10 @@ func LeaveRoomByID(
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown("device userID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prepare to ask the roomserver to perform the room join.
|
// Prepare to ask the roomserver to perform the room join.
|
||||||
leaveReq := roomserverAPI.PerformLeaveRequest{
|
leaveReq := roomserverAPI.PerformLeaveRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Leaver: *userID,
|
UserID: device.UserID,
|
||||||
}
|
}
|
||||||
leaveRes := roomserverAPI.PerformLeaveResponse{}
|
leaveRes := roomserverAPI.PerformLeaveResponse{}
|
||||||
|
|
||||||
|
@ -49,12 +41,12 @@ func LeaveRoomByID(
|
||||||
if leaveRes.Code != 0 {
|
if leaveRes.Code != 0 {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: leaveRes.Code,
|
Code: leaveRes.Code,
|
||||||
JSON: spec.LeaveServerNoticeError(),
|
JSON: jsonerror.LeaveServerNoticeError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown(err.Error()),
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,18 +19,19 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type loginResponse struct {
|
type loginResponse struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
DeviceID string `json:"device_id"`
|
HomeServer gomatrixserverlib.ServerName `json:"home_server"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type flows struct {
|
type flows struct {
|
||||||
|
@ -41,59 +42,56 @@ type flow struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func passwordLogin() flows {
|
||||||
|
f := flows{}
|
||||||
|
s := flow{
|
||||||
|
Type: "m.login.password",
|
||||||
|
}
|
||||||
|
f.Flows = append(f.Flows, s)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
// Login implements GET and POST /login
|
// Login implements GET and POST /login
|
||||||
func Login(
|
func Login(
|
||||||
req *http.Request, userAPI userapi.ClientUserAPI,
|
req *http.Request, userAPI userapi.ClientUserAPI,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if req.Method == http.MethodGet {
|
if req.Method == http.MethodGet {
|
||||||
loginFlows := []flow{{Type: authtypes.LoginTypePassword}}
|
// TODO: support other forms of login other than password, depending on config options
|
||||||
if len(cfg.Derived.ApplicationServices) > 0 {
|
|
||||||
loginFlows = append(loginFlows, flow{Type: authtypes.LoginTypeApplicationService})
|
|
||||||
}
|
|
||||||
// TODO: support other forms of login, depending on config options
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: flows{
|
JSON: passwordLogin(),
|
||||||
Flows: loginFlows,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
} else if req.Method == http.MethodPost {
|
} else if req.Method == http.MethodPost {
|
||||||
login, cleanup, authErr := auth.LoginFromJSONReader(req, userAPI, userAPI, cfg)
|
login, cleanup, authErr := auth.LoginFromJSONReader(req.Context(), req.Body, userAPI, userAPI, cfg)
|
||||||
if authErr != nil {
|
if authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
}
|
}
|
||||||
// make a device/access token
|
// make a device/access token
|
||||||
authErr2 := completeAuth(req.Context(), cfg.Matrix, userAPI, login, req.RemoteAddr, req.UserAgent())
|
authErr2 := completeAuth(req.Context(), cfg.Matrix.ServerName, userAPI, login, req.RemoteAddr, req.UserAgent())
|
||||||
cleanup(req.Context(), &authErr2)
|
cleanup(req.Context(), &authErr2)
|
||||||
return authErr2
|
return authErr2
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusMethodNotAllowed,
|
Code: http.StatusMethodNotAllowed,
|
||||||
JSON: spec.NotFound("Bad method"),
|
JSON: jsonerror.NotFound("Bad method"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func completeAuth(
|
func completeAuth(
|
||||||
ctx context.Context, cfg *config.Global, userAPI userapi.ClientUserAPI, login *auth.Login,
|
ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI userapi.ClientUserAPI, login *auth.Login,
|
||||||
ipAddr, userAgent string,
|
ipAddr, userAgent string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
token, err := auth.GenerateAccessToken()
|
token, err := auth.GenerateAccessToken()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("auth.GenerateAccessToken failed")
|
util.GetLogger(ctx).WithError(err).Error("auth.GenerateAccessToken failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, serverName, err := userutil.ParseUsernameParam(login.Username(), cfg)
|
localpart, err := userutil.ParseUsernameParam(login.Username(), &serverName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("auth.ParseUsernameParam failed")
|
util.GetLogger(ctx).WithError(err).Error("auth.ParseUsernameParam failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var performRes userapi.PerformDeviceCreationResponse
|
var performRes userapi.PerformDeviceCreationResponse
|
||||||
|
@ -102,14 +100,13 @@ func completeAuth(
|
||||||
DeviceID: login.DeviceID,
|
DeviceID: login.DeviceID,
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: serverName,
|
|
||||||
IPAddr: ipAddr,
|
IPAddr: ipAddr,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,6 +115,7 @@ func completeAuth(
|
||||||
JSON: loginResponse{
|
JSON: loginResponse{
|
||||||
UserID: performRes.Device.UserID,
|
UserID: performRes.Device.UserID,
|
||||||
AccessToken: performRes.Device.AccessToken,
|
AccessToken: performRes.Device.AccessToken,
|
||||||
|
HomeServer: serverName,
|
||||||
DeviceID: performRes.Device.ID,
|
DeviceID: performRes.Device.ID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,198 +0,0 @@
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/test"
|
|
||||||
"github.com/matrix-org/dendrite/test/testrig"
|
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLogin(t *testing.T) {
|
|
||||||
aliceAdmin := test.NewUser(t, test.WithAccountType(uapi.AccountTypeAdmin))
|
|
||||||
bobUser := &test.User{ID: "@bob:test", AccountType: uapi.AccountTypeUser}
|
|
||||||
charlie := &test.User{ID: "@Charlie:test", AccountType: uapi.AccountTypeUser}
|
|
||||||
vhUser := &test.User{ID: "@vhuser:vh1"}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) {
|
|
||||||
cfg, processCtx, close := testrig.CreateConfig(t, dbType)
|
|
||||||
defer close()
|
|
||||||
cfg.ClientAPI.RateLimiting.Enabled = false
|
|
||||||
natsInstance := jetstream.NATSInstance{}
|
|
||||||
// add a vhost
|
|
||||||
cfg.Global.VirtualHosts = append(cfg.Global.VirtualHosts, &config.VirtualHost{
|
|
||||||
SigningIdentity: fclient.SigningIdentity{ServerName: "vh1"},
|
|
||||||
})
|
|
||||||
|
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
|
||||||
routers := httputil.NewRouters()
|
|
||||||
caches := caching.NewRistrettoCache(128*1024*1024, time.Hour, caching.DisableMetrics)
|
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.DisableMetrics)
|
|
||||||
rsAPI.SetFederationAPI(nil, nil)
|
|
||||||
// Needed for /login
|
|
||||||
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, nil, caching.DisableMetrics, testIsBlacklistedOrBackingOff)
|
|
||||||
|
|
||||||
// We mostly need the userAPI for this test, so nil for other APIs/caches etc.
|
|
||||||
Setup(routers, cfg, nil, nil, userAPI, nil, nil, nil, nil, nil, nil, nil, caching.DisableMetrics)
|
|
||||||
|
|
||||||
// Create password
|
|
||||||
password := util.RandomString(8)
|
|
||||||
|
|
||||||
// create the users
|
|
||||||
for _, u := range []*test.User{aliceAdmin, bobUser, vhUser, charlie} {
|
|
||||||
localpart, serverName, _ := gomatrixserverlib.SplitID('@', u.ID)
|
|
||||||
userRes := &uapi.PerformAccountCreationResponse{}
|
|
||||||
|
|
||||||
if err := userAPI.PerformAccountCreation(ctx, &uapi.PerformAccountCreationRequest{
|
|
||||||
AccountType: u.AccountType,
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: password,
|
|
||||||
}, userRes); err != nil {
|
|
||||||
t.Errorf("failed to create account: %s", err)
|
|
||||||
}
|
|
||||||
if !userRes.AccountCreated {
|
|
||||||
t.Fatalf("account not created")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
testCases := []struct {
|
|
||||||
name string
|
|
||||||
userID string
|
|
||||||
wantOK bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "aliceAdmin can login",
|
|
||||||
userID: aliceAdmin.ID,
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bobUser can login",
|
|
||||||
userID: bobUser.ID,
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "vhuser can login",
|
|
||||||
userID: vhUser.ID,
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "bob with uppercase can login",
|
|
||||||
userID: "@Bob:test",
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Charlie can login (existing uppercase)",
|
|
||||||
userID: charlie.ID,
|
|
||||||
wantOK: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Charlie can not login with lowercase userID",
|
|
||||||
userID: strings.ToLower(charlie.ID),
|
|
||||||
wantOK: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// Inject a dummy application service, so we have a "m.login.application_service"
|
|
||||||
// in the login flows
|
|
||||||
as := &config.ApplicationService{}
|
|
||||||
cfg.AppServiceAPI.Derived.ApplicationServices = []config.ApplicationService{*as}
|
|
||||||
|
|
||||||
t.Run("Supported log-in flows are returned", func(t *testing.T) {
|
|
||||||
req := test.NewRequest(t, http.MethodGet, "/_matrix/client/v3/login")
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("failed to get log-in flows: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("response: %s", rec.Body.String())
|
|
||||||
resp := flows{}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
appServiceFound := false
|
|
||||||
passwordFound := false
|
|
||||||
for _, flow := range resp.Flows {
|
|
||||||
if flow.Type == "m.login.password" {
|
|
||||||
passwordFound = true
|
|
||||||
} else if flow.Type == "m.login.application_service" {
|
|
||||||
appServiceFound = true
|
|
||||||
} else {
|
|
||||||
t.Fatalf("got unknown login flow: %s", flow.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !appServiceFound {
|
|
||||||
t.Fatal("m.login.application_service missing from login flows")
|
|
||||||
}
|
|
||||||
if !passwordFound {
|
|
||||||
t.Fatal("m.login.password missing from login flows")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
req := test.NewRequest(t, http.MethodPost, "/_matrix/client/v3/login", test.WithJSONBody(t, map[string]interface{}{
|
|
||||||
"type": authtypes.LoginTypePassword,
|
|
||||||
"identifier": map[string]interface{}{
|
|
||||||
"type": "m.id.user",
|
|
||||||
"user": tc.userID,
|
|
||||||
},
|
|
||||||
"password": password,
|
|
||||||
}))
|
|
||||||
rec := httptest.NewRecorder()
|
|
||||||
routers.Client.ServeHTTP(rec, req)
|
|
||||||
if tc.wantOK && rec.Code != http.StatusOK {
|
|
||||||
t.Fatalf("failed to login: %s", rec.Body.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Response: %s", rec.Body.String())
|
|
||||||
// get the response
|
|
||||||
resp := loginResponse{}
|
|
||||||
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
// everything OK
|
|
||||||
if !tc.wantOK && resp.AccessToken == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if tc.wantOK && resp.AccessToken == "" {
|
|
||||||
t.Fatalf("expected accessToken after successful login but got none: %+v", resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
devicesResp := &uapi.QueryDevicesResponse{}
|
|
||||||
if err := userAPI.QueryDevices(ctx, &uapi.QueryDevicesRequest{UserID: resp.UserID}, devicesResp); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, dev := range devicesResp.Devices {
|
|
||||||
// We expect the userID on the device to be the same as resp.UserID
|
|
||||||
if dev.UserID != resp.UserID {
|
|
||||||
t.Fatalf("unexpected userID on device: %s", dev.UserID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -17,8 +17,8 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,10 +33,7 @@ func Logout(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -56,10 +53,7 @@ func LogoutAll(
|
||||||
}, &performRes)
|
}, &performRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -16,120 +16,106 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/threepid"
|
"github.com/matrix-org/dendrite/clientapi/threepid"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var errMissingUserID = errors.New("'user_id' must be supplied")
|
||||||
|
|
||||||
func SendBan(
|
func SendBan(
|
||||||
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, reqErr := extractRequestData(req)
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if body.UserID == "" {
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("missing user_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to ban this user, bad userID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("RoomID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
|
||||||
if err != nil || senderID == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to ban this user, unknown senderID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
if errRes != nil {
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
||||||
return *errRes
|
StateKey: "",
|
||||||
|
})
|
||||||
|
if plEvent == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, no power_levels event in this room."),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
allowedToBan := pl.UserLevel(*senderID) >= pl.Ban
|
pl, err := plEvent.PowerLevels()
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
||||||
if !allowedToBan {
|
if !allowedToBan {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: 403,
|
||||||
JSON: spec.Forbidden("You don't have permission to ban this user, power level too low."),
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Ban, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
||||||
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
||||||
|
roomVer gomatrixserverlib.RoomVersion,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
||||||
|
|
||||||
event, err := buildMembershipEvent(
|
event, err := buildMembershipEvent(
|
||||||
ctx, targetUserID, reason, profileAPI, device, membership,
|
ctx, targetUserID, reason, profileAPI, device, membership,
|
||||||
roomID, false, cfg, evTime, rsAPI, asAPI,
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err == errMissingUserID {
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
|
} else if err == eventutil.ErrRoomNoExists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound(err.Error()),
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
serverName := device.UserDomain()
|
|
||||||
if err = roomserverAPI.SendEvents(
|
if err = roomserverAPI.SendEvents(
|
||||||
ctx, rsAPI,
|
ctx, rsAPI,
|
||||||
roomserverAPI.KindNew,
|
roomserverAPI.KindNew,
|
||||||
[]*types.HeaderedEvent{event},
|
[]*gomatrixserverlib.HeaderedEvent{event.Event.Headered(roomVer)},
|
||||||
device.UserDomain(),
|
cfg.Matrix.ServerName,
|
||||||
serverName,
|
cfg.Matrix.ServerName,
|
||||||
serverName,
|
|
||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -143,81 +129,39 @@ func SendKick(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, reqErr := extractRequestData(req)
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
if body.UserID == "" {
|
if body.UserID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: 400,
|
||||||
JSON: spec.BadJSON("missing user_id"),
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("RoomID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
|
||||||
if err != nil || senderID == nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, unknown senderID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if errRes != nil {
|
if errRes != nil {
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyUserID, err := spec.NewUserID(body.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("body userID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
|
||||||
if errRes != nil {
|
|
||||||
return *errRes
|
|
||||||
}
|
|
||||||
allowedToKick := pl.UserLevel(*senderID) >= pl.Kick || bodyUserID.String() == deviceUserID.String()
|
|
||||||
if !allowedToKick {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, power level too low."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: *bodyUserID,
|
UserID: body.UserID,
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
||||||
if queryRes.Membership != spec.Join && queryRes.Membership != spec.Invite {
|
if queryRes.Membership != "join" && queryRes.Membership != "invite" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: 403,
|
||||||
JSON: spec.Unknown("cannot /kick banned or left users"),
|
JSON: jsonerror.Unknown("cannot /kick banned or left users"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be using SendLeave instead?
|
// TODO: should we be using SendLeave instead?
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendUnban(
|
func SendUnban(
|
||||||
|
@ -225,55 +169,40 @@ func SendUnban(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, reqErr := extractRequestData(req)
|
body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
if body.UserID == "" {
|
if body.UserID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: 400,
|
||||||
JSON: spec.BadJSON("missing user_id"),
|
JSON: jsonerror.BadJSON("missing user_id"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if errRes != nil {
|
|
||||||
return *errRes
|
|
||||||
}
|
|
||||||
|
|
||||||
bodyUserID, err := spec.NewUserID(body.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("body userID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
err = rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: *bodyUserID,
|
UserID: body.UserID,
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
if !queryRes.RoomExists {
|
||||||
// unban is only valid if the user is currently banned
|
|
||||||
if queryRes.Membership != spec.Ban {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Unknown("can only /unban users that are banned"),
|
JSON: jsonerror.Forbidden("room does not exist"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// unban is only valid if the user is currently banned
|
||||||
|
if queryRes.Membership != "ban" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.Unknown("can only /unban users that are banned"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// TODO: should we be using SendLeave instead?
|
// TODO: should we be using SendLeave instead?
|
||||||
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
return sendMembership(req.Context(), profileAPI, device, roomID, "leave", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func SendInvite(
|
func SendInvite(
|
||||||
|
@ -281,7 +210,7 @@ func SendInvite(
|
||||||
roomID string, cfg *config.ClientAPI,
|
roomID string, cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
body, evTime, reqErr := extractRequestData(req)
|
body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI)
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
@ -303,104 +232,56 @@ func SendInvite(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if body.UserID == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("missing user_id"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
errRes := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if errRes != nil {
|
|
||||||
return *errRes
|
|
||||||
}
|
|
||||||
|
|
||||||
// We already received the return value, so no need to check for an error here.
|
// We already received the return value, so no need to check for an error here.
|
||||||
response, _ := sendInvite(req.Context(), device, roomID, body.UserID, body.Reason, cfg, rsAPI, evTime)
|
response, _ := sendInvite(req.Context(), profileAPI, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime)
|
||||||
return response
|
return response
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendInvite sends an invitation to a user. Returns a JSONResponse and an error
|
// sendInvite sends an invitation to a user. Returns a JSONResponse and an error
|
||||||
func sendInvite(
|
func sendInvite(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
profileAPI userapi.ClientUserAPI,
|
||||||
device *userapi.Device,
|
device *userapi.Device,
|
||||||
roomID, userID, reason string,
|
roomID, userID, reason string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
evTime time.Time,
|
asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time,
|
||||||
) (util.JSONResponse, error) {
|
) (util.JSONResponse, error) {
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
event, err := buildMembershipEvent(
|
||||||
if err != nil {
|
ctx, userID, reason, profileAPI, device, "invite",
|
||||||
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
||||||
|
)
|
||||||
|
if err == errMissingUserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam("RoomID is invalid"),
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}, err
|
}, err
|
||||||
}
|
} else if err == eventutil.ErrRoomNoExists {
|
||||||
inviter, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.NotFound(err.Error()),
|
||||||
}, err
|
|
||||||
}
|
|
||||||
invitee, err := spec.NewUserID(userID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("UserID is invalid"),
|
|
||||||
}, err
|
}, err
|
||||||
|
} else if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
||||||
|
return jsonerror.InternalServerError(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
var inviteRes api.PerformInviteResponse
|
||||||
if err != nil {
|
if err := rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
||||||
return util.JSONResponse{
|
Event: event,
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
err = rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
|
||||||
InviteInput: roomserverAPI.InviteInput{
|
|
||||||
RoomID: *validRoomID,
|
|
||||||
Inviter: *inviter,
|
|
||||||
Invitee: *invitee,
|
|
||||||
Reason: reason,
|
|
||||||
IsDirect: false,
|
|
||||||
KeyID: identity.KeyID,
|
|
||||||
PrivateKey: identity.PrivateKey,
|
|
||||||
EventTime: evTime,
|
|
||||||
},
|
|
||||||
InviteRoomState: nil, // ask the roomserver to draw up invite room state for us
|
InviteRoomState: nil, // ask the roomserver to draw up invite room state for us
|
||||||
SendAsServer: string(device.UserDomain()),
|
RoomVersion: event.RoomVersion,
|
||||||
})
|
SendAsServer: string(cfg.Matrix.ServerName),
|
||||||
|
}, &inviteRes); err != nil {
|
||||||
switch e := err.(type) {
|
|
||||||
case roomserverAPI.ErrInvalidID:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown(e.Error()),
|
|
||||||
}, e
|
|
||||||
case roomserverAPI.ErrNotAllowed:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden(e.Error()),
|
|
||||||
}, e
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
||||||
sentry.CaptureException(err)
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.InternalServerError(),
|
||||||
}, err
|
}, err
|
||||||
}
|
}
|
||||||
|
if inviteRes.Error != nil {
|
||||||
|
return inviteRes.Error.JSONResponse(), inviteRes.Error
|
||||||
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -408,42 +289,6 @@ func sendInvite(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildMembershipEventDirect(
|
|
||||||
ctx context.Context,
|
|
||||||
targetSenderID spec.SenderID, reason string, userDisplayName, userAvatarURL string,
|
|
||||||
sender spec.SenderID, senderDomain spec.ServerName,
|
|
||||||
membership, roomID string, isDirect bool,
|
|
||||||
keyID gomatrixserverlib.KeyID, privateKey ed25519.PrivateKey, evTime time.Time,
|
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
||||||
) (*types.HeaderedEvent, error) {
|
|
||||||
targetSenderString := string(targetSenderID)
|
|
||||||
proto := gomatrixserverlib.ProtoEvent{
|
|
||||||
SenderID: string(sender),
|
|
||||||
RoomID: roomID,
|
|
||||||
Type: "m.room.member",
|
|
||||||
StateKey: &targetSenderString,
|
|
||||||
}
|
|
||||||
|
|
||||||
content := gomatrixserverlib.MemberContent{
|
|
||||||
Membership: membership,
|
|
||||||
DisplayName: userDisplayName,
|
|
||||||
AvatarURL: userAvatarURL,
|
|
||||||
Reason: reason,
|
|
||||||
IsDirect: isDirect,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := proto.SetContent(content); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
identity := &fclient.SigningIdentity{
|
|
||||||
ServerName: senderDomain,
|
|
||||||
KeyID: keyID,
|
|
||||||
PrivateKey: privateKey,
|
|
||||||
}
|
|
||||||
return eventutil.QueryAndBuildEvent(ctx, &proto, identity, evTime, rsAPI, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildMembershipEvent(
|
func buildMembershipEvent(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
targetUserID, reason string, profileAPI userapi.ClientUserAPI,
|
targetUserID, reason string, profileAPI userapi.ClientUserAPI,
|
||||||
|
@ -451,45 +296,32 @@ func buildMembershipEvent(
|
||||||
membership, roomID string, isDirect bool,
|
membership, roomID string, isDirect bool,
|
||||||
cfg *config.ClientAPI, evTime time.Time,
|
cfg *config.ClientAPI, evTime time.Time,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
) (*types.HeaderedEvent, error) {
|
) (*gomatrixserverlib.HeaderedEvent, error) {
|
||||||
profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI)
|
profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
builder := gomatrixserverlib.EventBuilder{
|
||||||
if err != nil {
|
Sender: device.UserID,
|
||||||
return nil, err
|
RoomID: roomID,
|
||||||
}
|
Type: "m.room.member",
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
StateKey: &targetUserID,
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
} else if senderID == nil {
|
|
||||||
return nil, fmt.Errorf("no sender ID for %s in %s", *userID, *validRoomID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetID, err := spec.NewUserID(targetUserID, true)
|
content := gomatrixserverlib.MemberContent{
|
||||||
if err != nil {
|
Membership: membership,
|
||||||
return nil, err
|
DisplayName: profile.DisplayName,
|
||||||
}
|
AvatarURL: profile.AvatarURL,
|
||||||
targetSenderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *targetID)
|
Reason: reason,
|
||||||
if err != nil {
|
IsDirect: isDirect,
|
||||||
return nil, err
|
|
||||||
} else if targetSenderID == nil {
|
|
||||||
return nil, fmt.Errorf("no sender ID for %s in %s", *targetID, *validRoomID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := rsAPI.SigningIdentityFor(ctx, *validRoomID, *userID)
|
if err = builder.SetContent(content); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildMembershipEventDirect(ctx, *targetSenderID, reason, profile.DisplayName, profile.AvatarURL,
|
return eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, evTime, rsAPI, nil)
|
||||||
*senderID, device.UserDomain(), membership, roomID, isDirect, identity.KeyID, identity.PrivateKey, evTime, rsAPI)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadProfile lookups the profile of a given user from the database and returns
|
// loadProfile lookups the profile of a given user from the database and returns
|
||||||
|
@ -509,7 +341,7 @@ func loadProfile(
|
||||||
}
|
}
|
||||||
|
|
||||||
var profile *authtypes.Profile
|
var profile *authtypes.Profile
|
||||||
if cfg.Matrix.IsLocalServerName(serverName) {
|
if serverName == cfg.Matrix.ServerName {
|
||||||
profile, err = appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
profile, err = appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
||||||
} else {
|
} else {
|
||||||
profile = &authtypes.Profile{}
|
profile = &authtypes.Profile{}
|
||||||
|
@ -518,7 +350,19 @@ func loadProfile(
|
||||||
return profile, err
|
return profile, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractRequestData(req *http.Request) (body *threepid.MembershipRequest, evTime time.Time, resErr *util.JSONResponse) {
|
func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.ClientRoomserverAPI) (
|
||||||
|
body *threepid.MembershipRequest, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, resErr *util.JSONResponse,
|
||||||
|
) {
|
||||||
|
verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
||||||
|
verRes := roomserverAPI.QueryRoomVersionForRoomResponse{}
|
||||||
|
if err := rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil {
|
||||||
|
resErr = &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
roomVer = verRes.RoomVersion
|
||||||
|
|
||||||
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
||||||
resErr = reqErr
|
resErr = reqErr
|
||||||
|
@ -529,7 +373,7 @@ func extractRequestData(req *http.Request) (body *threepid.MembershipRequest, ev
|
||||||
if err != nil {
|
if err != nil {
|
||||||
resErr = &util.JSONResponse{
|
resErr = &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -551,59 +395,67 @@ func checkAndProcessThreepid(
|
||||||
req.Context(), device, body, cfg, rsAPI, profileAPI,
|
req.Context(), device, body, cfg, rsAPI, profileAPI,
|
||||||
roomID, evTime,
|
roomID, evTime,
|
||||||
)
|
)
|
||||||
switch e := err.(type) {
|
if err == threepid.ErrMissingParameter {
|
||||||
case nil:
|
|
||||||
case threepid.ErrMissingParameter:
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON(err.Error()),
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
case threepid.ErrNotTrusted:
|
} else if err == threepid.ErrNotTrusted {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.NotTrusted(body.IDServer),
|
JSON: jsonerror.NotTrusted(body.IDServer),
|
||||||
}
|
}
|
||||||
case eventutil.ErrRoomNoExists:
|
} else if err == eventutil.ErrRoomNoExists {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound(err.Error()),
|
JSON: jsonerror.NotFound(err.Error()),
|
||||||
}
|
}
|
||||||
case gomatrixserverlib.BadJSONError:
|
} else if e, ok := err.(gomatrixserverlib.BadJSONError); ok {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
||||||
return inviteStored, &util.JSONResponse{
|
return inviteStored, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON(e.Error()),
|
JSON: jsonerror.BadJSON(e.Error()),
|
||||||
}
|
}
|
||||||
default:
|
}
|
||||||
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
||||||
return inviteStored, &util.JSONResponse{
|
er := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return inviteStored, &er
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID spec.UserID, roomID string) *util.JSONResponse {
|
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse {
|
||||||
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
tuple := gomatrixserverlib.StateKeyTuple{
|
||||||
err := rsAPI.QueryMembershipForUser(ctx, &roomserverAPI.QueryMembershipForUserRequest{
|
EventType: gomatrixserverlib.MRoomMember,
|
||||||
RoomID: roomID,
|
StateKey: userID,
|
||||||
UserID: userID,
|
}
|
||||||
|
var membershipRes roomserverAPI.QueryCurrentStateResponse
|
||||||
|
err := rsAPI.QueryCurrentState(ctx, &roomserverAPI.QueryCurrentStateRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
StateTuples: []gomatrixserverlib.StateKeyTuple{tuple},
|
||||||
}, &membershipRes)
|
}, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
util.GetLogger(ctx).WithError(err).Error("QueryCurrentState: could not query membership for user")
|
||||||
return &util.JSONResponse{
|
e := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &e
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !membershipRes.IsInRoom {
|
ev := membershipRes.StateEvents[tuple]
|
||||||
|
if ev == nil {
|
||||||
return &util.JSONResponse{
|
return &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("user does not belong to room"),
|
JSON: jsonerror.Forbidden("user does not belong to room"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
membership, err := ev.Membership()
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("Member event isn't valid")
|
||||||
|
e := jsonerror.InternalServerError()
|
||||||
|
return &e
|
||||||
|
}
|
||||||
|
if membership != gomatrixserverlib.Join {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("user does not belong to room"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -615,38 +467,26 @@ func SendForget(
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
ctx := req.Context()
|
ctx := req.Context()
|
||||||
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
||||||
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to kick this user, bad userID"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
||||||
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
UserID: *deviceUserID,
|
UserID: device.UserID,
|
||||||
}
|
}
|
||||||
err = rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
err := rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !membershipRes.RoomExists {
|
if !membershipRes.RoomExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("room does not exist"),
|
JSON: jsonerror.Forbidden("room does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if membershipRes.IsInRoom {
|
if membershipRes.IsInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
JSON: jsonerror.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -657,34 +497,10 @@ func SendForget(
|
||||||
response := roomserverAPI.PerformForgetResponse{}
|
response := roomserverAPI.PerformForgetResponse{}
|
||||||
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
||||||
logger.WithError(err).Error("PerformForget: unable to forget room")
|
logger.WithError(err).Error("PerformForget: unable to forget room")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPowerlevels(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, roomID string) (*gomatrixserverlib.PowerLevelContent, *util.JSONResponse) {
|
|
||||||
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
|
||||||
EventType: spec.MRoomPowerLevels,
|
|
||||||
StateKey: "",
|
|
||||||
})
|
|
||||||
if plEvent == nil {
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to perform this action, no power_levels event in this room."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pl, err := plEvent.PowerLevels()
|
|
||||||
if err != nil {
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("You don't have permission to perform this action, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return pl, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright 2024 The Matrix.org Foundation C.I.C.
|
// Copyright 2017 Vector Creations Ltd
|
||||||
//
|
//
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
// you may not use this file except in compliance with the License.
|
// you may not use this file except in compliance with the License.
|
||||||
|
@ -18,12 +18,22 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type getMembershipResponse struct {
|
||||||
|
Chunk []gomatrixserverlib.ClientEvent `json:"chunk"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type getJoinedRoomsResponse struct {
|
||||||
|
JoinedRooms []string `json:"joined_rooms"`
|
||||||
|
}
|
||||||
|
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members
|
// https://matrix.org/docs/spec/client_server/r0.6.0#get-matrix-client-r0-rooms-roomid-joined-members
|
||||||
type getJoinedMembersResponse struct {
|
type getJoinedMembersResponse struct {
|
||||||
Joined map[string]joinedMember `json:"joined"`
|
Joined map[string]joinedMember `json:"joined"`
|
||||||
|
@ -41,99 +51,71 @@ type databaseJoinedMember struct {
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetJoinedMembers implements
|
// GetMemberships implements GET /rooms/{roomId}/members
|
||||||
//
|
func GetMemberships(
|
||||||
// GET /rooms/{roomId}/joined_members
|
req *http.Request, device *userapi.Device, roomID string, joinedOnly bool,
|
||||||
func GetJoinedMembers(
|
_ *config.ClientAPI,
|
||||||
req *http.Request, device *userapi.Device, roomID string,
|
|
||||||
rsAPI api.ClientRoomserverAPI,
|
rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// Validate the userID
|
queryReq := api.QueryMembershipsForRoomRequest{
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
JoinedOnly: joinedOnly,
|
||||||
if err != nil {
|
RoomID: roomID,
|
||||||
return util.JSONResponse{
|
Sender: device.UserID,
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("Device UserID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
var queryRes api.QueryMembershipsForRoomResponse
|
||||||
// Validate the roomID
|
if err := rsAPI.QueryMembershipsForRoom(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||||
if err != nil {
|
return jsonerror.InternalServerError()
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("RoomID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the current memberships for the requesting user to determine
|
|
||||||
// if they are allowed to query this endpoint.
|
|
||||||
queryReq := api.QueryMembershipForUserRequest{
|
|
||||||
RoomID: validRoomID.String(),
|
|
||||||
UserID: *userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
var queryRes api.QueryMembershipForUserResponse
|
|
||||||
if queryErr := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); queryErr != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(queryErr).Error("rsAPI.QueryMembershipsForRoom failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !queryRes.HasBeenInRoom {
|
if !queryRes.HasBeenInRoom {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
|
JSON: jsonerror.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !queryRes.IsInRoom {
|
if joinedOnly {
|
||||||
return util.JSONResponse{
|
var res getJoinedMembersResponse
|
||||||
Code: http.StatusForbidden,
|
res.Joined = make(map[string]joinedMember)
|
||||||
JSON: spec.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
|
for _, ev := range queryRes.JoinEvents {
|
||||||
}
|
var content databaseJoinedMember
|
||||||
}
|
if err := json.Unmarshal(ev.Content, &content); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("failed to unmarshal event content")
|
||||||
// Get the current membership events
|
return jsonerror.InternalServerError()
|
||||||
var membershipsForRoomResp api.QueryMembershipsForRoomResponse
|
|
||||||
if err = rsAPI.QueryMembershipsForRoom(req.Context(), &api.QueryMembershipsForRoomRequest{
|
|
||||||
JoinedOnly: true,
|
|
||||||
RoomID: validRoomID.String(),
|
|
||||||
}, &membershipsForRoomResp); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryEventsByID failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var res getJoinedMembersResponse
|
|
||||||
res.Joined = make(map[string]joinedMember)
|
|
||||||
for _, ev := range membershipsForRoomResp.JoinEvents {
|
|
||||||
var content databaseJoinedMember
|
|
||||||
if err := json.Unmarshal(ev.Content, &content); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to unmarshal event content")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
}
|
||||||
|
res.Joined[ev.Sender] = joinedMember(content)
|
||||||
}
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
userID, err := rsAPI.QueryUserIDForSender(req.Context(), *validRoomID, spec.SenderID(ev.Sender))
|
Code: http.StatusOK,
|
||||||
if err != nil || userID == nil {
|
JSON: res,
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryUserIDForSender failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Joined[userID.String()] = joinedMember(content)
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: res,
|
JSON: getMembershipResponse{queryRes.JoinEvents},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetJoinedRooms(
|
||||||
|
req *http.Request,
|
||||||
|
device *userapi.Device,
|
||||||
|
rsAPI api.ClientRoomserverAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
var res api.QueryRoomsForUserResponse
|
||||||
|
err := rsAPI.QueryRoomsForUser(req.Context(), &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
WantMembership: "join",
|
||||||
|
}, &res)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
if res.RoomIDs == nil {
|
||||||
|
res.RoomIDs = []string{}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: getJoinedRoomsResponse{res.RoomIDs},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,9 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,35 +35,25 @@ func GetNotifications(
|
||||||
limit, err = strconv.ParseInt(limitStr, 10, 64)
|
limit, err = strconv.ParseInt(limitStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("ParseInt(limit) failed")
|
util.GetLogger(req.Context()).WithError(err).Error("ParseInt(limit) failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryRes userapi.QueryNotificationsResponse
|
var queryRes userapi.QueryNotificationsResponse
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = userAPI.QueryNotifications(req.Context(), &userapi.QueryNotificationsRequest{
|
err = userAPI.QueryNotifications(req.Context(), &userapi.QueryNotificationsRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
From: req.URL.Query().Get("from"),
|
||||||
From: req.URL.Query().Get("from"),
|
Limit: int(limit),
|
||||||
Limit: int(limit),
|
Only: req.URL.Query().Get("only"),
|
||||||
Only: req.URL.Query().Get("only"),
|
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryNotifications failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryNotifications failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
util.GetLogger(req.Context()).WithField("from", req.URL.Query().Get("from")).WithField("limit", limit).WithField("only", req.URL.Query().Get("only")).WithField("next", queryRes.NextToken).Infof("QueryNotifications: len %d", len(queryRes.Notifications))
|
util.GetLogger(req.Context()).WithField("from", req.URL.Query().Get("from")).WithField("limit", limit).WithField("only", req.URL.Query().Get("only")).WithField("next", queryRes.NextToken).Infof("QueryNotifications: len %d", len(queryRes.Notifications))
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -17,9 +17,9 @@ package routing
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ func CreateOpenIDToken(
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("Cannot request tokens for other users"),
|
JSON: jsonerror.Forbidden("Cannot request tokens for other users"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,10 +55,7 @@ func CreateOpenIDToken(
|
||||||
err := userAPI.PerformOpenIDTokenCreation(req.Context(), &request, &response)
|
err := userAPI.PerformOpenIDTokenCreation(req.Context(), &request, &response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.CreateOpenIDToken failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.CreateOpenIDToken failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -66,7 +63,7 @@ func CreateOpenIDToken(
|
||||||
JSON: openIDTokenResponse{
|
JSON: openIDTokenResponse{
|
||||||
AccessToken: response.Token.Token,
|
AccessToken: response.Token.Token,
|
||||||
TokenType: "Bearer",
|
TokenType: "Bearer",
|
||||||
MatrixServerName: string(device.UserDomain()),
|
MatrixServerName: string(cfg.Matrix.ServerName),
|
||||||
ExpiresIn: response.Token.ExpiresAtMS / 1000, // convert ms to s
|
ExpiresIn: response.Token.ExpiresAtMS / 1000, // convert ms to s
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,11 +6,10 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
@ -73,8 +72,8 @@ func Password(
|
||||||
|
|
||||||
// Check if the existing password is correct.
|
// Check if the existing password is correct.
|
||||||
typePassword := auth.LoginTypePassword{
|
typePassword := auth.LoginTypePassword{
|
||||||
UserAPI: userAPI,
|
GetAccountByPassword: userAPI.QueryAccountByPassword,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
if _, authErr := typePassword.Login(req.Context(), &r.Auth.PasswordRequest); authErr != nil {
|
||||||
return *authErr
|
return *authErr
|
||||||
|
@ -82,40 +81,30 @@ func Password(
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypePassword)
|
||||||
|
|
||||||
// Check the new password strength.
|
// Check the new password strength.
|
||||||
if err := internal.ValidatePassword(r.NewPassword); err != nil {
|
if resErr = validatePassword(r.NewPassword); resErr != nil {
|
||||||
return *internal.PasswordResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the local part.
|
// Get the local part.
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the user API to perform the password change.
|
// Ask the user API to perform the password change.
|
||||||
passwordReq := &api.PerformPasswordUpdateRequest{
|
passwordReq := &api.PerformPasswordUpdateRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
Password: r.NewPassword,
|
||||||
Password: r.NewPassword,
|
|
||||||
}
|
}
|
||||||
passwordRes := &api.PerformPasswordUpdateResponse{}
|
passwordRes := &api.PerformPasswordUpdateResponse{}
|
||||||
if err := userAPI.PerformPasswordUpdate(req.Context(), passwordReq, passwordRes); err != nil {
|
if err := userAPI.PerformPasswordUpdate(req.Context(), passwordReq, passwordRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPasswordUpdate failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPasswordUpdate failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !passwordRes.PasswordUpdated {
|
if !passwordRes.PasswordUpdated {
|
||||||
util.GetLogger(req.Context()).Error("Expected password to have been updated but wasn't")
|
util.GetLogger(req.Context()).Error("Expected password to have been updated but wasn't")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the request asks us to log out all other devices then
|
// If the request asks us to log out all other devices then
|
||||||
|
@ -129,23 +118,16 @@ func Password(
|
||||||
logoutRes := &api.PerformDeviceDeletionResponse{}
|
logoutRes := &api.PerformDeviceDeletionResponse{}
|
||||||
if err := userAPI.PerformDeviceDeletion(req.Context(), logoutReq, logoutRes); err != nil {
|
if err := userAPI.PerformDeviceDeletion(req.Context(), logoutReq, logoutRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformDeviceDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pushersReq := &api.PerformPusherDeletionRequest{
|
pushersReq := &api.PerformPusherDeletionRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
SessionID: device.SessionID,
|
||||||
SessionID: device.SessionID,
|
|
||||||
}
|
}
|
||||||
if err := userAPI.PerformPusherDeletion(req.Context(), pushersReq, &struct{}{}); err != nil {
|
if err := userAPI.PerformPusherDeletion(req.Context(), pushersReq, &struct{}{}); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherDeletion failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherDeletion failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,15 +15,13 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func PeekRoomByIDOrAlias(
|
func PeekRoomByIDOrAlias(
|
||||||
|
@ -43,42 +41,25 @@ func PeekRoomByIDOrAlias(
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
DeviceID: device.ID,
|
DeviceID: device.ID,
|
||||||
}
|
}
|
||||||
|
peekRes := roomserverAPI.PerformPeekResponse{}
|
||||||
|
|
||||||
// Check to see if any ?server_name= query parameters were
|
// Check to see if any ?server_name= query parameters were
|
||||||
// given in the request.
|
// given in the request.
|
||||||
if serverNames, ok := req.URL.Query()["server_name"]; ok {
|
if serverNames, ok := req.URL.Query()["server_name"]; ok {
|
||||||
for _, serverName := range serverNames {
|
for _, serverName := range serverNames {
|
||||||
peekReq.ServerNames = append(
|
peekReq.ServerNames = append(
|
||||||
peekReq.ServerNames,
|
peekReq.ServerNames,
|
||||||
spec.ServerName(serverName),
|
gomatrixserverlib.ServerName(serverName),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ask the roomserver to perform the peek.
|
// Ask the roomserver to perform the peek.
|
||||||
roomID, err := rsAPI.PerformPeek(req.Context(), &peekReq)
|
if err := rsAPI.PerformPeek(req.Context(), &peekReq, &peekRes); err != nil {
|
||||||
switch e := err.(type) {
|
return util.ErrorResponse(err)
|
||||||
case roomserverAPI.ErrInvalidID:
|
}
|
||||||
return util.JSONResponse{
|
if peekRes.Error != nil {
|
||||||
Code: http.StatusBadRequest,
|
return peekRes.Error.JSONResponse()
|
||||||
JSON: spec.Unknown(e.Error()),
|
|
||||||
}
|
|
||||||
case roomserverAPI.ErrNotAllowed:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden(e.Error()),
|
|
||||||
}
|
|
||||||
case *gomatrix.HTTPError:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: e.Code,
|
|
||||||
JSON: json.RawMessage(e.Message),
|
|
||||||
}
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
logrus.WithError(err).WithField("roomID", roomIDOrAlias).Errorf("Failed to peek room")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if this user is already joined to the room, we let them peek anyway
|
// if this user is already joined to the room, we let them peek anyway
|
||||||
|
@ -94,7 +75,7 @@ func PeekRoomByIDOrAlias(
|
||||||
// TODO: Put the response struct somewhere internal.
|
// TODO: Put the response struct somewhere internal.
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
RoomID string `json:"room_id"`
|
RoomID string `json:"room_id"`
|
||||||
}{roomID},
|
}{peekRes.RoomID},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,20 +85,18 @@ func UnpeekRoomByID(
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||||
roomID string,
|
roomID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
err := rsAPI.PerformUnpeek(req.Context(), roomID, device.UserID, device.ID)
|
unpeekReq := roomserverAPI.PerformUnpeekRequest{
|
||||||
switch e := err.(type) {
|
RoomID: roomID,
|
||||||
case roomserverAPI.ErrInvalidID:
|
UserID: device.UserID,
|
||||||
return util.JSONResponse{
|
DeviceID: device.ID,
|
||||||
Code: http.StatusBadRequest,
|
}
|
||||||
JSON: spec.Unknown(e.Error()),
|
unpeekRes := roomserverAPI.PerformUnpeekResponse{}
|
||||||
}
|
|
||||||
case nil:
|
if err := rsAPI.PerformUnpeek(req.Context(), &unpeekReq, &unpeekRes); err != nil {
|
||||||
default:
|
return jsonerror.InternalAPIError(req.Context(), err)
|
||||||
logrus.WithError(err).WithField("roomID", roomID).Errorf("Failed to un-peek room")
|
}
|
||||||
return util.JSONResponse{
|
if unpeekRes.Error != nil {
|
||||||
Code: http.StatusInternalServerError,
|
return unpeekRes.Error.JSONResponse()
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -21,12 +21,13 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
"github.com/matrix-org/dendrite/syncapi/types"
|
||||||
"github.com/matrix-org/dendrite/userapi/api"
|
"github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/nats-io/nats.go"
|
"github.com/nats-io/nats.go"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
@ -53,7 +54,7 @@ func SetPresence(
|
||||||
if device.UserID != userID {
|
if device.UserID != userID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("Unable to set presence for other user."),
|
JSON: jsonerror.Forbidden("Unable to set presence for other user."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var presence presenceReq
|
var presence presenceReq
|
||||||
|
@ -66,7 +67,7 @@ func SetPresence(
|
||||||
if !ok {
|
if !ok {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.Unknown(fmt.Sprintf("Unknown presence '%s'.", presence.Presence)),
|
JSON: jsonerror.Unknown(fmt.Sprintf("Unknown presence '%s'.", presence.Presence)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
err := producer.SendPresence(req.Context(), userID, presenceStatus, presence.StatusMsg)
|
err := producer.SendPresence(req.Context(), userID, presenceStatus, presence.StatusMsg)
|
||||||
|
@ -74,7 +75,7 @@ func SetPresence(
|
||||||
log.WithError(err).Errorf("failed to update presence")
|
log.WithError(err).Errorf("failed to update presence")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.InternalServerError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,7 +100,7 @@ func GetPresence(
|
||||||
log.WithError(err).Errorf("unable to get presence")
|
log.WithError(err).Errorf("unable to get presence")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.InternalServerError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,11 +119,11 @@ func GetPresence(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.InternalServerError(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
p := types.PresenceInternal{LastActiveTS: spec.Timestamp(lastActive)}
|
p := types.PresenceInternal{LastActiveTS: gomatrixserverlib.Timestamp(lastActive)}
|
||||||
currentlyActive := p.CurrentlyActive()
|
currentlyActive := p.CurrentlyActive()
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
|
@ -16,52 +16,46 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
|
|
||||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrix"
|
"github.com/matrix-org/gomatrix"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetProfile implements GET /profile/{userID}
|
// GetProfile implements GET /profile/{userID}
|
||||||
func GetProfile(
|
func GetProfile(
|
||||||
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == appserviceAPI.ErrProfileNotExists {
|
if err == eventutil.ErrProfileNoExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound("The user does not exist or does not have a profile"),
|
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.UserProfile{
|
JSON: eventutil.ProfileResponse{
|
||||||
AvatarURL: profile.AvatarURL,
|
AvatarURL: profile.AvatarURL,
|
||||||
DisplayName: profile.DisplayName,
|
DisplayName: profile.DisplayName,
|
||||||
},
|
},
|
||||||
|
@ -70,85 +64,125 @@ func GetProfile(
|
||||||
|
|
||||||
// GetAvatarURL implements GET /profile/{userID}/avatar_url
|
// GetAvatarURL implements GET /profile/{userID}/avatar_url
|
||||||
func GetAvatarURL(
|
func GetAvatarURL(
|
||||||
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
|
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
||||||
p, ok := profile.JSON.(eventutil.UserProfile)
|
if err != nil {
|
||||||
// not a profile response, so most likely an error, return that
|
if err == eventutil.ErrProfileNoExists {
|
||||||
if !ok {
|
return util.JSONResponse{
|
||||||
return profile
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.UserProfile{
|
JSON: eventutil.AvatarURL{
|
||||||
AvatarURL: p.AvatarURL,
|
AvatarURL: profile.AvatarURL,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
|
// SetAvatarURL implements PUT /profile/{userID}/avatar_url
|
||||||
func SetAvatarURL(
|
func SetAvatarURL(
|
||||||
req *http.Request, profileAPI userapi.ProfileAPI,
|
req *http.Request, profileAPI userapi.ClientUserAPI,
|
||||||
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID does not match the current user"),
|
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r eventutil.UserProfile
|
var r eventutil.AvatarURL
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
if r.AvatarURL == "" {
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.BadJSON("'avatar_url' must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
return util.JSONResponse{
|
if err != nil {
|
||||||
Code: http.StatusForbidden,
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
JSON: spec.Forbidden("userID does not belong to a locally configured domain"),
|
return jsonerror.InternalServerError()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
profile, changed, err := profileAPI.SetAvatarURL(req.Context(), localpart, domain, r.AvatarURL)
|
res := &userapi.QueryProfileResponse{}
|
||||||
|
err = profileAPI.QueryProfile(req.Context(), &userapi.QueryProfileRequest{
|
||||||
|
UserID: userID,
|
||||||
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.QueryProfile failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
oldProfile := &authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: res.DisplayName,
|
||||||
|
AvatarURL: res.AvatarURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
setRes := &userapi.PerformSetAvatarURLResponse{}
|
||||||
|
if err = profileAPI.SetAvatarURL(req.Context(), &userapi.PerformSetAvatarURLRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
AvatarURL: r.AvatarURL,
|
||||||
|
}, setRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetAvatarURL failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No need to build new membership events, since nothing changed
|
|
||||||
if !changed {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, evTime)
|
var roomsRes api.QueryRoomsForUserResponse
|
||||||
|
err = rsAPI.QueryRoomsForUser(req.Context(), &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
WantMembership: "join",
|
||||||
|
}, &roomsRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response
|
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
newProfile := authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: oldProfile.DisplayName,
|
||||||
|
AvatarURL: r.AvatarURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := buildMembershipEvents(
|
||||||
|
req.Context(), roomsRes.RoomIDs, newProfile, userID, cfg, evTime, rsAPI,
|
||||||
|
)
|
||||||
|
switch e := err.(type) {
|
||||||
|
case nil:
|
||||||
|
case gomatrixserverlib.BadJSONError:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON(e.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.SendEvents(req.Context(), rsAPI, api.KindNew, events, cfg.Matrix.ServerName, cfg.Matrix.ServerName, nil, true); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("SendEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -159,85 +193,125 @@ func SetAvatarURL(
|
||||||
|
|
||||||
// GetDisplayName implements GET /profile/{userID}/displayname
|
// GetDisplayName implements GET /profile/{userID}/displayname
|
||||||
func GetDisplayName(
|
func GetDisplayName(
|
||||||
req *http.Request, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
userID string, asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
profile := GetProfile(req, profileAPI, cfg, userID, asAPI, federation)
|
profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation)
|
||||||
p, ok := profile.JSON.(eventutil.UserProfile)
|
if err != nil {
|
||||||
// not a profile response, so most likely an error, return that
|
if err == eventutil.ErrProfileNoExists {
|
||||||
if !ok {
|
return util.JSONResponse{
|
||||||
return profile
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The user does not exist or does not have a profile"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("getProfile failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: eventutil.UserProfile{
|
JSON: eventutil.DisplayName{
|
||||||
DisplayName: p.DisplayName,
|
DisplayName: profile.DisplayName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDisplayName implements PUT /profile/{userID}/displayname
|
// SetDisplayName implements PUT /profile/{userID}/displayname
|
||||||
func SetDisplayName(
|
func SetDisplayName(
|
||||||
req *http.Request, profileAPI userapi.ProfileAPI,
|
req *http.Request, profileAPI userapi.ClientUserAPI,
|
||||||
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if userID != device.UserID {
|
if userID != device.UserID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden("userID does not match the current user"),
|
JSON: jsonerror.Forbidden("userID does not match the current user"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r eventutil.UserProfile
|
var r eventutil.DisplayName
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
if r.DisplayName == "" {
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.BadJSON("'displayname' must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
return util.JSONResponse{
|
if err != nil {
|
||||||
Code: http.StatusForbidden,
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
JSON: spec.Forbidden("userID does not belong to a locally configured domain"),
|
return jsonerror.InternalServerError()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
evTime, err := httputil.ParseTSParam(req)
|
evTime, err := httputil.ParseTSParam(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(err.Error()),
|
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
profile, changed, err := profileAPI.SetDisplayName(req.Context(), localpart, domain, r.DisplayName)
|
pRes := &userapi.QueryProfileResponse{}
|
||||||
|
err = profileAPI.QueryProfile(req.Context(), &userapi.QueryProfileRequest{
|
||||||
|
UserID: userID,
|
||||||
|
}, pRes)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.QueryProfile failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
oldProfile := &authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: pRes.DisplayName,
|
||||||
|
AvatarURL: pRes.AvatarURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
err = profileAPI.SetDisplayName(req.Context(), &userapi.PerformUpdateDisplayNameRequest{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
}, &struct{}{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
|
util.GetLogger(req.Context()).WithError(err).Error("profileAPI.SetDisplayName failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// No need to build new membership events, since nothing changed
|
|
||||||
if !changed {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct{}{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response, err := updateProfile(req.Context(), rsAPI, device, profile, userID, evTime)
|
var res api.QueryRoomsForUserResponse
|
||||||
|
err = rsAPI.QueryRoomsForUser(req.Context(), &api.QueryRoomsForUserRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
WantMembership: "join",
|
||||||
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return response
|
util.GetLogger(req.Context()).WithError(err).Error("QueryRoomsForUser failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
newProfile := authtypes.Profile{
|
||||||
|
Localpart: localpart,
|
||||||
|
DisplayName: r.DisplayName,
|
||||||
|
AvatarURL: oldProfile.AvatarURL,
|
||||||
|
}
|
||||||
|
|
||||||
|
events, err := buildMembershipEvents(
|
||||||
|
req.Context(), res.RoomIDs, newProfile, userID, cfg, evTime, rsAPI,
|
||||||
|
)
|
||||||
|
switch e := err.(type) {
|
||||||
|
case nil:
|
||||||
|
case gomatrixserverlib.BadJSONError:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON(e.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := api.SendEvents(req.Context(), rsAPI, api.KindNew, events, cfg.Matrix.ServerName, cfg.Matrix.ServerName, nil, true); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("SendEvents failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -246,91 +320,27 @@ func SetDisplayName(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateProfile(
|
|
||||||
ctx context.Context, rsAPI api.ClientRoomserverAPI, device *userapi.Device,
|
|
||||||
profile *authtypes.Profile,
|
|
||||||
userID string, evTime time.Time,
|
|
||||||
) (util.JSONResponse, error) {
|
|
||||||
deviceUserID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
rooms, err := rsAPI.QueryRoomsForUser(ctx, *deviceUserID, "join")
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("QueryRoomsForUser failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
roomIDStrs := make([]string, len(rooms))
|
|
||||||
for i, room := range rooms {
|
|
||||||
roomIDStrs[i] = room.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
events, err := buildMembershipEvents(
|
|
||||||
ctx, roomIDStrs, *profile, userID, evTime, rsAPI,
|
|
||||||
)
|
|
||||||
switch e := err.(type) {
|
|
||||||
case nil:
|
|
||||||
case gomatrixserverlib.BadJSONError:
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON(e.Error()),
|
|
||||||
}, e
|
|
||||||
default:
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvents failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}, e
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := api.SendEvents(ctx, rsAPI, api.KindNew, events, device.UserDomain(), domain, domain, nil, false); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}, err
|
|
||||||
}
|
|
||||||
return util.JSONResponse{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getProfile gets the full profile of a user by querying the database or a
|
// getProfile gets the full profile of a user by querying the database or a
|
||||||
// remote homeserver.
|
// remote homeserver.
|
||||||
// Returns an error when something goes wrong or specifically
|
// Returns an error when something goes wrong or specifically
|
||||||
// eventutil.ErrProfileNotExists when the profile doesn't exist.
|
// eventutil.ErrProfileNoExists when the profile doesn't exist.
|
||||||
func getProfile(
|
func getProfile(
|
||||||
ctx context.Context, profileAPI userapi.ProfileAPI, cfg *config.ClientAPI,
|
ctx context.Context, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
) (*authtypes.Profile, error) {
|
) (*authtypes.Profile, error) {
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
localpart, domain, err := gomatrixserverlib.SplitID('@', userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if domain != cfg.Matrix.ServerName {
|
||||||
profile, fedErr := federation.LookupProfile(ctx, cfg.Matrix.ServerName, domain, userID, "")
|
profile, fedErr := federation.LookupProfile(ctx, domain, userID, "")
|
||||||
if fedErr != nil {
|
if fedErr != nil {
|
||||||
if x, ok := fedErr.(gomatrix.HTTPError); ok {
|
if x, ok := fedErr.(gomatrix.HTTPError); ok {
|
||||||
if x.Code == http.StatusNotFound {
|
if x.Code == http.StatusNotFound {
|
||||||
return nil, appserviceAPI.ErrProfileNotExists
|
return nil, eventutil.ErrProfileNoExists
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -355,61 +365,42 @@ func getProfile(
|
||||||
func buildMembershipEvents(
|
func buildMembershipEvents(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
roomIDs []string,
|
roomIDs []string,
|
||||||
newProfile authtypes.Profile, userID string,
|
newProfile authtypes.Profile, userID string, cfg *config.ClientAPI,
|
||||||
evTime time.Time, rsAPI api.ClientRoomserverAPI,
|
evTime time.Time, rsAPI api.ClientRoomserverAPI,
|
||||||
) ([]*types.HeaderedEvent, error) {
|
) ([]*gomatrixserverlib.HeaderedEvent, error) {
|
||||||
evs := []*types.HeaderedEvent{}
|
evs := []*gomatrixserverlib.HeaderedEvent{}
|
||||||
|
|
||||||
fullUserID, err := spec.NewUserID(userID, true)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, roomID := range roomIDs {
|
for _, roomID := range roomIDs {
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID}
|
||||||
if err != nil {
|
verRes := api.QueryRoomVersionForRoomResponse{}
|
||||||
|
if err := rsAPI.QueryRoomVersionForRoom(ctx, &verReq, &verRes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
senderID, err := rsAPI.QuerySenderIDForUser(ctx, *validRoomID, *fullUserID)
|
|
||||||
if err != nil {
|
builder := gomatrixserverlib.EventBuilder{
|
||||||
return nil, err
|
Sender: userID,
|
||||||
} else if senderID == nil {
|
|
||||||
return nil, fmt.Errorf("sender ID not found for %s in %s", *fullUserID, *validRoomID)
|
|
||||||
}
|
|
||||||
senderIDString := string(*senderID)
|
|
||||||
proto := gomatrixserverlib.ProtoEvent{
|
|
||||||
SenderID: senderIDString,
|
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Type: "m.room.member",
|
Type: "m.room.member",
|
||||||
StateKey: &senderIDString,
|
StateKey: &userID,
|
||||||
}
|
}
|
||||||
|
|
||||||
content := gomatrixserverlib.MemberContent{
|
content := gomatrixserverlib.MemberContent{
|
||||||
Membership: spec.Join,
|
Membership: gomatrixserverlib.Join,
|
||||||
}
|
}
|
||||||
|
|
||||||
content.DisplayName = newProfile.DisplayName
|
content.DisplayName = newProfile.DisplayName
|
||||||
content.AvatarURL = newProfile.AvatarURL
|
content.AvatarURL = newProfile.AvatarURL
|
||||||
|
|
||||||
if err = proto.SetContent(content); err != nil {
|
if err := builder.SetContent(content); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := spec.NewUserID(userID, true)
|
event, err := eventutil.QueryAndBuildEvent(ctx, &builder, cfg.Matrix, evTime, rsAPI, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
identity, err := rsAPI.SigningIdentityFor(ctx, *validRoomID, *user)
|
evs = append(evs, event.Headered(verRes.RoomVersion))
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
event, err := eventutil.QueryAndBuildEvent(ctx, &proto, &identity, evTime, rsAPI, nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
evs = append(evs, event)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return evs, nil
|
return evs, nil
|
||||||
|
|
|
@ -19,9 +19,9 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -31,24 +31,17 @@ func GetPushers(
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var queryRes userapi.QueryPushersResponse
|
var queryRes userapi.QueryPushersResponse
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
err = userAPI.QueryPushers(req.Context(), &userapi.QueryPushersRequest{
|
err = userAPI.QueryPushers(req.Context(), &userapi.QueryPushersRequest{
|
||||||
Localpart: localpart,
|
Localpart: localpart,
|
||||||
ServerName: domain,
|
|
||||||
}, &queryRes)
|
}, &queryRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("QueryPushers failed")
|
util.GetLogger(req.Context()).WithError(err).Error("QueryPushers failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
for i := range queryRes.Pushers {
|
for i := range queryRes.Pushers {
|
||||||
queryRes.Pushers[i].SessionID = 0
|
queryRes.Pushers[i].SessionID = 0
|
||||||
|
@ -66,13 +59,10 @@ func SetPusher(
|
||||||
req *http.Request, device *userapi.Device,
|
req *http.Request, device *userapi.Device,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
localpart, domain, err := gomatrixserverlib.SplitID('@', device.UserID)
|
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("SplitID failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
body := userapi.PerformPusherSetRequest{}
|
body := userapi.PerformPusherSetRequest{}
|
||||||
if resErr := httputil.UnmarshalJSONRequest(req, &body); resErr != nil {
|
if resErr := httputil.UnmarshalJSONRequest(req, &body); resErr != nil {
|
||||||
|
@ -103,15 +93,11 @@ func SetPusher(
|
||||||
|
|
||||||
}
|
}
|
||||||
body.Localpart = localpart
|
body.Localpart = localpart
|
||||||
body.ServerName = domain
|
|
||||||
body.SessionID = device.SessionID
|
body.SessionID = device.SessionID
|
||||||
err = userAPI.PerformPusherSet(req.Context(), &body, &struct{}{})
|
err = userAPI.PerformPusherSet(req.Context(), &body, &struct{}{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherSet failed")
|
util.GetLogger(req.Context()).WithError(err).Error("PerformPusherSet failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -123,6 +109,6 @@ func SetPusher(
|
||||||
func invalidParam(msg string) util.JSONResponse {
|
func invalidParam(msg string) util.JSONResponse {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidParam(msg),
|
JSON: jsonerror.InvalidParam(msg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,34 +7,31 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/internal/pushrules"
|
"github.com/matrix-org/dendrite/internal/pushrules"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func errorResponse(ctx context.Context, err error, msg string, args ...interface{}) util.JSONResponse {
|
func errorResponse(ctx context.Context, err error, msg string, args ...interface{}) util.JSONResponse {
|
||||||
if eerr, ok := err.(spec.MatrixError); ok {
|
if eerr, ok := err.(*jsonerror.MatrixError); ok {
|
||||||
var status int
|
var status int
|
||||||
switch eerr.ErrCode {
|
switch eerr.ErrCode {
|
||||||
case spec.ErrorInvalidParam:
|
case "M_INVALID_ARGUMENT_VALUE":
|
||||||
status = http.StatusBadRequest
|
status = http.StatusBadRequest
|
||||||
case spec.ErrorNotFound:
|
case "M_NOT_FOUND":
|
||||||
status = http.StatusNotFound
|
status = http.StatusNotFound
|
||||||
default:
|
default:
|
||||||
status = http.StatusInternalServerError
|
status = http.StatusInternalServerError
|
||||||
}
|
}
|
||||||
return util.MatrixErrorResponse(status, string(eerr.ErrCode), eerr.Err)
|
return util.MatrixErrorResponse(status, eerr.ErrCode, eerr.Err)
|
||||||
}
|
}
|
||||||
util.GetLogger(ctx).WithError(err).Errorf(msg, args...)
|
util.GetLogger(ctx).WithError(err).Errorf(msg, args...)
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
||||||
}
|
}
|
||||||
|
@ -45,13 +42,13 @@ func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userap
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
return errorResponse(ctx, err, "queryPushRulesJSON failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -60,18 +57,17 @@ func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Devi
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
// Even if rulesPtr is not nil, there may not be any rules for this kind
|
if rulesPtr == nil {
|
||||||
if rulesPtr == nil || (rulesPtr != nil && len(*rulesPtr) == 0) {
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -80,21 +76,21 @@ func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -105,30 +101,26 @@ func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device
|
||||||
func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
var newRule pushrules.Rule
|
var newRule pushrules.Rule
|
||||||
if err := json.NewDecoder(body).Decode(&newRule); err != nil {
|
if err := json.NewDecoder(body).Decode(&newRule); err != nil {
|
||||||
return util.JSONResponse{
|
return errorResponse(ctx, err, "JSON Decode failed")
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON(err.Error()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
newRule.RuleID = ruleID
|
newRule.RuleID = ruleID
|
||||||
|
|
||||||
errs := pushrules.ValidateRule(pushrules.Kind(kind), &newRule)
|
errs := pushrules.ValidateRule(pushrules.Kind(kind), &newRule)
|
||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
return errorResponse(ctx, spec.InvalidParam(errs[0].Error()), "rule sanity check failed: %v", errs)
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue(errs[0].Error()), "rule sanity check failed: %v", errs)
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
// while this should be impossible (ValidateRule would already return an error), better keep it around
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i >= 0 && afterRuleID == "" && beforeRuleID == "" {
|
if i >= 0 && afterRuleID == "" && beforeRuleID == "" {
|
||||||
|
@ -152,7 +144,7 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add new rule.
|
// Add new rule.
|
||||||
i, err = findPushRuleInsertionIndex(*rulesPtr, afterRuleID, beforeRuleID)
|
i, err := findPushRuleInsertionIndex(*rulesPtr, afterRuleID, beforeRuleID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "findPushRuleInsertionIndex failed")
|
return errorResponse(ctx, err, "findPushRuleInsertionIndex failed")
|
||||||
}
|
}
|
||||||
|
@ -161,7 +153,7 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
util.GetLogger(ctx).WithField("after", afterRuleID).WithField("before", beforeRuleID).Infof("Added new push rule at %d", i)
|
util.GetLogger(ctx).WithField("after", afterRuleID).WithField("before", beforeRuleID).Infof("Added new push rule at %d", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,26 +161,26 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID,
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
*rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
|
*rulesPtr = append((*rulesPtr)[:i], (*rulesPtr)[i+1:]...)
|
||||||
|
|
||||||
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -200,21 +192,21 @@ func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "pushRuleAttrGetter failed")
|
return errorResponse(ctx, err, "pushRuleAttrGetter failed")
|
||||||
}
|
}
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
@ -229,7 +221,7 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil {
|
if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.BadJSON(err.Error()),
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if newPartialRule.Actions == nil {
|
if newPartialRule.Actions == nil {
|
||||||
|
@ -246,27 +238,27 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
return errorResponse(ctx, err, "pushRuleAttrSetter failed")
|
return errorResponse(ctx, err, "pushRuleAttrSetter failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
ruleSets, err := userAPI.QueryPushRules(ctx, device.UserID)
|
ruleSets, err := queryPushRules(ctx, device.UserID, userAPI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errorResponse(ctx, err, "queryPushRules failed")
|
return errorResponse(ctx, err, "queryPushRules failed")
|
||||||
}
|
}
|
||||||
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
ruleSet := pushRuleSetByScope(ruleSets, pushrules.Scope(scope))
|
||||||
if ruleSet == nil {
|
if ruleSet == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rule set"), "pushRuleSetByScope failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rule set"), "pushRuleSetByScope failed")
|
||||||
}
|
}
|
||||||
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
rulesPtr := pushRuleSetKindPointer(ruleSet, pushrules.Kind(kind))
|
||||||
if rulesPtr == nil {
|
if rulesPtr == nil {
|
||||||
return errorResponse(ctx, spec.InvalidParam("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
return errorResponse(ctx, jsonerror.InvalidArgumentValue("invalid push rules kind"), "pushRuleSetKindPointer failed")
|
||||||
}
|
}
|
||||||
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
i := pushRuleIndexByID(*rulesPtr, ruleID)
|
||||||
if i < 0 {
|
if i < 0 {
|
||||||
return errorResponse(ctx, spec.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
return errorResponse(ctx, jsonerror.NotFound("push rule ID not found"), "pushRuleIndexByID failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(attrGet((*rulesPtr)[i]), attrGet(&newPartialRule)) {
|
if !reflect.DeepEqual(attrGet((*rulesPtr)[i]), attrGet(&newPartialRule)) {
|
||||||
attrSet((*rulesPtr)[i], &newPartialRule)
|
attrSet((*rulesPtr)[i], &newPartialRule)
|
||||||
|
|
||||||
if err = userAPI.PerformPushRulesPut(ctx, device.UserID, ruleSets); err != nil {
|
if err := putPushRules(ctx, device.UserID, ruleSets, userAPI); err != nil {
|
||||||
return errorResponse(ctx, err, "putPushRules failed")
|
return errorResponse(ctx, err, "putPushRules failed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,6 +266,28 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
|
return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func queryPushRules(ctx context.Context, userID string, userAPI userapi.ClientUserAPI) (*pushrules.AccountRuleSets, error) {
|
||||||
|
var res userapi.QueryPushRulesResponse
|
||||||
|
if err := userAPI.QueryPushRules(ctx, &userapi.QueryPushRulesRequest{UserID: userID}, &res); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("userAPI.QueryPushRules failed")
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res.RuleSets, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func putPushRules(ctx context.Context, userID string, ruleSets *pushrules.AccountRuleSets, userAPI userapi.ClientUserAPI) error {
|
||||||
|
req := userapi.PerformPushRulesPutRequest{
|
||||||
|
UserID: userID,
|
||||||
|
RuleSets: ruleSets,
|
||||||
|
}
|
||||||
|
var res struct{}
|
||||||
|
if err := userAPI.PerformPushRulesPut(ctx, &req, &res); err != nil {
|
||||||
|
util.GetLogger(ctx).WithError(err).Error("userAPI.PerformPushRulesPut failed")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func pushRuleSetByScope(ruleSets *pushrules.AccountRuleSets, scope pushrules.Scope) *pushrules.RuleSet {
|
func pushRuleSetByScope(ruleSets *pushrules.AccountRuleSets, scope pushrules.Scope) *pushrules.RuleSet {
|
||||||
switch scope {
|
switch scope {
|
||||||
case pushrules.GlobalScope:
|
case pushrules.GlobalScope:
|
||||||
|
@ -316,7 +330,7 @@ func pushRuleAttrGetter(attr string) (func(*pushrules.Rule) interface{}, error)
|
||||||
case "enabled":
|
case "enabled":
|
||||||
return func(rule *pushrules.Rule) interface{} { return rule.Enabled }, nil
|
return func(rule *pushrules.Rule) interface{} { return rule.Enabled }, nil
|
||||||
default:
|
default:
|
||||||
return nil, spec.InvalidParam("invalid push rule attribute")
|
return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -327,7 +341,7 @@ func pushRuleAttrSetter(attr string) (func(dest, src *pushrules.Rule), error) {
|
||||||
case "enabled":
|
case "enabled":
|
||||||
return func(dest, src *pushrules.Rule) { dest.Enabled = src.Enabled }, nil
|
return func(dest, src *pushrules.Rule) { dest.Enabled = src.Enabled }, nil
|
||||||
default:
|
default:
|
||||||
return nil, spec.InvalidParam("invalid push rule attribute")
|
return nil, jsonerror.InvalidArgumentValue("invalid push rule attribute")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -341,10 +355,10 @@ func findPushRuleInsertionIndex(rules []*pushrules.Rule, afterID, beforeID strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i == len(rules) {
|
if i == len(rules) {
|
||||||
return 0, spec.NotFound("after: rule ID not found")
|
return 0, jsonerror.NotFound("after: rule ID not found")
|
||||||
}
|
}
|
||||||
if rules[i].Default {
|
if rules[i].Default {
|
||||||
return 0, spec.NotFound("after: rule ID must not be a default rule")
|
return 0, jsonerror.NotFound("after: rule ID must not be a default rule")
|
||||||
}
|
}
|
||||||
// We stopped on the "after" match to differentiate
|
// We stopped on the "after" match to differentiate
|
||||||
// not-found from is-last-entry. Now we move to the earliest
|
// not-found from is-last-entry. Now we move to the earliest
|
||||||
|
@ -359,10 +373,10 @@ func findPushRuleInsertionIndex(rules []*pushrules.Rule, afterID, beforeID strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if i == len(rules) {
|
if i == len(rules) {
|
||||||
return 0, spec.NotFound("before: rule ID not found")
|
return 0, jsonerror.NotFound("before: rule ID not found")
|
||||||
}
|
}
|
||||||
if rules[i].Default {
|
if rules[i].Default {
|
||||||
return 0, spec.NotFound("before: rule ID must not be a default rule")
|
return 0, jsonerror.NotFound("before: rule ID must not be a default rule")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,21 +15,20 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetReceipt(req *http.Request, userAPI userapi.ClientUserAPI, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
|
func SetReceipt(req *http.Request, syncProducer *producers.SyncAPIProducer, device *userapi.Device, roomID, receiptType, eventID string) util.JSONResponse {
|
||||||
timestamp := spec.AsTimestamp(time.Now())
|
timestamp := gomatrixserverlib.AsTimestamp(time.Now())
|
||||||
logrus.WithFields(logrus.Fields{
|
logrus.WithFields(logrus.Fields{
|
||||||
"roomID": roomID,
|
"roomID": roomID,
|
||||||
"receiptType": receiptType,
|
"receiptType": receiptType,
|
||||||
|
@ -38,35 +37,13 @@ func SetReceipt(req *http.Request, userAPI userapi.ClientUserAPI, syncProducer *
|
||||||
"timestamp": timestamp,
|
"timestamp": timestamp,
|
||||||
}).Debug("Setting receipt")
|
}).Debug("Setting receipt")
|
||||||
|
|
||||||
switch receiptType {
|
// currently only m.read is accepted
|
||||||
case "m.read", "m.read.private":
|
if receiptType != "m.read" {
|
||||||
if err := syncProducer.SendReceipt(req.Context(), device.UserID, roomID, eventID, receiptType, timestamp); err != nil {
|
return util.MessageResponse(400, fmt.Sprintf("receipt type must be m.read not '%s'", receiptType))
|
||||||
return util.ErrorResponse(err)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case "m.fully_read":
|
if err := syncProducer.SendReceipt(req.Context(), device.UserID, roomID, eventID, receiptType, timestamp); err != nil {
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: eventID})
|
return util.ErrorResponse(err)
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dataReq := userapi.InputAccountDataRequest{
|
|
||||||
UserID: device.UserID,
|
|
||||||
DataType: "m.fully_read",
|
|
||||||
RoomID: roomID,
|
|
||||||
AccountData: data,
|
|
||||||
}
|
|
||||||
dataRes := userapi.InputAccountDataResponse{}
|
|
||||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
|
||||||
return util.ErrorResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
return util.MessageResponse(400, fmt.Sprintf("Receipt type '%s' not known", receiptType))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
|
@ -16,26 +16,22 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/internal/transactions"
|
"github.com/matrix-org/dendrite/internal/transactions"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/types"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type redactionContent struct {
|
type redactionContent struct {
|
||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
Redacts string `json:"redacts"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type redactionResponse struct {
|
type redactionResponse struct {
|
||||||
|
@ -48,94 +44,62 @@ func SendRedaction(
|
||||||
txnID *string,
|
txnID *string,
|
||||||
txnCache *transactions.Cache,
|
txnCache *transactions.Cache,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
deviceUserID, userIDErr := spec.NewUserID(device.UserID, true)
|
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
if userIDErr != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("userID doesn't have power level to redact"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
validRoomID, err := spec.NewRoomID(roomID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("RoomID is invalid"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
senderID, queryErr := rsAPI.QuerySenderIDForUser(req.Context(), *validRoomID, *deviceUserID)
|
|
||||||
if queryErr != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("userID doesn't have power level to redact"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
// if user is member of room, and sender ID is nil, then this user doesn't have a pseudo ID for some reason,
|
|
||||||
// which is unexpected.
|
|
||||||
if senderID == nil {
|
|
||||||
util.GetLogger(req.Context()).WithField("userID", *deviceUserID).WithField("roomID", roomID).Error("missing sender ID for user, despite having membership")
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("internal server error"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if txnID != nil {
|
if txnID != nil {
|
||||||
// Try to fetch response from transactionsCache
|
// Try to fetch response from transactionsCache
|
||||||
if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID, req.URL); ok {
|
if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID); ok {
|
||||||
return *res
|
return *res
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ev := roomserverAPI.GetEvent(req.Context(), rsAPI, roomID, eventID)
|
ev := roomserverAPI.GetEvent(req.Context(), rsAPI, eventID)
|
||||||
if ev == nil {
|
if ev == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: spec.NotFound("unknown event ID"), // TODO: is it ok to leak existence?
|
JSON: jsonerror.NotFound("unknown event ID"), // TODO: is it ok to leak existence?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ev.RoomID().String() != roomID {
|
if ev.RoomID() != roomID {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: spec.NotFound("cannot redact event in another room"),
|
JSON: jsonerror.NotFound("cannot redact event in another room"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// "Users may redact their own events, and any user with a power level greater than or equal
|
// "Users may redact their own events, and any user with a power level greater than or equal
|
||||||
// to the redact power level of the room may redact events there"
|
// to the redact power level of the room may redact events there"
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
|
// https://matrix.org/docs/spec/client_server/r0.6.1#put-matrix-client-r0-rooms-roomid-redact-eventid-txnid
|
||||||
allowedToRedact := ev.SenderID() == *senderID
|
allowedToRedact := ev.Sender() == device.UserID
|
||||||
if !allowedToRedact {
|
if !allowedToRedact {
|
||||||
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
EventType: spec.MRoomPowerLevels,
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
||||||
StateKey: "",
|
StateKey: "",
|
||||||
})
|
})
|
||||||
if plEvent == nil {
|
if plEvent == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: spec.Forbidden("You don't have permission to redact this event, no power_levels event in this room."),
|
JSON: jsonerror.Forbidden("You don't have permission to redact this event, no power_levels event in this room."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pl, plErr := plEvent.PowerLevels()
|
pl, err := plEvent.PowerLevels()
|
||||||
if plErr != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: spec.Forbidden(
|
JSON: jsonerror.Forbidden(
|
||||||
"You don't have permission to redact this event, the power_levels event for this room is malformed so auth checks cannot be performed.",
|
"You don't have permission to redact this event, the power_levels event for this room is malformed so auth checks cannot be performed.",
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
allowedToRedact = pl.UserLevel(*senderID) >= pl.Redact
|
allowedToRedact = pl.UserLevel(device.UserID) >= pl.Redact
|
||||||
}
|
}
|
||||||
if !allowedToRedact {
|
if !allowedToRedact {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: spec.Forbidden("You don't have permission to redact this event, power level too low."),
|
JSON: jsonerror.Forbidden("You don't have permission to redact this event, power level too low."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,49 +110,29 @@ func SendRedaction(
|
||||||
}
|
}
|
||||||
|
|
||||||
// create the new event and set all the fields we can
|
// create the new event and set all the fields we can
|
||||||
proto := gomatrixserverlib.ProtoEvent{
|
builder := gomatrixserverlib.EventBuilder{
|
||||||
SenderID: string(*senderID),
|
Sender: device.UserID,
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
Type: spec.MRoomRedaction,
|
Type: gomatrixserverlib.MRoomRedaction,
|
||||||
Redacts: eventID,
|
Redacts: eventID,
|
||||||
}
|
}
|
||||||
|
err := builder.SetContent(r)
|
||||||
// Room version 11 expects the "redacts" field on the
|
|
||||||
// content field, so add it here as well
|
|
||||||
r.Redacts = eventID
|
|
||||||
|
|
||||||
err = proto.SetContent(r)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("proto.SetContent failed")
|
util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
identity, err := rsAPI.SigningIdentityFor(req.Context(), *validRoomID, *deviceUserID)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var queryRes roomserverAPI.QueryLatestEventsAndStateResponse
|
var queryRes roomserverAPI.QueryLatestEventsAndStateResponse
|
||||||
e, err := eventutil.QueryAndBuildEvent(req.Context(), &proto, &identity, time.Now(), rsAPI, &queryRes)
|
e, err := eventutil.QueryAndBuildEvent(req.Context(), &builder, cfg.Matrix, time.Now(), rsAPI, &queryRes)
|
||||||
if errors.Is(err, eventutil.ErrRoomNoExists{}) {
|
if err == eventutil.ErrRoomNoExists {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: spec.NotFound("Room does not exist"),
|
JSON: jsonerror.NotFound("Room does not exist"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
domain := device.UserDomain()
|
if err = roomserverAPI.SendEvents(context.Background(), rsAPI, roomserverAPI.KindNew, []*gomatrixserverlib.HeaderedEvent{e}, cfg.Matrix.ServerName, cfg.Matrix.ServerName, nil, false); err != nil {
|
||||||
if err = roomserverAPI.SendEvents(context.Background(), rsAPI, roomserverAPI.KindNew, []*types.HeaderedEvent{e}, device.UserDomain(), domain, domain, nil, false); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents")
|
util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res := util.JSONResponse{
|
res := util.JSONResponse{
|
||||||
|
@ -200,7 +144,7 @@ func SendRedaction(
|
||||||
|
|
||||||
// Add response to transactionsCache
|
// Add response to transactionsCache
|
||||||
if txnID != nil {
|
if txnID != nil {
|
||||||
txnCache.AddTransaction(device.AccessToken, *txnID, req.URL, &res)
|
txnCache.AddTransaction(device.AccessToken, *txnID, &res)
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
|
@ -18,26 +18,23 @@ package routing
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"regexp"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal"
|
|
||||||
"github.com/tidwall/gjson"
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/tokens"
|
"github.com/matrix-org/gomatrixserverlib/tokens"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
@ -46,6 +43,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth"
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
)
|
)
|
||||||
|
@ -60,7 +58,12 @@ var (
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
const sessionIDLength = 24
|
const (
|
||||||
|
minPasswordLength = 8 // http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
|
||||||
|
maxPasswordLength = 512 // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
|
||||||
|
maxUsernameLength = 254 // http://matrix.org/speculator/spec/HEAD/intro.html#user-identifiers TODO account for domain
|
||||||
|
sessionIDLength = 24
|
||||||
|
)
|
||||||
|
|
||||||
// sessionsDict keeps track of completed auth stages for each session.
|
// sessionsDict keeps track of completed auth stages for each session.
|
||||||
// It shouldn't be passed by value because it contains a mutex.
|
// It shouldn't be passed by value because it contains a mutex.
|
||||||
|
@ -164,7 +167,7 @@ func (d *sessionsDict) addCompletedSessionStage(sessionID string, stage authtype
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
d.sessions[sessionID] = append(d.sessions[sessionID], stage)
|
d.sessions[sessionID] = append(sessions.sessions[sessionID], stage)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *sessionsDict) addDeviceToDelete(sessionID, deviceID string) {
|
func (d *sessionsDict) addDeviceToDelete(sessionID, deviceID string) {
|
||||||
|
@ -195,7 +198,8 @@ func (d *sessionsDict) getDeviceToDelete(sessionID string) (string, bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
sessions = newSessionsDict()
|
sessions = newSessionsDict()
|
||||||
|
validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-=./]+$`)
|
||||||
)
|
)
|
||||||
|
|
||||||
// registerRequest represents the submitted registration request.
|
// registerRequest represents the submitted registration request.
|
||||||
|
@ -206,10 +210,9 @@ var (
|
||||||
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
// previous parameters with the ones supplied. This mean you cannot "build up" request params.
|
||||||
type registerRequest struct {
|
type registerRequest struct {
|
||||||
// registration parameters
|
// registration parameters
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
ServerName spec.ServerName `json:"-"`
|
Admin bool `json:"admin"`
|
||||||
Admin bool `json:"admin"`
|
|
||||||
// user-interactive auth params
|
// user-interactive auth params
|
||||||
Auth authDict `json:"auth"`
|
Auth authDict `json:"auth"`
|
||||||
|
|
||||||
|
@ -236,7 +239,7 @@ type authDict struct {
|
||||||
// TODO: Lots of custom keys depending on the type
|
// TODO: Lots of custom keys depending on the type
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://spec.matrix.org/v1.7/client-server-api/#user-interactive-authentication-api
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
|
||||||
type userInteractiveResponse struct {
|
type userInteractiveResponse struct {
|
||||||
Flows []authtypes.Flow `json:"flows"`
|
Flows []authtypes.Flow `json:"flows"`
|
||||||
Completed []authtypes.LoginType `json:"completed"`
|
Completed []authtypes.LoginType `json:"completed"`
|
||||||
|
@ -256,11 +259,12 @@ func newUserInteractiveResponse(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
||||||
type registerResponse struct {
|
type registerResponse struct {
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
AccessToken string `json:"access_token,omitempty"`
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
DeviceID string `json:"device_id,omitempty"`
|
HomeServer gomatrixserverlib.ServerName `json:"home_server"`
|
||||||
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// recaptchaResponse represents the HTTP response from a Google Recaptcha server
|
// recaptchaResponse represents the HTTP response from a Google Recaptcha server
|
||||||
|
@ -271,38 +275,95 @@ type recaptchaResponse struct {
|
||||||
ErrorCodes []int `json:"error-codes"`
|
ErrorCodes []int `json:"error-codes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// validateUsername returns an error response if the username is invalid
|
||||||
ErrInvalidCaptcha = errors.New("invalid captcha response")
|
func validateUsername(localpart string, domain gomatrixserverlib.ServerName) *util.JSONResponse {
|
||||||
ErrMissingResponse = errors.New("captcha response is required")
|
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
|
||||||
ErrCaptchaDisabled = errors.New("captcha registration is disabled")
|
if id := fmt.Sprintf("@%s:%s", localpart, domain); len(id) > maxUsernameLength {
|
||||||
)
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON(fmt.Sprintf("%q exceeds the maximum length of %d characters", id, maxUsernameLength)),
|
||||||
|
}
|
||||||
|
} else if !validUsernameRegex.MatchString(localpart) {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./='"),
|
||||||
|
}
|
||||||
|
} else if localpart[0] == '_' { // Regex checks its not a zero length string
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidUsername("Username cannot start with a '_'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateApplicationServiceUsername returns an error response if the username is invalid for an application service
|
||||||
|
func validateApplicationServiceUsername(localpart string, domain gomatrixserverlib.ServerName) *util.JSONResponse {
|
||||||
|
if id := fmt.Sprintf("@%s:%s", localpart, domain); len(id) > maxUsernameLength {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON(fmt.Sprintf("%q exceeds the maximum length of %d characters", id, maxUsernameLength)),
|
||||||
|
}
|
||||||
|
} else if !validUsernameRegex.MatchString(localpart) {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./='"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validatePassword returns an error response if the password is invalid
|
||||||
|
func validatePassword(password string) *util.JSONResponse {
|
||||||
|
// https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
|
||||||
|
if len(password) > maxPasswordLength {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)),
|
||||||
|
}
|
||||||
|
} else if len(password) > 0 && len(password) < minPasswordLength {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// validateRecaptcha returns an error response if the captcha response is invalid
|
// validateRecaptcha returns an error response if the captcha response is invalid
|
||||||
func validateRecaptcha(
|
func validateRecaptcha(
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
response string,
|
response string,
|
||||||
clientip string,
|
clientip string,
|
||||||
) error {
|
) *util.JSONResponse {
|
||||||
ip, _, _ := net.SplitHostPort(clientip)
|
|
||||||
if !cfg.RecaptchaEnabled {
|
if !cfg.RecaptchaEnabled {
|
||||||
return ErrCaptchaDisabled
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusConflict,
|
||||||
|
JSON: jsonerror.Unknown("Captcha registration is disabled"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if response == "" {
|
if response == "" {
|
||||||
return ErrMissingResponse
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Captcha response is required"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make a POST request to the captcha provider API to check the captcha response
|
// Make a POST request to Google's API to check the captcha response
|
||||||
resp, err := http.PostForm(cfg.RecaptchaSiteVerifyAPI,
|
resp, err := http.PostForm(cfg.RecaptchaSiteVerifyAPI,
|
||||||
url.Values{
|
url.Values{
|
||||||
"secret": {cfg.RecaptchaPrivateKey},
|
"secret": {cfg.RecaptchaPrivateKey},
|
||||||
"response": {response},
|
"response": {response},
|
||||||
"remoteip": {ip},
|
"remoteip": {clientip},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.BadJSON("Error in requesting validation of captcha response"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close the request once we're finishing reading from it
|
// Close the request once we're finishing reading from it
|
||||||
|
@ -312,16 +373,25 @@ func validateRecaptcha(
|
||||||
var r recaptchaResponse
|
var r recaptchaResponse
|
||||||
body, err := io.ReadAll(resp.Body)
|
body, err := io.ReadAll(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusGatewayTimeout,
|
||||||
|
JSON: jsonerror.Unknown("Error in contacting captcha server" + err.Error()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
err = json.Unmarshal(body, &r)
|
err = json.Unmarshal(body, &r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
JSON: jsonerror.BadJSON("Error in unmarshaling captcha server's response: " + err.Error()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that we received a "success"
|
// Check that we received a "success"
|
||||||
if !r.Success {
|
if !r.Success {
|
||||||
return ErrInvalidCaptcha
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusUnauthorized,
|
||||||
|
JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -342,7 +412,7 @@ func UserIDIsWithinApplicationServiceNamespace(
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if !cfg.Matrix.IsLocalServerName(domain) {
|
if domain != cfg.Matrix.ServerName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -427,7 +497,7 @@ func validateApplicationService(
|
||||||
if matchedApplicationService == nil {
|
if matchedApplicationService == nil {
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.UnknownToken("Supplied access_token does not match any known application service"),
|
JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -438,7 +508,7 @@ func validateApplicationService(
|
||||||
// If we didn't find any matches, return M_EXCLUSIVE
|
// If we didn't find any matches, return M_EXCLUSIVE
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.ASExclusive(fmt.Sprintf(
|
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
||||||
"Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
|
"Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -447,14 +517,14 @@ func validateApplicationService(
|
||||||
if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) {
|
if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) {
|
||||||
return "", &util.JSONResponse{
|
return "", &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.ASExclusive(fmt.Sprintf(
|
JSON: jsonerror.ASExclusive(fmt.Sprintf(
|
||||||
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
|
"Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check username application service is trying to register is valid
|
// Check username application service is trying to register is valid
|
||||||
if err := internal.ValidateApplicationServiceUsername(username, cfg.Matrix.ServerName); err != nil {
|
if err := validateApplicationServiceUsername(username, cfg.Matrix.ServerName); err != nil {
|
||||||
return "", internal.UsernameResponse(err)
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// No errors, registration valid
|
// No errors, registration valid
|
||||||
|
@ -462,7 +532,7 @@ func validateApplicationService(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register processes a /register request.
|
// Register processes a /register request.
|
||||||
// https://spec.matrix.org/v1.7/client-server-api/#post_matrixclientv3register
|
// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
|
||||||
func Register(
|
func Register(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
|
@ -473,17 +543,11 @@ func Register(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.NotJSON("Unable to read request body"),
|
JSON: jsonerror.NotJSON("Unable to read request body"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var r registerRequest
|
var r registerRequest
|
||||||
host := spec.ServerName(req.Host)
|
|
||||||
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
|
||||||
r.ServerName = v.ServerName
|
|
||||||
} else {
|
|
||||||
r.ServerName = cfg.Matrix.ServerName
|
|
||||||
}
|
|
||||||
sessionID := gjson.GetBytes(reqBody, "auth.session").String()
|
sessionID := gjson.GetBytes(reqBody, "auth.session").String()
|
||||||
if sessionID == "" {
|
if sessionID == "" {
|
||||||
// Generate a new, random session ID
|
// Generate a new, random session ID
|
||||||
|
@ -493,7 +557,6 @@ func Register(
|
||||||
// Some of these might end up being overwritten if the
|
// Some of these might end up being overwritten if the
|
||||||
// values are specified again in the request body.
|
// values are specified again in the request body.
|
||||||
r.Username = data.Username
|
r.Username = data.Username
|
||||||
r.ServerName = data.ServerName
|
|
||||||
r.Password = data.Password
|
r.Password = data.Password
|
||||||
r.DeviceID = data.DeviceID
|
r.DeviceID = data.DeviceID
|
||||||
r.InitialDisplayName = data.InitialDisplayName
|
r.InitialDisplayName = data.InitialDisplayName
|
||||||
|
@ -505,6 +568,7 @@ func Register(
|
||||||
JSON: response,
|
JSON: response,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
if resErr := httputil.UnmarshalJSON(reqBody, &r); resErr != nil {
|
if resErr := httputil.UnmarshalJSON(reqBody, &r); resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
|
@ -514,26 +578,20 @@ func Register(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't allow numeric usernames less than MAX_INT64.
|
// Don't allow numeric usernames less than MAX_INT64.
|
||||||
if _, err = strconv.ParseInt(r.Username, 10, 64); err == nil {
|
if _, err := strconv.ParseInt(r.Username, 10, 64); err == nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.InvalidUsername("Numeric user IDs are reserved"),
|
JSON: jsonerror.InvalidUsername("Numeric user IDs are reserved"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Auto generate a numeric username if r.Username is empty
|
// Auto generate a numeric username if r.Username is empty
|
||||||
if r.Username == "" {
|
if r.Username == "" {
|
||||||
nreq := &userapi.QueryNumericLocalpartRequest{
|
res := &userapi.QueryNumericLocalpartResponse{}
|
||||||
ServerName: r.ServerName,
|
if err := userAPI.QueryNumericLocalpart(req.Context(), res); err != nil {
|
||||||
}
|
|
||||||
nres := &userapi.QueryNumericLocalpartResponse{}
|
|
||||||
if err = userAPI.QueryNumericLocalpart(req.Context(), nreq, nres); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryNumericLocalpart failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryNumericLocalpart failed")
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
r.Username = strconv.FormatInt(nres.ID, 10)
|
r.Username = strconv.FormatInt(res.ID, 10)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is this an appservice registration? It will be if the access
|
// Is this an appservice registration? It will be if the access
|
||||||
|
@ -546,25 +604,25 @@ func Register(
|
||||||
case r.Type == authtypes.LoginTypeApplicationService && accessTokenErr == nil:
|
case r.Type == authtypes.LoginTypeApplicationService && accessTokenErr == nil:
|
||||||
// Spec-compliant case (the access_token is specified and the login type
|
// Spec-compliant case (the access_token is specified and the login type
|
||||||
// is correctly set, so it's an appservice registration)
|
// is correctly set, so it's an appservice registration)
|
||||||
if err = internal.ValidateApplicationServiceUsername(r.Username, r.ServerName); err != nil {
|
if resErr := validateApplicationServiceUsername(r.Username, cfg.Matrix.ServerName); resErr != nil {
|
||||||
return *internal.UsernameResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
case accessTokenErr == nil:
|
case accessTokenErr == nil:
|
||||||
// Non-spec-compliant case (the access_token is specified but the login
|
// Non-spec-compliant case (the access_token is specified but the login
|
||||||
// type is not known or specified)
|
// type is not known or specified)
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
JSON: jsonerror.MissingArgument("A known registration type (e.g. m.login.application_service) must be specified if an access_token is provided"),
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
// Spec-compliant case (neither the access_token nor the login type are
|
// Spec-compliant case (neither the access_token nor the login type are
|
||||||
// specified, so it's a normal user registration)
|
// specified, so it's a normal user registration)
|
||||||
if err = internal.ValidateUsername(r.Username, r.ServerName); err != nil {
|
if resErr := validateUsername(r.Username, cfg.Matrix.ServerName); resErr != nil {
|
||||||
return *internal.UsernameResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err = internal.ValidatePassword(r.Password); err != nil {
|
if resErr := validatePassword(r.Password); resErr != nil {
|
||||||
return *internal.PasswordResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
logger := util.GetLogger(req.Context())
|
logger := util.GetLogger(req.Context())
|
||||||
|
@ -583,30 +641,21 @@ func handleGuestRegistration(
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
registrationEnabled := !cfg.RegistrationDisabled
|
if cfg.RegistrationDisabled || cfg.GuestsDisabled {
|
||||||
guestsEnabled := !cfg.GuestsDisabled
|
|
||||||
if v := cfg.Matrix.VirtualHost(r.ServerName); v != nil {
|
|
||||||
registrationEnabled, guestsEnabled = v.RegistrationAllowed()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !registrationEnabled || !guestsEnabled {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden(
|
JSON: jsonerror.Forbidden("Guest registration is disabled"),
|
||||||
fmt.Sprintf("Guest registration is disabled on %q", r.ServerName),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var res userapi.PerformAccountCreationResponse
|
var res userapi.PerformAccountCreationResponse
|
||||||
err := userAPI.PerformAccountCreation(req.Context(), &userapi.PerformAccountCreationRequest{
|
err := userAPI.PerformAccountCreation(req.Context(), &userapi.PerformAccountCreationRequest{
|
||||||
AccountType: userapi.AccountTypeGuest,
|
AccountType: userapi.AccountTypeGuest,
|
||||||
ServerName: r.ServerName,
|
|
||||||
}, &res)
|
}, &res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
|
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
|
||||||
|
@ -618,24 +667,22 @@ func handleGuestRegistration(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("Failed to generate access token"),
|
JSON: jsonerror.Unknown("Failed to generate access token"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//we don't allow guests to specify their own device_id
|
//we don't allow guests to specify their own device_id
|
||||||
var devRes userapi.PerformDeviceCreationResponse
|
var devRes userapi.PerformDeviceCreationResponse
|
||||||
err = userAPI.PerformDeviceCreation(req.Context(), &userapi.PerformDeviceCreationRequest{
|
err = userAPI.PerformDeviceCreation(req.Context(), &userapi.PerformDeviceCreationRequest{
|
||||||
Localpart: res.Account.Localpart,
|
Localpart: res.Account.Localpart,
|
||||||
ServerName: res.Account.ServerName,
|
|
||||||
DeviceDisplayName: r.InitialDisplayName,
|
DeviceDisplayName: r.InitialDisplayName,
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
IPAddr: req.RemoteAddr,
|
IPAddr: req.RemoteAddr,
|
||||||
UserAgent: req.UserAgent(),
|
UserAgent: req.UserAgent(),
|
||||||
FromRegistration: true,
|
|
||||||
}, &devRes)
|
}, &devRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
@ -643,21 +690,12 @@ func handleGuestRegistration(
|
||||||
JSON: registerResponse{
|
JSON: registerResponse{
|
||||||
UserID: devRes.Device.UserID,
|
UserID: devRes.Device.UserID,
|
||||||
AccessToken: devRes.Device.AccessToken,
|
AccessToken: devRes.Device.AccessToken,
|
||||||
|
HomeServer: res.Account.ServerName,
|
||||||
DeviceID: devRes.Device.ID,
|
DeviceID: devRes.Device.ID,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// localpartMatchesExclusiveNamespaces will check if a given username matches any
|
|
||||||
// application service's exclusive users namespace
|
|
||||||
func localpartMatchesExclusiveNamespaces(
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
localpart string,
|
|
||||||
) bool {
|
|
||||||
userID := userutil.MakeUserID(localpart, cfg.Matrix.ServerName)
|
|
||||||
return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleRegistrationFlow will direct and complete registration flow stages
|
// handleRegistrationFlow will direct and complete registration flow stages
|
||||||
// that the client has requested.
|
// that the client has requested.
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
|
@ -688,16 +726,10 @@ func handleRegistrationFlow(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
registrationEnabled := !cfg.RegistrationDisabled
|
if cfg.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
|
||||||
if v := cfg.Matrix.VirtualHost(r.ServerName); v != nil {
|
|
||||||
registrationEnabled, _ = v.RegistrationAllowed()
|
|
||||||
}
|
|
||||||
if !registrationEnabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: spec.Forbidden(
|
JSON: jsonerror.Forbidden("Registration is disabled"),
|
||||||
fmt.Sprintf("Registration is disabled on %q", r.ServerName),
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -706,28 +738,19 @@ func handleRegistrationFlow(
|
||||||
// If an access token is provided, ignore this check this is an appservice
|
// If an access token is provided, ignore this check this is an appservice
|
||||||
// request and we will validate in validateApplicationService
|
// request and we will validate in validateApplicationService
|
||||||
if len(cfg.Derived.ApplicationServices) != 0 &&
|
if len(cfg.Derived.ApplicationServices) != 0 &&
|
||||||
localpartMatchesExclusiveNamespaces(cfg, r.Username) {
|
UsernameMatchesExclusiveNamespaces(cfg, r.Username) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.ASExclusive("This username is reserved by an application service."),
|
JSON: jsonerror.ASExclusive("This username is reserved by an application service."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch r.Auth.Type {
|
switch r.Auth.Type {
|
||||||
case authtypes.LoginTypeRecaptcha:
|
case authtypes.LoginTypeRecaptcha:
|
||||||
// Check given captcha response
|
// Check given captcha response
|
||||||
err := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
|
||||||
switch err {
|
if resErr != nil {
|
||||||
case ErrCaptchaDisabled:
|
return *resErr
|
||||||
return util.JSONResponse{Code: http.StatusForbidden, JSON: spec.Unknown(err.Error())}
|
|
||||||
case ErrMissingResponse:
|
|
||||||
return util.JSONResponse{Code: http.StatusBadRequest, JSON: spec.BadJSON(err.Error())}
|
|
||||||
case ErrInvalidCaptcha:
|
|
||||||
return util.JSONResponse{Code: http.StatusUnauthorized, JSON: spec.BadJSON(err.Error())}
|
|
||||||
case nil:
|
|
||||||
default:
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
|
||||||
return util.JSONResponse{Code: http.StatusInternalServerError, JSON: spec.InternalServerError{}}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Recaptcha to the list of completed registration stages
|
// Add Recaptcha to the list of completed registration stages
|
||||||
|
@ -745,7 +768,7 @@ func handleRegistrationFlow(
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotImplemented,
|
Code: http.StatusNotImplemented,
|
||||||
JSON: spec.Unknown("unknown/unimplemented auth type"),
|
JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,13 +800,13 @@ func handleApplicationServiceRegistration(
|
||||||
if tokenErr != nil {
|
if tokenErr != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: spec.MissingToken(tokenErr.Error()),
|
JSON: jsonerror.MissingToken(tokenErr.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check application service register user request is valid.
|
// Check application service register user request is valid.
|
||||||
// The application service's ID is returned if so.
|
// The application service's ID is returned if so.
|
||||||
appserviceID, err := internal.ValidateApplicationServiceRequest(
|
appserviceID, err := validateApplicationService(
|
||||||
cfg, r.Username, accessToken,
|
cfg, r.Username, accessToken,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -794,9 +817,8 @@ func handleApplicationServiceRegistration(
|
||||||
// Don't need to worry about appending to registration stages as
|
// Don't need to worry about appending to registration stages as
|
||||||
// application service registration is entirely separate.
|
// application service registration is entirely separate.
|
||||||
return completeRegistration(
|
return completeRegistration(
|
||||||
req.Context(), userAPI, r.Username, r.ServerName, "", "", appserviceID, req.RemoteAddr,
|
req.Context(), userAPI, r.Username, "", appserviceID, req.RemoteAddr, req.UserAgent(), r.Auth.Session,
|
||||||
req.UserAgent(), r.Auth.Session, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
|
r.InhibitLogin, r.InitialDisplayName, r.DeviceID, userapi.AccountTypeAppService,
|
||||||
userapi.AccountTypeAppService,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -814,9 +836,8 @@ func checkAndCompleteFlow(
|
||||||
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
|
if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
|
||||||
// This flow was completed, registration can continue
|
// This flow was completed, registration can continue
|
||||||
return completeRegistration(
|
return completeRegistration(
|
||||||
req.Context(), userAPI, r.Username, r.ServerName, "", r.Password, "", req.RemoteAddr,
|
req.Context(), userAPI, r.Username, r.Password, "", req.RemoteAddr, req.UserAgent(), sessionID,
|
||||||
req.UserAgent(), sessionID, r.InhibitLogin, r.InitialDisplayName, r.DeviceID,
|
r.InhibitLogin, r.InitialDisplayName, r.DeviceID, userapi.AccountTypeUser,
|
||||||
userapi.AccountTypeUser,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
sessions.addParams(sessionID, r)
|
sessions.addParams(sessionID, r)
|
||||||
|
@ -838,30 +859,28 @@ func checkAndCompleteFlow(
|
||||||
func completeRegistration(
|
func completeRegistration(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.ClientUserAPI,
|
||||||
username string, serverName spec.ServerName, displayName string,
|
username, password, appserviceID, ipAddr, userAgent, sessionID string,
|
||||||
password, appserviceID, ipAddr, userAgent, sessionID string,
|
|
||||||
inhibitLogin eventutil.WeakBoolean,
|
inhibitLogin eventutil.WeakBoolean,
|
||||||
deviceDisplayName, deviceID *string,
|
displayName, deviceID *string,
|
||||||
accType userapi.AccountType,
|
accType userapi.AccountType,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam("Missing username"),
|
JSON: jsonerror.MissingArgument("Missing username"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Blank passwords are only allowed by registered application services
|
// Blank passwords are only allowed by registered application services
|
||||||
if password == "" && appserviceID == "" {
|
if password == "" && appserviceID == "" {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.MissingParam("Missing password"),
|
JSON: jsonerror.MissingArgument("Missing password"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
var accRes userapi.PerformAccountCreationResponse
|
var accRes userapi.PerformAccountCreationResponse
|
||||||
err := userAPI.PerformAccountCreation(ctx, &userapi.PerformAccountCreationRequest{
|
err := userAPI.PerformAccountCreation(ctx, &userapi.PerformAccountCreationRequest{
|
||||||
AppServiceID: appserviceID,
|
AppServiceID: appserviceID,
|
||||||
Localpart: username,
|
Localpart: username,
|
||||||
ServerName: serverName,
|
|
||||||
Password: password,
|
Password: password,
|
||||||
AccountType: accType,
|
AccountType: accType,
|
||||||
OnConflict: userapi.ConflictAbort,
|
OnConflict: userapi.ConflictAbort,
|
||||||
|
@ -870,12 +889,12 @@ func completeRegistration(
|
||||||
if _, ok := err.(*userapi.ErrorConflict); ok { // user already exists
|
if _, ok := err.(*userapi.ErrorConflict); ok { // user already exists
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.UserInUse("Desired user ID is already taken."),
|
JSON: jsonerror.UserInUse("Desired user ID is already taken."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to create account: " + err.Error()),
|
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -888,7 +907,8 @@ func completeRegistration(
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: registerResponse{
|
JSON: registerResponse{
|
||||||
UserID: userutil.MakeUserID(username, accRes.Account.ServerName),
|
UserID: userutil.MakeUserID(username, accRes.Account.ServerName),
|
||||||
|
HomeServer: accRes.Account.ServerName,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -897,41 +917,30 @@ func completeRegistration(
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("Failed to generate access token"),
|
JSON: jsonerror.Unknown("Failed to generate access token"),
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if displayName != "" {
|
|
||||||
_, _, err = userAPI.SetDisplayName(ctx, username, serverName, displayName)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("failed to set display name: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var devRes userapi.PerformDeviceCreationResponse
|
var devRes userapi.PerformDeviceCreationResponse
|
||||||
err = userAPI.PerformDeviceCreation(ctx, &userapi.PerformDeviceCreationRequest{
|
err = userAPI.PerformDeviceCreation(ctx, &userapi.PerformDeviceCreationRequest{
|
||||||
Localpart: username,
|
Localpart: username,
|
||||||
ServerName: serverName,
|
|
||||||
AccessToken: token,
|
AccessToken: token,
|
||||||
DeviceDisplayName: deviceDisplayName,
|
DeviceDisplayName: displayName,
|
||||||
DeviceID: deviceID,
|
DeviceID: deviceID,
|
||||||
IPAddr: ipAddr,
|
IPAddr: ipAddr,
|
||||||
UserAgent: userAgent,
|
UserAgent: userAgent,
|
||||||
FromRegistration: true,
|
|
||||||
}, &devRes)
|
}, &devRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to create device: " + err.Error()),
|
JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result := registerResponse{
|
result := registerResponse{
|
||||||
UserID: devRes.Device.UserID,
|
UserID: devRes.Device.UserID,
|
||||||
AccessToken: devRes.Device.AccessToken,
|
AccessToken: devRes.Device.AccessToken,
|
||||||
|
HomeServer: accRes.Account.ServerName,
|
||||||
DeviceID: devRes.Device.ID,
|
DeviceID: devRes.Device.ID,
|
||||||
}
|
}
|
||||||
sessions.addCompletedRegistration(sessionID, result)
|
sessions.addCompletedRegistration(sessionID, result)
|
||||||
|
@ -1008,56 +1017,37 @@ func RegisterAvailable(
|
||||||
|
|
||||||
// Squash username to all lowercase letters
|
// Squash username to all lowercase letters
|
||||||
username = strings.ToLower(username)
|
username = strings.ToLower(username)
|
||||||
domain := cfg.Matrix.ServerName
|
|
||||||
host := spec.ServerName(req.Host)
|
|
||||||
if v := cfg.Matrix.VirtualHostForHTTPHost(host); v != nil {
|
|
||||||
domain = v.ServerName
|
|
||||||
}
|
|
||||||
if u, l, err := cfg.Matrix.SplitLocalID('@', username); err == nil {
|
|
||||||
username, domain = u, l
|
|
||||||
}
|
|
||||||
for _, v := range cfg.Matrix.VirtualHosts {
|
|
||||||
if v.ServerName == domain && !v.AllowRegistration {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden(
|
|
||||||
fmt.Sprintf("Registration is not allowed on %q", string(v.ServerName)),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := internal.ValidateUsername(username, domain); err != nil {
|
if err := validateUsername(username, cfg.Matrix.ServerName); err != nil {
|
||||||
return *internal.UsernameResponse(err)
|
return *err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if this username is reserved by an application service
|
// Check if this username is reserved by an application service
|
||||||
userID := userutil.MakeUserID(username, domain)
|
userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
|
||||||
for _, appservice := range cfg.Derived.ApplicationServices {
|
for _, appservice := range cfg.Derived.ApplicationServices {
|
||||||
if appservice.OwnsNamespaceCoveringUserId(userID) {
|
if appservice.OwnsNamespaceCoveringUserId(userID) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.UserInUse("Desired user ID is reserved by an application service."),
|
JSON: jsonerror.UserInUse("Desired user ID is reserved by an application service."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res := &userapi.QueryAccountAvailabilityResponse{}
|
res := &userapi.QueryAccountAvailabilityResponse{}
|
||||||
err := registerAPI.QueryAccountAvailability(req.Context(), &userapi.QueryAccountAvailabilityRequest{
|
err := registerAPI.QueryAccountAvailability(req.Context(), &userapi.QueryAccountAvailabilityRequest{
|
||||||
Localpart: username,
|
Localpart: username,
|
||||||
ServerName: domain,
|
|
||||||
}, res)
|
}, res)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.Unknown("failed to check availability:" + err.Error()),
|
JSON: jsonerror.Unknown("failed to check availability:" + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !res.Available {
|
if !res.Available {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: spec.UserInUse("Desired User ID is already taken."),
|
JSON: jsonerror.UserInUse("Desired User ID is already taken."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1074,7 +1064,7 @@ func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.Clien
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 400,
|
Code: 400,
|
||||||
JSON: spec.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
JSON: jsonerror.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
||||||
|
@ -1084,17 +1074,17 @@ func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.Clien
|
||||||
if !valid {
|
if !valid {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: 403,
|
Code: 403,
|
||||||
JSON: spec.Forbidden("bad mac"),
|
JSON: jsonerror.Forbidden("bad mac"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// downcase capitals
|
// downcase capitals
|
||||||
ssrr.User = strings.ToLower(ssrr.User)
|
ssrr.User = strings.ToLower(ssrr.User)
|
||||||
|
|
||||||
if err = internal.ValidateUsername(ssrr.User, cfg.Matrix.ServerName); err != nil {
|
if resErr := validateUsername(ssrr.User, cfg.Matrix.ServerName); resErr != nil {
|
||||||
return *internal.UsernameResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
if err = internal.ValidatePassword(ssrr.Password); err != nil {
|
if resErr := validatePassword(ssrr.Password); resErr != nil {
|
||||||
return *internal.PasswordResponse(err)
|
return *resErr
|
||||||
}
|
}
|
||||||
deviceID := "shared_secret_registration"
|
deviceID := "shared_secret_registration"
|
||||||
|
|
||||||
|
@ -1102,5 +1092,5 @@ func handleSharedSecretRegistration(cfg *config.ClientAPI, userAPI userapi.Clien
|
||||||
if ssrr.Admin {
|
if ssrr.Admin {
|
||||||
accType = userapi.AccountTypeAdmin
|
accType = userapi.AccountTypeAdmin
|
||||||
}
|
}
|
||||||
return completeRegistration(req.Context(), userAPI, ssrr.User, cfg.Matrix.ServerName, ssrr.DisplayName, ssrr.Password, "", req.RemoteAddr, req.UserAgent(), "", false, &ssrr.User, &deviceID, accType)
|
return completeRegistration(req.Context(), userAPI, ssrr.User, ssrr.Password, "", req.RemoteAddr, req.UserAgent(), "", false, &ssrr.User, &deviceID, accType)
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,13 +18,12 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type SharedSecretRegistrationRequest struct {
|
type SharedSecretRegistrationRequest struct {
|
||||||
User string `json:"username"`
|
User string `json:"username"`
|
||||||
Password string `json:"password"`
|
Password string `json:"password"`
|
||||||
Nonce string `json:"nonce"`
|
Nonce string `json:"nonce"`
|
||||||
MacBytes []byte
|
MacBytes []byte
|
||||||
MacStr string `json:"mac"`
|
MacStr string `json:"mac"`
|
||||||
Admin bool `json:"admin"`
|
Admin bool `json:"admin"`
|
||||||
DisplayName string `json:"displayname,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSharedSecretRegistrationRequest(reader io.ReadCloser) (*SharedSecretRegistrationRequest, error) {
|
func NewSharedSecretRegistrationRequest(reader io.ReadCloser) (*SharedSecretRegistrationRequest, error) {
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue