mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-07 06:53:09 -06:00
Compare commits
No commits in common. "main" and "v0.5.0rc1" have entirely different histories.
|
|
@ -1,2 +1,3 @@
|
||||||
bin
|
bin
|
||||||
*.wasm
|
*.wasm
|
||||||
|
.git
|
||||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -1 +0,0 @@
|
||||||
* @matrix-org/dendrite-core
|
|
||||||
54
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
54
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
|
|
@ -4,52 +4,34 @@ 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.
|
|
||||||
|
|
||||||
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 -->
|
|
||||||
- **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)**:
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
This is a bug report template. By following the instructions below and
|
||||||
|
filling out the sections with your information, you will help the us to get all
|
||||||
|
the necessary data to fix your issue.
|
||||||
|
|
||||||
|
You can also preview your report before submitting it. You may remove sections
|
||||||
|
that aren't relevant to your particular case.
|
||||||
|
|
||||||
|
Text between <!-- and --> marks will be invisible in the report.
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
### Description
|
### Description
|
||||||
|
|
||||||
- **What** is the problem:
|
<!-- Describe here the problem that you are experiencing -->
|
||||||
- **Who** is affected:
|
|
||||||
- **How** is this bug manifesting:
|
|
||||||
- **When** did this first appear:
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Examples of good descriptions:
|
|
||||||
- What: "I cannot log in, getting HTTP 500 responses"
|
|
||||||
- Who: "Clients on my server"
|
|
||||||
- How: "Errors in the logs saying 500 internal server error"
|
|
||||||
- When: "After upgrading to 0.3.0"
|
|
||||||
|
|
||||||
- What: "Dendrite ran out of memory"
|
|
||||||
- Who: "Server admin"
|
|
||||||
- How: "Lots of logs about device change updates"
|
|
||||||
- When: "After my server joined Matrix HQ"
|
|
||||||
|
|
||||||
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?
|
|
||||||
- Who: "Me" - Who are you? Running the server or a user on a Dendrite server?
|
|
||||||
- How: "Can't send messages" - Same as "What".
|
|
||||||
- When: "1 day ago" - It's impossible to know what changed 1 day ago without further input.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Steps to reproduce
|
### Steps to reproduce
|
||||||
<!-- Please try reproducing this bug before submitting it. Issues which cannot be reproduced risk being closed. -->
|
|
||||||
|
|
||||||
- list the steps
|
- list the steps
|
||||||
- that reproduce the bug
|
- that reproduce the bug
|
||||||
|
|
@ -62,6 +44,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 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 any new tests that need to pass to `sytest-whitelist` as specified in [docs/sytest.md](https://github.com/matrix-org/dendrite/blob/master/docs/sytest.md)
|
||||||
* [ ] 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/master/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
|
|
||||||
34
.github/workflows/codeql-analysis.yml
vendored
Normal file
34
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
name: "CodeQL"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
pull_request:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
analyze:
|
||||||
|
name: Analyze
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
language: ['go']
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
|
||||||
|
- run: git checkout HEAD^2
|
||||||
|
if: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
|
- name: Initialize CodeQL
|
||||||
|
uses: github/codeql-action/init@v1
|
||||||
|
with:
|
||||||
|
languages: ${{ matrix.language }}
|
||||||
|
|
||||||
|
- name: Perform CodeQL Analysis
|
||||||
|
uses: github/codeql-action/analyze@v1
|
||||||
497
.github/workflows/dendrite.yml
vendored
497
.github/workflows/dendrite.yml
vendored
|
|
@ -1,497 +0,0 @@
|
||||||
name: Dendrite
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '**.go' # only execute on changes to go files
|
|
||||||
- 'go.sum' # or dependency updates
|
|
||||||
- '.github/workflows/**' # or workflow changes
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**.go'
|
|
||||||
- 'go.sum' # or dependency updates
|
|
||||||
- '.github/workflows/**'
|
|
||||||
release:
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
|
||||||
cancel-in-progress: true
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
wasm:
|
|
||||||
name: WASM build test
|
|
||||||
timeout-minutes: 5
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ false }} # disable for now
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
|
|
||||||
- name: Install Node
|
|
||||||
uses: actions/setup-node@v2
|
|
||||||
with:
|
|
||||||
node-version: 14
|
|
||||||
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.npm
|
|
||||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-node-
|
|
||||||
|
|
||||||
- name: Reconfigure Git to use HTTPS auth for repo packages
|
|
||||||
run: >
|
|
||||||
git config --global url."https://github.com/".insteadOf
|
|
||||||
ssh://git@github.com/
|
|
||||||
|
|
||||||
- name: Install test dependencies
|
|
||||||
working-directory: ./test/wasm
|
|
||||||
run: npm ci
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: ./test-dendritejs.sh
|
|
||||||
|
|
||||||
# Run golangci-lint
|
|
||||||
lint:
|
|
||||||
timeout-minutes: 5
|
|
||||||
name: Linting
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Install libolm
|
|
||||||
run: sudo apt-get install libolm-dev libolm3
|
|
||||||
- name: Install Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
- name: golangci-lint
|
|
||||||
uses: golangci/golangci-lint-action@v3
|
|
||||||
|
|
||||||
# run go test with different go versions
|
|
||||||
test:
|
|
||||||
timeout-minutes: 10
|
|
||||||
name: Unit 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-file: 'go.mod'
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
# manually set up caches, as they otherwise clash with different steps using setup-go with cache=true
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-stable-unit-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-stable-unit-
|
|
||||||
- name: Set up gotestfmt
|
|
||||||
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:
|
|
||||||
POSTGRES_HOST: localhost
|
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_PASSWORD: postgres
|
|
||||||
POSTGRES_DB: dendrite
|
|
||||||
|
|
||||||
# build Dendrite for linux with different architectures and go versions
|
|
||||||
build:
|
|
||||||
name: Build for Linux
|
|
||||||
timeout-minutes: 10
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
goos: ["linux"]
|
|
||||||
goarch: ["amd64", "386"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
|
||||||
- name: Install dependencies x86
|
|
||||||
if: ${{ matrix.goarch == '386' }}
|
|
||||||
run: sudo apt update && sudo apt-get install -y gcc-multilib
|
|
||||||
- env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
CGO_CFLAGS: -fno-stack-protector
|
|
||||||
run: go build -trimpath -v -o "bin/" ./cmd/...
|
|
||||||
|
|
||||||
# build for Windows 64-bit
|
|
||||||
build_windows:
|
|
||||||
name: Build for Windows
|
|
||||||
timeout-minutes: 10
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
goos: ["windows"]
|
|
||||||
goarch: ["amd64"]
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup Go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
key: ${{ runner.os }}-go-stable-${{ matrix.goos }}-${{ matrix.goarch }}-
|
|
||||||
- name: Install dependencies
|
|
||||||
run: sudo apt update && sudo apt install -y gcc-mingw-w64-x86-64 # install required gcc
|
|
||||||
- env:
|
|
||||||
GOOS: ${{ matrix.goos }}
|
|
||||||
GOARCH: ${{ matrix.goarch }}
|
|
||||||
CGO_ENABLED: 1
|
|
||||||
CC: "/usr/bin/x86_64-w64-mingw32-gcc"
|
|
||||||
run: go build -trimpath -v -o "bin/" ./cmd/...
|
|
||||||
|
|
||||||
# Dummy step to gate other tests on without repeating the whole list
|
|
||||||
initial-tests-done:
|
|
||||||
name: Initial tests passed
|
|
||||||
needs: [lint, test, build, build_windows]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
|
||||||
steps:
|
|
||||||
- name: Check initial tests passed
|
|
||||||
uses: re-actors/alls-green@release/v1
|
|
||||||
with:
|
|
||||||
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-file: 'go.mod'
|
|
||||||
- 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
|
|
||||||
upgrade_test:
|
|
||||||
name: Upgrade tests
|
|
||||||
timeout-minutes: 20
|
|
||||||
needs: initial-tests-done
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-upgrade-test-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-upgrade-test-
|
|
||||||
- name: Docker version
|
|
||||||
run: docker version
|
|
||||||
- name: Build upgrade-tests
|
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
|
||||||
- name: Test upgrade (PostgreSQL)
|
|
||||||
run: ./dendrite-upgrade-tests --head .
|
|
||||||
- name: Test upgrade (SQLite)
|
|
||||||
run: ./dendrite-upgrade-tests --sqlite --head .
|
|
||||||
|
|
||||||
# run database upgrade tests, skipping over one version
|
|
||||||
upgrade_test_direct:
|
|
||||||
name: Upgrade tests from HEAD-2
|
|
||||||
timeout-minutes: 20
|
|
||||||
needs: initial-tests-done
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
- name: Setup go
|
|
||||||
uses: actions/setup-go@v4
|
|
||||||
with:
|
|
||||||
go-version-file: 'go.mod'
|
|
||||||
cache: true
|
|
||||||
- uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.cache/go-build
|
|
||||||
~/go/pkg/mod
|
|
||||||
key: ${{ runner.os }}-go-upgrade-direct-test-${{ hashFiles('**/go.sum') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-go-upgrade-direct-test-
|
|
||||||
- name: Docker version
|
|
||||||
run: docker version
|
|
||||||
- name: Build upgrade-tests
|
|
||||||
run: go build ./cmd/dendrite-upgrade-tests
|
|
||||||
- name: Test upgrade (PostgreSQL)
|
|
||||||
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
|
||||||
- name: Test upgrade (SQLite)
|
|
||||||
run: ./dendrite-upgrade-tests -direct -from HEAD-2 --head .
|
|
||||||
|
|
||||||
# run Sytest in different variations
|
|
||||||
sytest:
|
|
||||||
timeout-minutes: 20
|
|
||||||
needs: initial-tests-done
|
|
||||||
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
|
|
||||||
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 }}
|
|
||||||
CGO_ENABLED: ${{ matrix.cgo && 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*
|
|
||||||
|
|
||||||
# run Complement
|
|
||||||
complement:
|
|
||||||
name: "Complement (${{ matrix.label }})"
|
|
||||||
timeout-minutes: 20
|
|
||||||
needs: initial-tests-done
|
|
||||||
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
|
|
||||||
|
|
||||||
# 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_
|
|
||||||
working-directory: complement
|
|
||||||
|
|
||||||
integration-tests-done:
|
|
||||||
name: Integration tests passed
|
|
||||||
needs:
|
|
||||||
[
|
|
||||||
initial-tests-done,
|
|
||||||
upgrade_test,
|
|
||||||
upgrade_test_direct,
|
|
||||||
sytest,
|
|
||||||
complement,
|
|
||||||
integration
|
|
||||||
]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ !cancelled() }} # Run this even if prior jobs were skipped
|
|
||||||
steps:
|
|
||||||
- name: Check integration tests passed
|
|
||||||
uses: re-actors/alls-green@release/v1
|
|
||||||
with:
|
|
||||||
jobs: ${{ toJSON(needs) }}
|
|
||||||
|
|
||||||
update-docker-images:
|
|
||||||
name: Update Docker images
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
security-events: write # To upload Trivy sarif files
|
|
||||||
if: github.repository == 'matrix-org/dendrite' && github.ref_name == 'main'
|
|
||||||
needs: [integration-tests-done]
|
|
||||||
uses: matrix-org/dendrite/.github/workflows/docker.yml@main
|
|
||||||
secrets:
|
|
||||||
DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }}
|
|
||||||
71
.github/workflows/docker-hub.yml
vendored
Normal file
71
.github/workflows/docker-hub.yml
vendored
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
# Based on https://github.com/docker/build-push-action
|
||||||
|
|
||||||
|
name: "Docker Hub"
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
env:
|
||||||
|
DOCKER_NAMESPACE: matrixdotorg
|
||||||
|
DOCKER_HUB_USER: dendritegithub
|
||||||
|
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Monolith:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Get release tag
|
||||||
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build monolith image
|
||||||
|
id: docker_build_monolith
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.monolith
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-monolith:latest
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
||||||
|
|
||||||
|
Polylith:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Get release tag
|
||||||
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v1
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ env.DOCKER_HUB_USER }}
|
||||||
|
password: ${{ secrets.DOCKER_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build polylith image
|
||||||
|
id: docker_build_polylith
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./build/docker/Dockerfile.polylith
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:latest
|
||||||
|
${{ env.DOCKER_NAMESPACE }}/dendrite-polylith:${{ env.RELEASE_VERSION }}
|
||||||
213
.github/workflows/docker.yml
vendored
213
.github/workflows/docker.yml
vendored
|
|
@ -1,213 +0,0 @@
|
||||||
# Based on https://github.com/docker/build-push-action
|
|
||||||
|
|
||||||
name: "Docker"
|
|
||||||
|
|
||||||
on:
|
|
||||||
release: # A GitHub release was published
|
|
||||||
types: [published]
|
|
||||||
workflow_dispatch: # A build was manually requested
|
|
||||||
workflow_call: # Another pipeline called us
|
|
||||||
secrets:
|
|
||||||
DOCKER_TOKEN:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
env:
|
|
||||||
DOCKER_NAMESPACE: matrixdotorg
|
|
||||||
DOCKER_HUB_USER: dendritegithub
|
|
||||||
GHCR_NAMESPACE: matrix-org
|
|
||||||
PLATFORMS: linux/amd64,linux/arm64,linux/arm/v7
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
monolith:
|
|
||||||
name: Monolith image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
security-events: write # To upload Trivy sarif files
|
|
||||||
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 monolith image
|
|
||||||
if: github.ref_name == 'main'
|
|
||||||
id: docker_build_monolith
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache
|
|
||||||
cache-to: type=registry,ref=ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:buildcache,mode=max
|
|
||||||
context: .
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-monolith:${{ github.ref_name }}
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ github.ref_name }}
|
|
||||||
|
|
||||||
- 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:
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
context: .
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-monolith:latest
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:latest
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-monolith:${{ env.RELEASE_VERSION }}
|
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner
|
|
||||||
uses: aquasecurity/trivy-action@master
|
|
||||||
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
|
|
||||||
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 Pinecone demo image
|
|
||||||
if: github.ref_name == 'main'
|
|
||||||
id: docker_build_demo_pinecone
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
context: .
|
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
|
||||||
platforms: ${{ env.PLATFORMS }}
|
|
||||||
push: true
|
|
||||||
tags: |
|
|
||||||
${{ env.DOCKER_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
|
||||||
ghcr.io/${{ env.GHCR_NAMESPACE }}/dendrite-demo-pinecone:${{ github.ref_name }}
|
|
||||||
|
|
||||||
- name: Build release Pinecone demo image
|
|
||||||
if: github.event_name == 'release' # Only for GitHub releases
|
|
||||||
id: docker_build_demo_pinecone_release
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
context: .
|
|
||||||
file: ./build/docker/Dockerfile.demo-pinecone
|
|
||||||
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 }}
|
|
||||||
|
|
||||||
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@v1.6.0
|
|
||||||
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 }}
|
|
||||||
49
.github/workflows/wasm.yml
vendored
Normal file
49
.github/workflows/wasm.yml
vendored
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
name: WebAssembly
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Go
|
||||||
|
uses: actions/setup-go@v2
|
||||||
|
with:
|
||||||
|
go-version: 1.16.5
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache/go-build
|
||||||
|
~/go/pkg/mod
|
||||||
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-go-
|
||||||
|
|
||||||
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 14
|
||||||
|
|
||||||
|
- uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-node-
|
||||||
|
|
||||||
|
- name: Reconfigure Git to use HTTPS auth for repo packages
|
||||||
|
run: >
|
||||||
|
git config --global url."https://github.com/".insteadOf
|
||||||
|
ssh://git@github.com/
|
||||||
|
|
||||||
|
- name: Install test dependencies
|
||||||
|
working-directory: ./test/wasm
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: ./test-dendritejs.sh
|
||||||
21
.gitignore
vendored
21
.gitignore
vendored
|
|
@ -23,7 +23,6 @@
|
||||||
/vendor/bin
|
/vendor/bin
|
||||||
/docker/build
|
/docker/build
|
||||||
/logs
|
/logs
|
||||||
/jetstream
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
# Architecture specific extensions/prefixes
|
||||||
*.[568vq]
|
*.[568vq]
|
||||||
|
|
@ -41,10 +40,6 @@ _testmain.go
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
*.wasm
|
*.wasm
|
||||||
*.aar
|
|
||||||
*.jar
|
|
||||||
*.framework
|
|
||||||
*.xcframework
|
|
||||||
|
|
||||||
# Generated keys
|
# Generated keys
|
||||||
*.pem
|
*.pem
|
||||||
|
|
@ -56,7 +51,6 @@ dendrite.yaml
|
||||||
|
|
||||||
# Database files
|
# Database files
|
||||||
*.db
|
*.db
|
||||||
*.db-journal
|
|
||||||
|
|
||||||
# Log files
|
# Log files
|
||||||
*.log*
|
*.log*
|
||||||
|
|
@ -66,18 +60,3 @@ cmd/dendrite-demo-yggdrasil/embed/fs*.go
|
||||||
|
|
||||||
# Test dependencies
|
# Test dependencies
|
||||||
test/wasm/node_modules
|
test/wasm/node_modules
|
||||||
|
|
||||||
# Ignore complement folder when running locally
|
|
||||||
complement/
|
|
||||||
|
|
||||||
# Stuff from GitHub Pages
|
|
||||||
docs/_site
|
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
1018
CHANGES.md
1018
CHANGES.md
File diff suppressed because it is too large
Load diff
|
|
@ -1,59 +0,0 @@
|
||||||
# Contributing to Dendrite
|
|
||||||
|
|
||||||
Thank you for taking the time to contribute to Matrix!
|
|
||||||
|
|
||||||
This is the repository for Dendrite, a second-generation Matrix homeserver written in Go.
|
|
||||||
|
|
||||||
## Sign off
|
|
||||||
|
|
||||||
We ask that everybody who contributes to this project signs off their contributions, as explained below.
|
|
||||||
|
|
||||||
We follow a simple 'inbound=outbound' model for contributions: the act of submitting an 'inbound' contribution means that the contributor agrees to license their contribution under the same terms as the project's overall 'outbound' license - in our case, this is Apache Software License v2 (see [LICENSE](./LICENSE)).
|
|
||||||
|
|
||||||
In order to have a concrete record that your contribution is intentional and you agree to license it under the same terms as the project's license, we've adopted the same lightweight approach used by the [Linux Kernel](https://www.kernel.org/doc/html/latest/process/submitting-patches.html), [Docker](https://github.com/docker/docker/blob/master/CONTRIBUTING.md), and many other projects: the [Developer Certificate of Origin](https://developercertificate.org/) (DCO). This is a simple declaration that you wrote the contribution or otherwise have the right to contribute it to Matrix:
|
|
||||||
|
|
||||||
```
|
|
||||||
Developer Certificate of Origin
|
|
||||||
Version 1.1
|
|
||||||
|
|
||||||
Copyright (C) 2004, 2006 The Linux Foundation and its contributors.
|
|
||||||
660 York Street, Suite 102,
|
|
||||||
San Francisco, CA 94110 USA
|
|
||||||
|
|
||||||
Everyone is permitted to copy and distribute verbatim copies of this
|
|
||||||
license document, but changing it is not allowed.
|
|
||||||
|
|
||||||
Developer's Certificate of Origin 1.1
|
|
||||||
|
|
||||||
By making a contribution to this project, I certify that:
|
|
||||||
|
|
||||||
(a) The contribution was created in whole or in part by me and I
|
|
||||||
have the right to submit it under the open source license
|
|
||||||
indicated in the file; or
|
|
||||||
|
|
||||||
(b) The contribution is based upon previous work that, to the best
|
|
||||||
of my knowledge, is covered under an appropriate open source
|
|
||||||
license and I have the right under that license to submit that
|
|
||||||
work with modifications, whether created in whole or in part
|
|
||||||
by me, under the same open source license (unless I am
|
|
||||||
permitted to submit under a different license), as indicated
|
|
||||||
in the file; or
|
|
||||||
|
|
||||||
(c) The contribution was provided directly to me by some other
|
|
||||||
person who certified (a), (b) or (c) and I have not modified
|
|
||||||
it.
|
|
||||||
|
|
||||||
(d) I understand and agree that this project and the contribution
|
|
||||||
are public and that a record of the contribution (including all
|
|
||||||
personal information I submit with it, including my sign-off) is
|
|
||||||
maintained indefinitely and may be redistributed consistent with
|
|
||||||
this project or the open source license(s) involved.
|
|
||||||
```
|
|
||||||
|
|
||||||
If you agree to this for your contribution, then all that's needed is to include the line in your commit or pull request comment:
|
|
||||||
|
|
||||||
```
|
|
||||||
Signed-off-by: Your Name <your@email.example.org>
|
|
||||||
```
|
|
||||||
|
|
||||||
Git allows you to add this signoff automatically when using the `-s` flag to `git commit`, which uses the name and email set in your `user.name` and `user.email` git configs.
|
|
||||||
48
Dockerfile
48
Dockerfile
|
|
@ -1,48 +0,0 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
|
||||||
|
|
||||||
#
|
|
||||||
# base installs required dependencies and runs go mod download to cache dependencies
|
|
||||||
#
|
|
||||||
FROM --platform=${BUILDPLATFORM} docker.io/golang:1.22-alpine 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
|
|
||||||
|
|
||||||
143
README.md
143
README.md
|
|
@ -1,37 +1,27 @@
|
||||||
# Dendrite
|
# Dendrite [](https://buildkite.com/matrix-dot-org/dendrite) [](https://matrix.to/#/#dendrite:matrix.org) [](https://matrix.to/#/#dendrite-dev:matrix.org)
|
||||||
|
|
||||||
## Dendrite is now maintained at [element-hq/dendrite](https://github.com/element-hq/dendrite)
|
|
||||||
|
|
||||||
Dendrite is an open-source [Matrix](https://matrix.org/) homeserver developed from 2019 through 2023 as part of the Matrix.org Foundation.
|
|
||||||
The Matrix.org Foundation is not able to resource maintenance of Dendrite and it [continues to be developed by Element](https://github.com/element-hq/dendrite)
|
|
||||||
additionally you have the choice of [other Matrix homeservers](https://matrix.org/ecosystem/servers/)
|
|
||||||
|
|
||||||
See [The future of Synapse and Dendrite](https://matrix.org/blog/2023/11/06/future-of-synapse-dendrite/) blog post for more information.
|
|
||||||
|
|
||||||
[](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml) [](https://matrix.to/#/#dendrite:matrix.org) [](https://matrix.to/#/#dendrite-dev:matrix.org)
|
|
||||||
|
|
||||||
Dendrite is a second-generation Matrix homeserver written in Go.
|
Dendrite is a second-generation Matrix homeserver written in Go.
|
||||||
It intends to provide an **efficient**, **reliable** and **scalable** alternative to [Synapse](https://github.com/matrix-org/synapse):
|
It intends to provide an **efficient**, **reliable** and **scalable** alternative to [Synapse](https://github.com/matrix-org/synapse):
|
||||||
|
- Efficient: A small memory footprint with better baseline performance than an out-of-the-box Synapse.
|
||||||
- Efficient: A small memory footprint with better baseline performance than an out-of-the-box Synapse.
|
- Reliable: Implements the Matrix specification as written, using the
|
||||||
- Reliable: Implements the Matrix specification as written, using the
|
|
||||||
[same test suite](https://github.com/matrix-org/sytest) as Synapse as well as
|
[same test suite](https://github.com/matrix-org/sytest) as Synapse as well as
|
||||||
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, Dendrite has now entered **beta** which means:
|
||||||
|
- Dendrite is ready for early adopters. We recommend running in Monolith mode with a PostgreSQL database.
|
||||||
- Dendrite is ready for early adopters. We recommend running Dendrite with a PostgreSQL database.
|
- Dendrite has periodic semver releases. We intend to release new versions as we land significant features.
|
||||||
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and 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.
|
- All of the CS/Federation APIs are implemented. We are tracking progress via a script called 'Are We Synapse Yet?'. In particular,
|
||||||
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
presence and push notifications are entirely missing from Dendrite. See [CHANGES.md](CHANGES.md) for updates.
|
||||||
- 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:
|
||||||
|
|
||||||
|
|
@ -41,34 +31,29 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
To build Dendrite, you will need Go 1.15 or later.
|
||||||
more information on requirements.
|
|
||||||
|
|
||||||
To build Dendrite, you will need Go 1.21 or later.
|
|
||||||
|
|
||||||
For a usable federating Dendrite deployment, you will also need:
|
For a usable federating Dendrite deployment, you will also need:
|
||||||
|
|
||||||
- A domain name (or subdomain)
|
- A domain name (or subdomain)
|
||||||
- A valid TLS certificate issued by a trusted authority for that domain
|
- A valid TLS certificate issued by a trusted authority for that domain
|
||||||
- SRV records or a well-known file pointing to your deployment
|
- SRV records or a well-known file pointing to your deployment
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
If you wish to build a fully-federating Dendrite instance, see [the Installation documentation](https://matrix-org.github.io/dendrite/installation). For running in Docker, see [build/docker](build/docker).
|
If you wish to build a fully-federating Dendrite instance, see [INSTALL.md](docs/INSTALL.md). For running in Docker, see [build/docker](build/docker).
|
||||||
|
|
||||||
The following instructions are enough to get Dendrite started as a non-federating test deployment using self-signed certificates and SQLite databases:
|
The following instructions are enough to get Dendrite started as a non-federating test deployment using self-signed certificates and SQLite databases:
|
||||||
|
|
||||||
```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
|
||||||
|
|
@ -79,47 +64,42 @@ $ ./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-config.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).
|
|
||||||
# Specify the localpart only, e.g. 'alice' for '@alice:domain.com'
|
|
||||||
$ ./bin/create-account --config dendrite.yaml --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 November 2020 we're at around 58% CS API coverage and 83% 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. There's a long list of features that are not implemented, notably:
|
||||||
|
- Push
|
||||||
|
- Search and Context
|
||||||
|
- User Directory
|
||||||
|
- Presence
|
||||||
|
- Guests
|
||||||
|
|
||||||
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 (User Directory, OpenID, Guests, Admin APIs, AS API).
|
||||||
This means Dendrite supports amongst others:
|
This means Dendrite supports amongst others:
|
||||||
|
- Core room functionality (creating rooms, invites, auth rules)
|
||||||
|
- Federation in rooms v1-v6
|
||||||
|
- Backfilling locally and via federation
|
||||||
|
- Accounts, Profiles and Devices
|
||||||
|
- Published room lists
|
||||||
|
- Typing
|
||||||
|
- Media APIs
|
||||||
|
- Redaction
|
||||||
|
- Tagging
|
||||||
|
- E2E keys and device lists
|
||||||
|
- Receipts
|
||||||
|
|
||||||
- Core room functionality (creating rooms, invites, auth rules)
|
|
||||||
- Room versions 1 to 10 supported
|
|
||||||
- Backfilling locally and via federation
|
|
||||||
- Accounts, profiles and devices
|
|
||||||
- Published room lists
|
|
||||||
- Typing
|
|
||||||
- Media APIs
|
|
||||||
- Redaction
|
|
||||||
- Tagging
|
|
||||||
- Context
|
|
||||||
- E2E keys and device lists
|
|
||||||
- Receipts
|
|
||||||
- Push
|
|
||||||
- Guests
|
|
||||||
- User Directory
|
|
||||||
- Presence
|
|
||||||
- Fulltext search
|
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
|
|
@ -128,8 +108,49 @@ We would be grateful for any help on issues marked as
|
||||||
all have related Sytests which need to pass in order for the issue to be closed. Once you've written your
|
all have related Sytests which need to pass in order for the issue to be closed. Once you've written your
|
||||||
code, you can quickly run Sytest to ensure that the test names are now passing.
|
code, you can quickly run Sytest to ensure that the test names are now passing.
|
||||||
|
|
||||||
If you're new to the project, see our
|
For example, if the test `Local device key changes get to remote servers` was marked as failing, find the
|
||||||
[Contributing page](https://matrix-org.github.io/dendrite/development/contributing) to get up to speed, then
|
test file (e.g via `grep` or via the
|
||||||
|
[CI log output](https://buildkite.com/matrix-dot-org/dendrite/builds/2826#39cff5de-e032-4ad0-ad26-f819e6919c42)
|
||||||
|
it's `tests/50federation/40devicelists.pl` ) then to run Sytest:
|
||||||
|
```
|
||||||
|
docker run --rm --name sytest
|
||||||
|
-v "/Users/kegan/github/sytest:/sytest"
|
||||||
|
-v "/Users/kegan/github/dendrite:/src"
|
||||||
|
-v "/Users/kegan/logs:/logs"
|
||||||
|
-v "/Users/kegan/go/:/gopath"
|
||||||
|
-e "POSTGRES=1" -e "DENDRITE_TRACE_HTTP=1"
|
||||||
|
matrixdotorg/sytest-dendrite:latest tests/50federation/40devicelists.pl
|
||||||
|
```
|
||||||
|
See [sytest.md](docs/sytest.md) for the full description of these flags.
|
||||||
|
|
||||||
|
You can try running sytest outside of docker for faster runs, but the dependencies can be temperamental
|
||||||
|
and we recommend using docker where possible.
|
||||||
|
```
|
||||||
|
cd sytest
|
||||||
|
export PERL5LIB=$HOME/lib/perl5
|
||||||
|
export PERL_MB_OPT=--install_base=$HOME
|
||||||
|
export PERL_MM_OPT=INSTALL_BASE=$HOME
|
||||||
|
./install-deps.pl
|
||||||
|
|
||||||
|
./run-tests.pl -I Dendrite::Monolith -d $PATH_TO_DENDRITE_BINARIES
|
||||||
|
```
|
||||||
|
|
||||||
|
Sometimes Sytest is testing the wrong thing or is flakey, so it will need to be patched.
|
||||||
|
Ask on `#dendrite-dev:matrix.org` if you think this is the case for you and we'll be happy to help.
|
||||||
|
|
||||||
|
If you're new to the project, see [CONTRIBUTING.md](docs/CONTRIBUTING.md) to get up to speed then
|
||||||
look for [Good First Issues](https://github.com/matrix-org/dendrite/labels/good%20first%20issue). If you're
|
look for [Good First Issues](https://github.com/matrix-org/dendrite/labels/good%20first%20issue). If you're
|
||||||
familiar with the project, look for [Help Wanted](https://github.com/matrix-org/dendrite/labels/help-wanted)
|
familiar with the project, look for [Help Wanted](https://github.com/matrix-org/dendrite/labels/help-wanted)
|
||||||
issues.
|
issues.
|
||||||
|
|
||||||
|
## Hardware requirements
|
||||||
|
|
||||||
|
Dendrite in Monolith + SQLite works in a range of environments including iOS and in-browser via WASM.
|
||||||
|
|
||||||
|
For small homeserver installations joined on ~10s rooms on matrix.org with ~100s of users in those rooms, including some
|
||||||
|
encrypted rooms:
|
||||||
|
- Memory: uses around 100MB of RAM, with peaks at around 200MB.
|
||||||
|
- Disk space: After a few months of usage, the database grew to around 2GB (in Monolith mode).
|
||||||
|
- CPU: Brief spikes when processing events, typically idles at 1% CPU.
|
||||||
|
|
||||||
|
This means Dendrite should comfortably work on things like Raspberry Pis.
|
||||||
|
|
|
||||||
10
appservice/README.md
Normal file
10
appservice/README.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
# Application Service
|
||||||
|
|
||||||
|
This component interfaces with external [Application
|
||||||
|
Services](https://matrix.org/docs/spec/application_service/unstable.html).
|
||||||
|
This includes any HTTP endpoints that application services call, as well as talking
|
||||||
|
to any HTTP endpoints that application services provide themselves.
|
||||||
|
|
||||||
|
## Consumers
|
||||||
|
|
||||||
|
This component consumes and filters events from the Roomserver Kafka stream, passing on any necessary events to subscribing application services.
|
||||||
|
|
@ -19,34 +19,14 @@ package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"database/sql"
|
||||||
"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"
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppServiceInternalAPI is used to query user and room alias data from application
|
|
||||||
// services
|
|
||||||
type AppServiceInternalAPI interface {
|
|
||||||
// Check whether a room alias exists within any application service namespaces
|
|
||||||
RoomAliasExists(
|
|
||||||
ctx context.Context,
|
|
||||||
req *RoomAliasExistsRequest,
|
|
||||||
resp *RoomAliasExistsResponse,
|
|
||||||
) error
|
|
||||||
// Check whether a user ID exists within any application service namespaces
|
|
||||||
UserIDExists(
|
|
||||||
ctx context.Context,
|
|
||||||
req *UserIDExistsRequest,
|
|
||||||
resp *UserIDExistsResponse,
|
|
||||||
) 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
|
||||||
// about whether a room alias exists
|
// about whether a room alias exists
|
||||||
type RoomAliasExistsRequest struct {
|
type RoomAliasExistsRequest struct {
|
||||||
|
|
@ -81,97 +61,42 @@ type UserIDExistsResponse struct {
|
||||||
UserIDExists bool `json:"exists"`
|
UserIDExists bool `json:"exists"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
// AppServiceQueryAPI is used to query user and room alias data from application
|
||||||
ASProtocolLegacyPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
// services
|
||||||
ASUserLegacyPath = "/_matrix/app/unstable/thirdparty/user"
|
type AppServiceQueryAPI interface {
|
||||||
ASLocationLegacyPath = "/_matrix/app/unstable/thirdparty/location"
|
// Check whether a room alias exists within any application service namespaces
|
||||||
ASRoomAliasExistsLegacyPath = "/rooms/"
|
RoomAliasExists(
|
||||||
ASUserExistsLegacyPath = "/users/"
|
ctx context.Context,
|
||||||
|
req *RoomAliasExistsRequest,
|
||||||
ASProtocolPath = "/_matrix/app/v1/thirdparty/protocol/"
|
resp *RoomAliasExistsResponse,
|
||||||
ASUserPath = "/_matrix/app/v1/thirdparty/user"
|
) error
|
||||||
ASLocationPath = "/_matrix/app/v1/thirdparty/location"
|
// Check whether a user ID exists within any application service namespaces
|
||||||
ASRoomAliasExistsPath = "/_matrix/app/v1/rooms/"
|
UserIDExists(
|
||||||
ASUserExistsPath = "/_matrix/app/v1/users/"
|
ctx context.Context,
|
||||||
)
|
req *UserIDExistsRequest,
|
||||||
|
resp *UserIDExistsResponse,
|
||||||
type ProtocolRequest struct {
|
) error
|
||||||
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
|
||||||
func RetrieveUserProfile(
|
func RetrieveUserProfile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
userID string,
|
userID string,
|
||||||
asAPI AppServiceInternalAPI,
|
asAPI AppServiceQueryAPI,
|
||||||
profileAPI userapi.ProfileAPI,
|
accountDB accounts.Database,
|
||||||
) (*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)
|
profile, err := accountDB.GetProfileByLocalpart(ctx, localpart)
|
||||||
if err == nil {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return nil, err
|
||||||
|
} else if profile != nil {
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,11 +109,11 @@ 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)
|
profile, err = accountDB.GetProfileByLocalpart(ctx, localpart)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,66 +16,99 @@ package appservice
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
|
"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"
|
|
||||||
|
|
||||||
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"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/types"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/workers"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/kafka"
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AddInternalRoutes registers HTTP handlers for internal API calls
|
||||||
|
func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceQueryAPI) {
|
||||||
|
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 *setup.BaseDendrite,
|
||||||
cfg *config.Dendrite,
|
userAPI userapi.UserInternalAPI,
|
||||||
natsInstance *jetstream.NATSInstance,
|
|
||||||
userAPI userapi.AppserviceUserAPI,
|
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) appserviceAPI.AppServiceInternalAPI {
|
) appserviceAPI.AppServiceQueryAPI {
|
||||||
|
client := &http.Client{
|
||||||
// Create appserivce query API with an HTTP client that will be used for all
|
Timeout: time.Second * 30,
|
||||||
// outbound and inbound requests (inbound only for the internal API)
|
Transport: &http.Transport{
|
||||||
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
DisableKeepAlives: true,
|
||||||
Cfg: &cfg.AppServiceAPI,
|
TLSClientConfig: &tls.Config{
|
||||||
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
|
||||||
CacheMu: sync.Mutex{},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
consumer, _ := kafka.SetupConsumerProducer(&base.Cfg.Global.Kafka)
|
||||||
|
|
||||||
if len(cfg.Derived.ApplicationServices) == 0 {
|
// Create a connection to the appservice postgres DB
|
||||||
return appserviceQueryAPI
|
appserviceDB, err := storage.NewDatabase(&base.Cfg.AppServiceAPI.Database)
|
||||||
|
if err != nil {
|
||||||
|
logrus.WithError(err).Panicf("failed to connect to appservice db")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
workerStates := make([]types.ApplicationServiceWorkerState, len(base.Cfg.Derived.ApplicationServices))
|
||||||
|
for i, appservice := range base.Cfg.Derived.ApplicationServices {
|
||||||
|
m := sync.Mutex{}
|
||||||
|
ws := types.ApplicationServiceWorkerState{
|
||||||
|
AppService: appservice,
|
||||||
|
Cond: sync.NewCond(&m),
|
||||||
|
}
|
||||||
|
workerStates[i] = ws
|
||||||
|
|
||||||
// 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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create appserivce query API with an HTTP client that will be used for all
|
||||||
|
// outbound and inbound requests (inbound only for the internal API)
|
||||||
|
appserviceQueryAPI := &query.AppServiceQueryAPI{
|
||||||
|
HTTPClient: client,
|
||||||
|
Cfg: base.Cfg,
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
if len(workerStates) > 0 {
|
||||||
consumer := consumers.NewOutputRoomEventConsumer(
|
consumer := consumers.NewOutputRoomEventConsumer(
|
||||||
processContext, &cfg.AppServiceAPI,
|
base.ProcessContext, base.Cfg, consumer, appserviceDB,
|
||||||
js, rsAPI,
|
rsAPI, workerStates,
|
||||||
)
|
)
|
||||||
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")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create application service transaction workers
|
||||||
|
if err := workers.SetupTransactionWorkers(client, appserviceDB, workerStates); err != nil {
|
||||||
|
logrus.WithError(err).Panicf("failed to start app service transaction workers")
|
||||||
|
}
|
||||||
return appserviceQueryAPI
|
return appserviceQueryAPI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,15 +116,13 @@ func NewInternalAPI(
|
||||||
// `sender_localpart` field of each application service if it doesn't
|
// `sender_localpart` field of each application service if it doesn't
|
||||||
// exist already
|
// exist already
|
||||||
func generateAppServiceAccount(
|
func generateAppServiceAccount(
|
||||||
userAPI userapi.AppserviceUserAPI,
|
userAPI userapi.UserInternalAPI,
|
||||||
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.AccountTypeUser,
|
||||||
Localpart: as.SenderLocalpart,
|
Localpart: as.SenderLocalpart,
|
||||||
ServerName: serverName,
|
|
||||||
AppServiceID: as.ID,
|
AppServiceID: as.ID,
|
||||||
OnConflict: userapi.ConflictUpdate,
|
OnConflict: userapi.ConflictUpdate,
|
||||||
}, &accRes)
|
}, &accRes)
|
||||||
|
|
@ -101,11 +132,9 @@ 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,
|
||||||
NoDeviceListUpdate: true,
|
|
||||||
}, &devRes)
|
}, &devRes)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -15,267 +15,173 @@
|
||||||
package consumers
|
package consumers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"math"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/types"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
"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/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
"github.com/matrix-org/dendrite/syncapi/synctypes"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
|
"github.com/Shopify/sarama"
|
||||||
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
|
roomServerConsumer *internal.ContinualConsumer
|
||||||
cfg *config.AppServiceAPI
|
asDB storage.Database
|
||||||
jetstream nats.JetStreamContext
|
rsAPI api.RoomserverInternalAPI
|
||||||
topic string
|
serverName string
|
||||||
rsAPI api.AppserviceRoomserverAPI
|
workerStates []types.ApplicationServiceWorkerState
|
||||||
}
|
|
||||||
|
|
||||||
type appserviceState struct {
|
|
||||||
*config.ApplicationService
|
|
||||||
backoff int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call
|
// NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call
|
||||||
// Start() to begin consuming from room servers.
|
// Start() to begin consuming from room servers.
|
||||||
func NewOutputRoomEventConsumer(
|
func NewOutputRoomEventConsumer(
|
||||||
process *process.ProcessContext,
|
process *process.ProcessContext,
|
||||||
cfg *config.AppServiceAPI,
|
cfg *config.Dendrite,
|
||||||
js nats.JetStreamContext,
|
kafkaConsumer sarama.Consumer,
|
||||||
rsAPI api.AppserviceRoomserverAPI,
|
appserviceDB storage.Database,
|
||||||
|
rsAPI api.RoomserverInternalAPI,
|
||||||
|
workerStates []types.ApplicationServiceWorkerState,
|
||||||
) *OutputRoomEventConsumer {
|
) *OutputRoomEventConsumer {
|
||||||
return &OutputRoomEventConsumer{
|
consumer := internal.ContinualConsumer{
|
||||||
ctx: process.Context(),
|
Process: process,
|
||||||
cfg: cfg,
|
ComponentName: "appservice/roomserver",
|
||||||
jetstream: js,
|
Topic: cfg.Global.Kafka.TopicFor(config.TopicOutputRoomEvent),
|
||||||
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputAppserviceEvent),
|
Consumer: kafkaConsumer,
|
||||||
rsAPI: rsAPI,
|
PartitionStore: appserviceDB,
|
||||||
}
|
}
|
||||||
|
s := &OutputRoomEventConsumer{
|
||||||
|
roomServerConsumer: &consumer,
|
||||||
|
asDB: appserviceDB,
|
||||||
|
rsAPI: rsAPI,
|
||||||
|
serverName: string(cfg.Global.ServerName),
|
||||||
|
workerStates: workerStates,
|
||||||
|
}
|
||||||
|
consumer.ProcessMessage = s.onMessage
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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))
|
return s.roomServerConsumer.Start()
|
||||||
for _, as := range s.cfg.Derived.ApplicationServices {
|
|
||||||
appsvc := as
|
|
||||||
state := &appserviceState{
|
|
||||||
ApplicationService: &appsvc,
|
|
||||||
}
|
|
||||||
token := jetstream.Tokenise(as.ID)
|
|
||||||
if err := jetstream.JetStreamConsumer(
|
|
||||||
s.ctx, s.jetstream, s.topic,
|
|
||||||
s.cfg.Matrix.JetStream.Durable("Appservice_"+token),
|
|
||||||
50, // maximum number of events to send in a single transaction
|
|
||||||
func(ctx context.Context, msgs []*nats.Msg) bool {
|
|
||||||
return s.onMessage(ctx, state, msgs)
|
|
||||||
},
|
|
||||||
nats.DeliverNew(), nats.ManualAck(),
|
|
||||||
); err != nil {
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// onMessage is called when the appservice component receives a new event from
|
// onMessage is called when the appservice component receives a new event from
|
||||||
// the room server output log.
|
// the room server output log.
|
||||||
func (s *OutputRoomEventConsumer) onMessage(
|
func (s *OutputRoomEventConsumer) onMessage(msg *sarama.ConsumerMessage) error {
|
||||||
ctx context.Context, state *appserviceState, msgs []*nats.Msg,
|
|
||||||
) bool {
|
|
||||||
log.WithField("appservice", state.ID).Tracef("Appservice worker received %d message(s) from roomserver", len(msgs))
|
|
||||||
events := make([]*types.HeaderedEvent, 0, len(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.Value, &output); err != nil {
|
||||||
// If the message was invalid, log it and move on to the next message in the stream
|
// If the message was invalid, log it and move on to the next message in the stream
|
||||||
log.WithField("appservice", state.ID).WithError(err).Errorf("Appservice failed to parse message, ignoring")
|
log.WithError(err).Errorf("roomserver output log: message parse failure")
|
||||||
continue
|
return nil
|
||||||
}
|
|
||||||
switch output.Type {
|
|
||||||
case api.OutputTypeNewRoomEvent:
|
|
||||||
if output.NewRoomEvent == nil || !s.appserviceIsInterestedInEvent(ctx, output.NewRoomEvent.Event, state.ApplicationService) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
events = append(events, output.NewRoomEvent.Event)
|
|
||||||
if len(output.NewRoomEvent.AddsStateEventIDs) > 0 {
|
|
||||||
newEventID := output.NewRoomEvent.Event.EventID()
|
|
||||||
eventsReq := &api.QueryEventsByIDRequest{
|
|
||||||
RoomID: output.NewRoomEvent.Event.RoomID().String(),
|
|
||||||
EventIDs: make([]string, 0, len(output.NewRoomEvent.AddsStateEventIDs)),
|
|
||||||
}
|
|
||||||
eventsRes := &api.QueryEventsByIDResponse{}
|
|
||||||
for _, eventID := range output.NewRoomEvent.AddsStateEventIDs {
|
|
||||||
if eventID != newEventID {
|
|
||||||
eventsReq.EventIDs = append(eventsReq.EventIDs, eventID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(eventsReq.EventIDs) > 0 {
|
|
||||||
if err := s.rsAPI.QueryEventsByID(s.ctx, eventsReq, eventsRes); err != nil {
|
|
||||||
log.WithError(err).Errorf("s.rsAPI.QueryEventsByID failed")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
events = append(events, eventsRes.Events...)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
if output.Type != api.OutputTypeNewRoomEvent {
|
||||||
continue
|
return nil
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there are no events selected for sending then we should
|
events := []*gomatrixserverlib.HeaderedEvent{output.NewRoomEvent.Event}
|
||||||
// ack the messages so that we don't get sent them again in the
|
events = append(events, output.NewRoomEvent.AddStateEvents...)
|
||||||
// future.
|
|
||||||
if len(events) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
txnID := ""
|
// Send event to any relevant application services
|
||||||
// Try to get the message metadata, if we're able to, use the timestamp as the txnID
|
return s.filterRoomserverEvents(context.TODO(), events)
|
||||||
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
|
|
||||||
// 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))
|
|
||||||
return s.sendEvents(ctx, state, events, txnID) == nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendEvents passes events to the appservice by using the transactions
|
// filterRoomserverEvents takes in events and decides whether any of them need
|
||||||
// endpoint. It will block for the backoff period if necessary.
|
// to be passed on to an external application service. It does this by checking
|
||||||
func (s *OutputRoomEventConsumer) sendEvents(
|
// each namespace of each registered application service, and if there is a
|
||||||
ctx context.Context, state *appserviceState,
|
// match, adds the event to the queue for events to be sent to a particular
|
||||||
events []*types.HeaderedEvent,
|
// application service.
|
||||||
txnID string,
|
func (s *OutputRoomEventConsumer) filterRoomserverEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
events []*gomatrixserverlib.HeaderedEvent,
|
||||||
) error {
|
) error {
|
||||||
// Create the transaction body.
|
for _, ws := range s.workerStates {
|
||||||
transaction, err := json.Marshal(
|
for _, event := range events {
|
||||||
ApplicationServiceTransaction{
|
// Check if this event is interesting to this application service
|
||||||
Events: synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(events), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
if s.appserviceIsInterestedInEvent(ctx, event, ws.AppService) {
|
||||||
return s.rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
// Queue this event to be sent off to the application service
|
||||||
}),
|
if err := s.asDB.StoreEvent(ctx, ws.AppService.ID, event); err != nil {
|
||||||
},
|
log.WithError(err).Warn("failed to insert incoming event into appservices database")
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
return err
|
||||||
|
} else {
|
||||||
|
// Tell our worker to send out new messages by updating remaining message
|
||||||
|
// count and waking them up with a broadcast
|
||||||
|
ws.NotifyNewEvents()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If txnID is not defined, generate one from the events.
|
|
||||||
if txnID == "" {
|
|
||||||
txnID = fmt.Sprintf("%d_%d", events[0].PDU.OriginServerTS(), len(transaction))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send the transaction to the appservice.
|
|
||||||
// https://spec.matrix.org/v1.9/application-service-api/#pushing-events
|
|
||||||
path := "_matrix/app/v1/transactions"
|
|
||||||
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))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", state.HSToken))
|
|
||||||
resp, err := state.HTTPClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return state.backoffAndPause(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the response was fine then we can clear any backoffs in place and
|
|
||||||
// report that everything was OK. Otherwise, back off for a while.
|
|
||||||
switch resp.StatusCode {
|
|
||||||
case http.StatusOK:
|
|
||||||
state.backoff = 0
|
|
||||||
default:
|
|
||||||
return state.backoffAndPause(fmt.Errorf("received HTTP status code %d from appservice url %s", resp.StatusCode, address))
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// backoff pauses the calling goroutine for a 2^some backoff exponent seconds
|
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
||||||
func (s *appserviceState) backoffAndPause(err error) error {
|
// appservice has membership at the time a given event was created.
|
||||||
if s.backoff < 6 {
|
func (s *OutputRoomEventConsumer) appserviceJoinedAtEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice config.ApplicationService) bool {
|
||||||
s.backoff++
|
// 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
|
||||||
|
// until we have a lighter way of checking the state before the event that
|
||||||
|
// doesn't involve state res, then this is probably OK.
|
||||||
|
membershipReq := &api.QueryMembershipsForRoomRequest{
|
||||||
|
RoomID: event.RoomID(),
|
||||||
|
JoinedOnly: true,
|
||||||
}
|
}
|
||||||
duration := time.Second * time.Duration(math.Pow(2, float64(s.backoff)))
|
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
||||||
log.WithField("appservice", s.ID).WithError(err).Errorf("Unable to send transaction to appservice, backing off for %s", duration.String())
|
|
||||||
time.Sleep(duration)
|
// XXX: This could potentially race if the state for the event is not known yet
|
||||||
return err
|
// e.g. the event came over federation but we do not have the full state persisted.
|
||||||
|
if err := s.rsAPI.QueryMembershipsForRoom(ctx, membershipReq, membershipRes); err == nil {
|
||||||
|
for _, ev := range membershipRes.JoinEvents {
|
||||||
|
var membership gomatrixserverlib.MemberContent
|
||||||
|
if err = json.Unmarshal(ev.Content, &membership); err != nil || ev.StateKey == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"room_id": event.RoomID(),
|
||||||
|
}).WithError(err).Errorf("Unable to get membership for room")
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// appserviceIsInterestedInEvent returns a boolean depending on whether a given
|
// appserviceIsInterestedInEvent returns a boolean depending on whether a given
|
||||||
// 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 := ""
|
// No reason to queue events if they'll never be sent to the application
|
||||||
userID, err := s.rsAPI.QueryUserIDForSender(ctx, event.RoomID(), event.SenderID())
|
// service
|
||||||
if err == nil {
|
if appservice.URL == "" {
|
||||||
user = userID.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case appservice.URL == "":
|
|
||||||
return false
|
return false
|
||||||
case appservice.IsInterestedInUserID(user):
|
}
|
||||||
return true
|
|
||||||
case appservice.IsInterestedInRoomID(event.RoomID().String()):
|
// Check Room ID and Sender of the event
|
||||||
|
if appservice.IsInterestedInUserID(event.Sender()) ||
|
||||||
|
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 {
|
||||||
|
|
@ -285,54 +191,10 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"appservice": appservice.ID,
|
"room_id": event.RoomID(),
|
||||||
"room_id": event.RoomID().String(),
|
|
||||||
}).WithError(err).Errorf("Unable to get aliases for room")
|
}).WithError(err).Errorf("Unable to get aliases for room")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if any of the members in the room match the appservice
|
// Check if any of the members in the room match the appservice
|
||||||
return s.appserviceJoinedAtEvent(ctx, event, appservice)
|
return s.appserviceJoinedAtEvent(ctx, event, appservice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// appserviceJoinedAtEvent returns a boolean depending on whether a given
|
|
||||||
// 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 {
|
|
||||||
// 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
|
|
||||||
// until we have a lighter way of checking the state before the event that
|
|
||||||
// doesn't involve state res, then this is probably OK.
|
|
||||||
membershipReq := &api.QueryMembershipsForRoomRequest{
|
|
||||||
RoomID: event.RoomID().String(),
|
|
||||||
JoinedOnly: true,
|
|
||||||
}
|
|
||||||
membershipRes := &api.QueryMembershipsForRoomResponse{}
|
|
||||||
|
|
||||||
// XXX: This could potentially race if the state for the event is not known yet
|
|
||||||
// e.g. the event came over federation but we do not have the full state persisted.
|
|
||||||
if err := s.rsAPI.QueryMembershipsForRoom(ctx, membershipReq, membershipRes); err == nil {
|
|
||||||
for _, ev := range membershipRes.JoinEvents {
|
|
||||||
switch {
|
|
||||||
case ev.StateKey == nil:
|
|
||||||
continue
|
|
||||||
case ev.Type != spec.MRoomMember:
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var membership gomatrixserverlib.MemberContent
|
|
||||||
err = json.Unmarshal(ev.Content, &membership)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
continue
|
|
||||||
case membership.Membership == spec.Join:
|
|
||||||
if appservice.IsInterestedInUserID(*ev.StateKey) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.WithFields(log.Fields{
|
|
||||||
"appservice": appservice.ID,
|
|
||||||
"room_id": event.RoomID().String(),
|
|
||||||
}).WithError(err).Errorf("Unable to get membership for room")
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
|
||||||
63
appservice/inthttp/client.go
Normal file
63
appservice/inthttp/client.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package inthttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/opentracing/opentracing-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.AppServiceQueryAPI, 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 {
|
||||||
|
span, ctx := opentracing.StartSpanFromContext(ctx, "appserviceRoomAliasExists")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
apiURL := h.appserviceURL + AppServiceRoomAliasExistsPath
|
||||||
|
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserIDExists implements AppServiceQueryAPI
|
||||||
|
func (h *httpAppServiceQueryAPI) UserIDExists(
|
||||||
|
ctx context.Context,
|
||||||
|
request *api.UserIDExistsRequest,
|
||||||
|
response *api.UserIDExistsResponse,
|
||||||
|
) error {
|
||||||
|
span, ctx := opentracing.StartSpanFromContext(ctx, "appserviceUserIDExists")
|
||||||
|
defer span.Finish()
|
||||||
|
|
||||||
|
apiURL := h.appserviceURL + AppServiceUserIDExistsPath
|
||||||
|
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response)
|
||||||
|
}
|
||||||
43
appservice/inthttp/server.go
Normal file
43
appservice/inthttp/server.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package inthttp
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/api"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddRoutes adds the AppServiceQueryAPI handlers to the http.ServeMux.
|
||||||
|
func AddRoutes(a api.AppServiceQueryAPI, internalAPIMux *mux.Router) {
|
||||||
|
internalAPIMux.Handle(
|
||||||
|
AppServiceRoomAliasExistsPath,
|
||||||
|
httputil.MakeInternalAPI("appserviceRoomAliasExists", func(req *http.Request) util.JSONResponse {
|
||||||
|
var request api.RoomAliasExistsRequest
|
||||||
|
var response api.RoomAliasExistsResponse
|
||||||
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
if err := a.RoomAliasExists(req.Context(), &request, &response); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
internalAPIMux.Handle(
|
||||||
|
AppServiceUserIDExistsPath,
|
||||||
|
httputil.MakeInternalAPI("appserviceUserIDExists", func(req *http.Request) util.JSONResponse {
|
||||||
|
var request api.UserIDExistsRequest
|
||||||
|
var response api.UserIDExistsResponse
|
||||||
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
if err := a.UserIDExists(req.Context(), &request, &response); err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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.Dendrite
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
32
appservice/storage/interface.go
Normal file
32
appservice/storage/interface.go
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2020 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Database interface {
|
||||||
|
internal.PartitionStorer
|
||||||
|
StoreEvent(ctx context.Context, appServiceID string, event *gomatrixserverlib.HeaderedEvent) error
|
||||||
|
GetEventsWithAppServiceID(ctx context.Context, appServiceID string, limit int) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error)
|
||||||
|
CountEventsWithAppServiceID(ctx context.Context, appServiceID string) (int, error)
|
||||||
|
UpdateTxnIDForEvents(ctx context.Context, appserviceID string, maxID, txnID int) error
|
||||||
|
RemoveEventsBeforeAndIncludingID(ctx context.Context, appserviceID string, eventTableID int) error
|
||||||
|
GetLatestTxnID(ctx context.Context) (int, error)
|
||||||
|
}
|
||||||
256
appservice/storage/postgres/appservice_events_table.go
Normal file
256
appservice/storage/postgres/appservice_events_table.go
Normal file
|
|
@ -0,0 +1,256 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appserviceEventsSchema = `
|
||||||
|
-- Stores events to be sent to application services
|
||||||
|
CREATE TABLE IF NOT EXISTS appservice_events (
|
||||||
|
-- An auto-incrementing id unique to each event in the table
|
||||||
|
id BIGSERIAL NOT NULL PRIMARY KEY,
|
||||||
|
-- The ID of the application service the event will be sent to
|
||||||
|
as_id TEXT NOT NULL,
|
||||||
|
-- JSON representation of the event
|
||||||
|
headered_event_json TEXT NOT NULL,
|
||||||
|
-- The ID of the transaction that this event is a part of
|
||||||
|
txn_id BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS appservice_events_as_id ON appservice_events(as_id);
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectEventsByApplicationServiceIDSQL = "" +
|
||||||
|
"SELECT id, headered_event_json, txn_id " +
|
||||||
|
"FROM appservice_events WHERE as_id = $1 ORDER BY txn_id DESC, id ASC"
|
||||||
|
|
||||||
|
const countEventsByApplicationServiceIDSQL = "" +
|
||||||
|
"SELECT COUNT(id) FROM appservice_events WHERE as_id = $1"
|
||||||
|
|
||||||
|
const insertEventSQL = "" +
|
||||||
|
"INSERT INTO appservice_events(as_id, headered_event_json, txn_id) " +
|
||||||
|
"VALUES ($1, $2, $3)"
|
||||||
|
|
||||||
|
const updateTxnIDForEventsSQL = "" +
|
||||||
|
"UPDATE appservice_events SET txn_id = $1 WHERE as_id = $2 AND id <= $3"
|
||||||
|
|
||||||
|
const deleteEventsBeforeAndIncludingIDSQL = "" +
|
||||||
|
"DELETE FROM appservice_events WHERE as_id = $1 AND id <= $2"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A transaction ID number that no transaction should ever have. Used for
|
||||||
|
// checking again the default value.
|
||||||
|
invalidTxnID = -2
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventsStatements struct {
|
||||||
|
selectEventsByApplicationServiceIDStmt *sql.Stmt
|
||||||
|
countEventsByApplicationServiceIDStmt *sql.Stmt
|
||||||
|
insertEventStmt *sql.Stmt
|
||||||
|
updateTxnIDForEventsStmt *sql.Stmt
|
||||||
|
deleteEventsBeforeAndIncludingIDStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventsStatements) prepare(db *sql.DB) (err error) {
|
||||||
|
_, err = db.Exec(appserviceEventsSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.selectEventsByApplicationServiceIDStmt, err = db.Prepare(selectEventsByApplicationServiceIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.countEventsByApplicationServiceIDStmt, err = db.Prepare(countEventsByApplicationServiceIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.insertEventStmt, err = db.Prepare(insertEventSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.updateTxnIDForEventsStmt, err = db.Prepare(updateTxnIDForEventsSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.deleteEventsBeforeAndIncludingIDStmt, err = db.Prepare(deleteEventsBeforeAndIncludingIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectEventsByApplicationServiceID takes in an application service ID and
|
||||||
|
// returns a slice of events that need to be sent to that application service,
|
||||||
|
// as well as an int later used to remove these same events from the database
|
||||||
|
// once successfully sent to an application service.
|
||||||
|
func (s *eventsStatements) selectEventsByApplicationServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
applicationServiceID string,
|
||||||
|
limit int,
|
||||||
|
) (
|
||||||
|
txnID, maxID int,
|
||||||
|
events []gomatrixserverlib.HeaderedEvent,
|
||||||
|
eventsRemaining bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": applicationServiceID,
|
||||||
|
}).WithError(err).Fatalf("appservice unable to select new events to send")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Retrieve events from the database. Unsuccessfully sent events first
|
||||||
|
eventRows, err := s.selectEventsByApplicationServiceIDStmt.QueryContext(ctx, applicationServiceID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer checkNamedErr(eventRows.Close, &err)
|
||||||
|
events, maxID, txnID, eventsRemaining, err = retrieveEvents(eventRows, limit)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
||||||
|
func checkNamedErr(fn func() error, err *error) {
|
||||||
|
if e := fn(); e != nil && *err == nil {
|
||||||
|
*err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveEvents(eventRows *sql.Rows, limit int) (events []gomatrixserverlib.HeaderedEvent, maxID, txnID int, eventsRemaining bool, err error) {
|
||||||
|
// Get current time for use in calculating event age
|
||||||
|
nowMilli := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
// Iterate through each row and store event contents
|
||||||
|
// If txn_id changes dramatically, we've switched from collecting old events to
|
||||||
|
// new ones. Send back those events first.
|
||||||
|
lastTxnID := invalidTxnID
|
||||||
|
for eventsProcessed := 0; eventRows.Next(); {
|
||||||
|
var event gomatrixserverlib.HeaderedEvent
|
||||||
|
var eventJSON []byte
|
||||||
|
var id int
|
||||||
|
err = eventRows.Scan(
|
||||||
|
&id,
|
||||||
|
&eventJSON,
|
||||||
|
&txnID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal eventJSON
|
||||||
|
if err = json.Unmarshal(eventJSON, &event); err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If txnID has changed on this event from the previous event, then we've
|
||||||
|
// reached the end of a transaction's events. Return only those events.
|
||||||
|
if lastTxnID > invalidTxnID && lastTxnID != txnID {
|
||||||
|
return events, maxID, lastTxnID, true, nil
|
||||||
|
}
|
||||||
|
lastTxnID = txnID
|
||||||
|
|
||||||
|
// Limit events that aren't part of an old transaction
|
||||||
|
if txnID == -1 {
|
||||||
|
// Return if we've hit the limit
|
||||||
|
if eventsProcessed++; eventsProcessed > limit {
|
||||||
|
return events, maxID, lastTxnID, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if id > maxID {
|
||||||
|
maxID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portion of the event that is unsigned due to rapid change
|
||||||
|
// TODO: Consider removing age as not many app services use it
|
||||||
|
if err = event.SetUnsignedField("age", nowMilli-int64(event.OriginServerTS())); err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
events = append(events, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// countEventsByApplicationServiceID inserts an event mapped to its corresponding application service
|
||||||
|
// IDs into the db.
|
||||||
|
func (s *eventsStatements) countEventsByApplicationServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := s.countEventsByApplicationServiceIDStmt.QueryRowContext(ctx, appServiceID).Scan(&count)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertEvent inserts an event mapped to its corresponding application service
|
||||||
|
// IDs into the db.
|
||||||
|
func (s *eventsStatements) insertEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
event *gomatrixserverlib.HeaderedEvent,
|
||||||
|
) (err error) {
|
||||||
|
// Convert event to JSON before inserting
|
||||||
|
eventJSON, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.insertEventStmt.ExecContext(
|
||||||
|
ctx,
|
||||||
|
appServiceID,
|
||||||
|
eventJSON,
|
||||||
|
-1, // No transaction ID yet
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateTxnIDForEvents sets the transactionID for a collection of events. Done
|
||||||
|
// before sending them to an AppService. Referenced before sending to make sure
|
||||||
|
// we aren't constructing multiple transactions with the same events.
|
||||||
|
func (s *eventsStatements) updateTxnIDForEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
maxID, txnID int,
|
||||||
|
) (err error) {
|
||||||
|
_, err = s.updateTxnIDForEventsStmt.ExecContext(ctx, txnID, appserviceID, maxID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteEventsBeforeAndIncludingID removes events matching given IDs from the database.
|
||||||
|
func (s *eventsStatements) deleteEventsBeforeAndIncludingID(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
eventTableID int,
|
||||||
|
) (err error) {
|
||||||
|
_, err = s.deleteEventsBeforeAndIncludingIDStmt.ExecContext(ctx, appserviceID, eventTableID)
|
||||||
|
return
|
||||||
|
}
|
||||||
119
appservice/storage/postgres/storage.go
Normal file
119
appservice/storage/postgres/storage.go
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
// Import postgres database driver
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database stores events intended to be later sent to application services
|
||||||
|
type Database struct {
|
||||||
|
sqlutil.PartitionOffsetStatements
|
||||||
|
events eventsStatements
|
||||||
|
txnID txnStatements
|
||||||
|
db *sql.DB
|
||||||
|
writer sqlutil.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase opens a new database
|
||||||
|
func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) {
|
||||||
|
var result Database
|
||||||
|
var err error
|
||||||
|
if result.db, err = sqlutil.Open(dbProperties); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.writer = sqlutil.NewDummyWriter()
|
||||||
|
if err = result.prepare(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = result.PartitionOffsetStatements.Prepare(result.db, result.writer, "appservice"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) prepare() error {
|
||||||
|
if err := d.events.prepare(d.db); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.txnID.prepare(d.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreEvent takes in a gomatrixserverlib.HeaderedEvent and stores it in the database
|
||||||
|
// for a transaction worker to pull and later send to an application service.
|
||||||
|
func (d *Database) StoreEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
event *gomatrixserverlib.HeaderedEvent,
|
||||||
|
) error {
|
||||||
|
return d.events.insertEvent(ctx, appServiceID, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEventsWithAppServiceID returns a slice of events and their IDs intended to
|
||||||
|
// be sent to an application service given its ID.
|
||||||
|
func (d *Database) GetEventsWithAppServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
limit int,
|
||||||
|
) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error) {
|
||||||
|
return d.events.selectEventsByApplicationServiceID(ctx, appServiceID, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountEventsWithAppServiceID returns the number of events destined for an
|
||||||
|
// application service given its ID.
|
||||||
|
func (d *Database) CountEventsWithAppServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
) (int, error) {
|
||||||
|
return d.events.countEventsByApplicationServiceID(ctx, appServiceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTxnIDForEvents takes in an application service ID and a
|
||||||
|
// and stores them in the DB, unless the pair already exists, in
|
||||||
|
// which case it updates them.
|
||||||
|
func (d *Database) UpdateTxnIDForEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
maxID, txnID int,
|
||||||
|
) error {
|
||||||
|
return d.events.updateTxnIDForEvents(ctx, appserviceID, maxID, txnID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEventsBeforeAndIncludingID removes all events from the database that
|
||||||
|
// are less than or equal to a given maximum ID. IDs here are implemented as a
|
||||||
|
// serial, thus this should always delete events in chronological order.
|
||||||
|
func (d *Database) RemoveEventsBeforeAndIncludingID(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
eventTableID int,
|
||||||
|
) error {
|
||||||
|
return d.events.deleteEventsBeforeAndIncludingID(ctx, appserviceID, eventTableID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestTxnID returns the latest available transaction id
|
||||||
|
func (d *Database) GetLatestTxnID(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int, error) {
|
||||||
|
return d.txnID.selectTxnID(ctx)
|
||||||
|
}
|
||||||
53
appservice/storage/postgres/txn_id_counter_table.go
Normal file
53
appservice/storage/postgres/txn_id_counter_table.go
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
)
|
||||||
|
|
||||||
|
const txnIDSchema = `
|
||||||
|
-- Keeps a count of the current transaction ID
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS txn_id_counter START 1;
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectTxnIDSQL = "SELECT nextval('txn_id_counter')"
|
||||||
|
|
||||||
|
type txnStatements struct {
|
||||||
|
selectTxnIDStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *txnStatements) prepare(db *sql.DB) (err error) {
|
||||||
|
_, err = db.Exec(txnIDSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.selectTxnIDStmt, err = db.Prepare(selectTxnIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectTxnID selects the latest ascending transaction ID
|
||||||
|
func (s *txnStatements) selectTxnID(
|
||||||
|
ctx context.Context,
|
||||||
|
) (txnID int, err error) {
|
||||||
|
err = s.selectTxnIDStmt.QueryRowContext(ctx).Scan(&txnID)
|
||||||
|
return
|
||||||
|
}
|
||||||
267
appservice/storage/sqlite3/appservice_events_table.go
Normal file
267
appservice/storage/sqlite3/appservice_events_table.go
Normal file
|
|
@ -0,0 +1,267 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
const appserviceEventsSchema = `
|
||||||
|
-- Stores events to be sent to application services
|
||||||
|
CREATE TABLE IF NOT EXISTS appservice_events (
|
||||||
|
-- An auto-incrementing id unique to each event in the table
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
-- The ID of the application service the event will be sent to
|
||||||
|
as_id TEXT NOT NULL,
|
||||||
|
-- JSON representation of the event
|
||||||
|
headered_event_json TEXT NOT NULL,
|
||||||
|
-- The ID of the transaction that this event is a part of
|
||||||
|
txn_id INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS appservice_events_as_id ON appservice_events(as_id);
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectEventsByApplicationServiceIDSQL = "" +
|
||||||
|
"SELECT id, headered_event_json, txn_id " +
|
||||||
|
"FROM appservice_events WHERE as_id = $1 ORDER BY txn_id DESC, id ASC"
|
||||||
|
|
||||||
|
const countEventsByApplicationServiceIDSQL = "" +
|
||||||
|
"SELECT COUNT(id) FROM appservice_events WHERE as_id = $1"
|
||||||
|
|
||||||
|
const insertEventSQL = "" +
|
||||||
|
"INSERT INTO appservice_events(as_id, headered_event_json, txn_id) " +
|
||||||
|
"VALUES ($1, $2, $3)"
|
||||||
|
|
||||||
|
const updateTxnIDForEventsSQL = "" +
|
||||||
|
"UPDATE appservice_events SET txn_id = $1 WHERE as_id = $2 AND id <= $3"
|
||||||
|
|
||||||
|
const deleteEventsBeforeAndIncludingIDSQL = "" +
|
||||||
|
"DELETE FROM appservice_events WHERE as_id = $1 AND id <= $2"
|
||||||
|
|
||||||
|
const (
|
||||||
|
// A transaction ID number that no transaction should ever have. Used for
|
||||||
|
// checking again the default value.
|
||||||
|
invalidTxnID = -2
|
||||||
|
)
|
||||||
|
|
||||||
|
type eventsStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
writer sqlutil.Writer
|
||||||
|
selectEventsByApplicationServiceIDStmt *sql.Stmt
|
||||||
|
countEventsByApplicationServiceIDStmt *sql.Stmt
|
||||||
|
insertEventStmt *sql.Stmt
|
||||||
|
updateTxnIDForEventsStmt *sql.Stmt
|
||||||
|
deleteEventsBeforeAndIncludingIDStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *eventsStatements) prepare(db *sql.DB, writer sqlutil.Writer) (err error) {
|
||||||
|
s.db = db
|
||||||
|
s.writer = writer
|
||||||
|
_, err = db.Exec(appserviceEventsSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.selectEventsByApplicationServiceIDStmt, err = db.Prepare(selectEventsByApplicationServiceIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.countEventsByApplicationServiceIDStmt, err = db.Prepare(countEventsByApplicationServiceIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.insertEventStmt, err = db.Prepare(insertEventSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.updateTxnIDForEventsStmt, err = db.Prepare(updateTxnIDForEventsSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.deleteEventsBeforeAndIncludingIDStmt, err = db.Prepare(deleteEventsBeforeAndIncludingIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectEventsByApplicationServiceID takes in an application service ID and
|
||||||
|
// returns a slice of events that need to be sent to that application service,
|
||||||
|
// as well as an int later used to remove these same events from the database
|
||||||
|
// once successfully sent to an application service.
|
||||||
|
func (s *eventsStatements) selectEventsByApplicationServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
applicationServiceID string,
|
||||||
|
limit int,
|
||||||
|
) (
|
||||||
|
txnID, maxID int,
|
||||||
|
events []gomatrixserverlib.HeaderedEvent,
|
||||||
|
eventsRemaining bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": applicationServiceID,
|
||||||
|
}).WithError(err).Fatalf("appservice unable to select new events to send")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
// Retrieve events from the database. Unsuccessfully sent events first
|
||||||
|
eventRows, err := s.selectEventsByApplicationServiceIDStmt.QueryContext(ctx, applicationServiceID)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer checkNamedErr(eventRows.Close, &err)
|
||||||
|
events, maxID, txnID, eventsRemaining, err = retrieveEvents(eventRows, limit)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
||||||
|
func checkNamedErr(fn func() error, err *error) {
|
||||||
|
if e := fn(); e != nil && *err == nil {
|
||||||
|
*err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveEvents(eventRows *sql.Rows, limit int) (events []gomatrixserverlib.HeaderedEvent, maxID, txnID int, eventsRemaining bool, err error) {
|
||||||
|
// Get current time for use in calculating event age
|
||||||
|
nowMilli := time.Now().UnixNano() / int64(time.Millisecond)
|
||||||
|
|
||||||
|
// Iterate through each row and store event contents
|
||||||
|
// If txn_id changes dramatically, we've switched from collecting old events to
|
||||||
|
// new ones. Send back those events first.
|
||||||
|
lastTxnID := invalidTxnID
|
||||||
|
for eventsProcessed := 0; eventRows.Next(); {
|
||||||
|
var event gomatrixserverlib.HeaderedEvent
|
||||||
|
var eventJSON []byte
|
||||||
|
var id int
|
||||||
|
err = eventRows.Scan(
|
||||||
|
&id,
|
||||||
|
&eventJSON,
|
||||||
|
&txnID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmarshal eventJSON
|
||||||
|
if err = json.Unmarshal(eventJSON, &event); err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If txnID has changed on this event from the previous event, then we've
|
||||||
|
// reached the end of a transaction's events. Return only those events.
|
||||||
|
if lastTxnID > invalidTxnID && lastTxnID != txnID {
|
||||||
|
return events, maxID, lastTxnID, true, nil
|
||||||
|
}
|
||||||
|
lastTxnID = txnID
|
||||||
|
|
||||||
|
// Limit events that aren't part of an old transaction
|
||||||
|
if txnID == -1 {
|
||||||
|
// Return if we've hit the limit
|
||||||
|
if eventsProcessed++; eventsProcessed > limit {
|
||||||
|
return events, maxID, lastTxnID, true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if id > maxID {
|
||||||
|
maxID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
// Portion of the event that is unsigned due to rapid change
|
||||||
|
// TODO: Consider removing age as not many app services use it
|
||||||
|
if err = event.SetUnsignedField("age", nowMilli-int64(event.OriginServerTS())); err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
events = append(events, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// countEventsByApplicationServiceID inserts an event mapped to its corresponding application service
|
||||||
|
// IDs into the db.
|
||||||
|
func (s *eventsStatements) countEventsByApplicationServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := s.countEventsByApplicationServiceIDStmt.QueryRowContext(ctx, appServiceID).Scan(&count)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// insertEvent inserts an event mapped to its corresponding application service
|
||||||
|
// IDs into the db.
|
||||||
|
func (s *eventsStatements) insertEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
event *gomatrixserverlib.HeaderedEvent,
|
||||||
|
) (err error) {
|
||||||
|
// Convert event to JSON before inserting
|
||||||
|
eventJSON, err := json.Marshal(event)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
||||||
|
_, err := s.insertEventStmt.ExecContext(
|
||||||
|
ctx,
|
||||||
|
appServiceID,
|
||||||
|
eventJSON,
|
||||||
|
-1, // No transaction ID yet
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateTxnIDForEvents sets the transactionID for a collection of events. Done
|
||||||
|
// before sending them to an AppService. Referenced before sending to make sure
|
||||||
|
// we aren't constructing multiple transactions with the same events.
|
||||||
|
func (s *eventsStatements) updateTxnIDForEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
maxID, txnID int,
|
||||||
|
) (err error) {
|
||||||
|
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
||||||
|
_, err := s.updateTxnIDForEventsStmt.ExecContext(ctx, txnID, appserviceID, maxID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteEventsBeforeAndIncludingID removes events matching given IDs from the database.
|
||||||
|
func (s *eventsStatements) deleteEventsBeforeAndIncludingID(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
eventTableID int,
|
||||||
|
) (err error) {
|
||||||
|
return s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
||||||
|
_, err := s.deleteEventsBeforeAndIncludingIDStmt.ExecContext(ctx, appserviceID, eventTableID)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
118
appservice/storage/sqlite3/storage.go
Normal file
118
appservice/storage/sqlite3/storage.go
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
// Import SQLite database driver
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Database stores events intended to be later sent to application services
|
||||||
|
type Database struct {
|
||||||
|
sqlutil.PartitionOffsetStatements
|
||||||
|
events eventsStatements
|
||||||
|
txnID txnStatements
|
||||||
|
db *sql.DB
|
||||||
|
writer sqlutil.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDatabase opens a new database
|
||||||
|
func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) {
|
||||||
|
var result Database
|
||||||
|
var err error
|
||||||
|
if result.db, err = sqlutil.Open(dbProperties); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result.writer = sqlutil.NewExclusiveWriter()
|
||||||
|
if err = result.prepare(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = result.PartitionOffsetStatements.Prepare(result.db, result.writer, "appservice"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) prepare() error {
|
||||||
|
if err := d.events.prepare(d.db, d.writer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.txnID.prepare(d.db, d.writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StoreEvent takes in a gomatrixserverlib.HeaderedEvent and stores it in the database
|
||||||
|
// for a transaction worker to pull and later send to an application service.
|
||||||
|
func (d *Database) StoreEvent(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
event *gomatrixserverlib.HeaderedEvent,
|
||||||
|
) error {
|
||||||
|
return d.events.insertEvent(ctx, appServiceID, event)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetEventsWithAppServiceID returns a slice of events and their IDs intended to
|
||||||
|
// be sent to an application service given its ID.
|
||||||
|
func (d *Database) GetEventsWithAppServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
limit int,
|
||||||
|
) (int, int, []gomatrixserverlib.HeaderedEvent, bool, error) {
|
||||||
|
return d.events.selectEventsByApplicationServiceID(ctx, appServiceID, limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountEventsWithAppServiceID returns the number of events destined for an
|
||||||
|
// application service given its ID.
|
||||||
|
func (d *Database) CountEventsWithAppServiceID(
|
||||||
|
ctx context.Context,
|
||||||
|
appServiceID string,
|
||||||
|
) (int, error) {
|
||||||
|
return d.events.countEventsByApplicationServiceID(ctx, appServiceID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateTxnIDForEvents takes in an application service ID and a
|
||||||
|
// and stores them in the DB, unless the pair already exists, in
|
||||||
|
// which case it updates them.
|
||||||
|
func (d *Database) UpdateTxnIDForEvents(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
maxID, txnID int,
|
||||||
|
) error {
|
||||||
|
return d.events.updateTxnIDForEvents(ctx, appserviceID, maxID, txnID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEventsBeforeAndIncludingID removes all events from the database that
|
||||||
|
// are less than or equal to a given maximum ID. IDs here are implemented as a
|
||||||
|
// serial, thus this should always delete events in chronological order.
|
||||||
|
func (d *Database) RemoveEventsBeforeAndIncludingID(
|
||||||
|
ctx context.Context,
|
||||||
|
appserviceID string,
|
||||||
|
eventTableID int,
|
||||||
|
) error {
|
||||||
|
return d.events.deleteEventsBeforeAndIncludingID(ctx, appserviceID, eventTableID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLatestTxnID returns the latest available transaction id
|
||||||
|
func (d *Database) GetLatestTxnID(
|
||||||
|
ctx context.Context,
|
||||||
|
) (int, error) {
|
||||||
|
return d.txnID.selectTxnID(ctx)
|
||||||
|
}
|
||||||
82
appservice/storage/sqlite3/txn_id_counter_table.go
Normal file
82
appservice/storage/sqlite3/txn_id_counter_table.go
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
// Copyright 2018 New Vector Ltd
|
||||||
|
// Copyright 2019-2020 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 sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
const txnIDSchema = `
|
||||||
|
-- Keeps a count of the current transaction ID
|
||||||
|
CREATE TABLE IF NOT EXISTS appservice_counters (
|
||||||
|
name TEXT PRIMARY KEY NOT NULL,
|
||||||
|
last_id INTEGER DEFAULT 1
|
||||||
|
);
|
||||||
|
INSERT OR IGNORE INTO appservice_counters (name, last_id) VALUES('txn_id', 1);
|
||||||
|
`
|
||||||
|
|
||||||
|
const selectTxnIDSQL = `
|
||||||
|
SELECT last_id FROM appservice_counters WHERE name='txn_id'
|
||||||
|
`
|
||||||
|
|
||||||
|
const updateTxnIDSQL = `
|
||||||
|
UPDATE appservice_counters SET last_id=last_id+1 WHERE name='txn_id'
|
||||||
|
`
|
||||||
|
|
||||||
|
type txnStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
writer sqlutil.Writer
|
||||||
|
selectTxnIDStmt *sql.Stmt
|
||||||
|
updateTxnIDStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *txnStatements) prepare(db *sql.DB, writer sqlutil.Writer) (err error) {
|
||||||
|
s.db = db
|
||||||
|
s.writer = writer
|
||||||
|
_, err = db.Exec(txnIDSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.selectTxnIDStmt, err = db.Prepare(selectTxnIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.updateTxnIDStmt, err = db.Prepare(updateTxnIDSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectTxnID selects the latest ascending transaction ID
|
||||||
|
func (s *txnStatements) selectTxnID(
|
||||||
|
ctx context.Context,
|
||||||
|
) (txnID int, err error) {
|
||||||
|
err = s.writer.Do(s.db, nil, func(txn *sql.Tx) error {
|
||||||
|
err := s.selectTxnIDStmt.QueryRowContext(ctx).Scan(&txnID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = s.updateTxnIDStmt.ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
38
appservice/storage/storage.go
Normal file
38
appservice/storage/storage.go
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
// Copyright 2020 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.
|
||||||
|
|
||||||
|
// +build !wasm
|
||||||
|
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage/postgres"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage/sqlite3"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme)
|
||||||
|
// and sets DB connection parameters
|
||||||
|
func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) {
|
||||||
|
switch {
|
||||||
|
case dbProperties.ConnectionString.IsSQLite():
|
||||||
|
return sqlite3.NewDatabase(dbProperties)
|
||||||
|
case dbProperties.ConnectionString.IsPostgres():
|
||||||
|
return postgres.NewDatabase(dbProperties)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected database type")
|
||||||
|
}
|
||||||
|
}
|
||||||
33
appservice/storage/storage_wasm.go
Normal file
33
appservice/storage/storage_wasm.go
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright 2020 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 storage
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage/sqlite3"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) {
|
||||||
|
switch {
|
||||||
|
case dbProperties.ConnectionString.IsSQLite():
|
||||||
|
return sqlite3.NewDatabase(dbProperties)
|
||||||
|
case dbProperties.ConnectionString.IsPostgres():
|
||||||
|
return nil, fmt.Errorf("can't use Postgres implementation")
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unexpected database type")
|
||||||
|
}
|
||||||
|
}
|
||||||
64
appservice/types/types.go
Normal file
64
appservice/types/types.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
// 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 types
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// AppServiceDeviceID is the AS dummy device ID
|
||||||
|
AppServiceDeviceID = "AS_Device"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ApplicationServiceWorkerState is a type that couples an application service,
|
||||||
|
// a lockable condition as well as some other state variables, allowing the
|
||||||
|
// roomserver to notify appservice workers when there are events ready to send
|
||||||
|
// externally to application services.
|
||||||
|
type ApplicationServiceWorkerState struct {
|
||||||
|
AppService config.ApplicationService
|
||||||
|
Cond *sync.Cond
|
||||||
|
// Events ready to be sent
|
||||||
|
EventsReady bool
|
||||||
|
// Backoff exponent (2^x secs). Max 6, aka 64s.
|
||||||
|
Backoff int
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotifyNewEvents wakes up all waiting goroutines, notifying that events remain
|
||||||
|
// in the event queue for this application service worker.
|
||||||
|
func (a *ApplicationServiceWorkerState) NotifyNewEvents() {
|
||||||
|
a.Cond.L.Lock()
|
||||||
|
a.EventsReady = true
|
||||||
|
a.Cond.Broadcast()
|
||||||
|
a.Cond.L.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FinishEventProcessing marks all events of this worker as being sent to the
|
||||||
|
// application service.
|
||||||
|
func (a *ApplicationServiceWorkerState) FinishEventProcessing() {
|
||||||
|
a.Cond.L.Lock()
|
||||||
|
a.EventsReady = false
|
||||||
|
a.Cond.L.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForNewEvents causes the calling goroutine to wait on the worker state's
|
||||||
|
// condition for a broadcast or similar wakeup, if there are no events ready.
|
||||||
|
func (a *ApplicationServiceWorkerState) WaitForNewEvents() {
|
||||||
|
a.Cond.L.Lock()
|
||||||
|
if !a.EventsReady {
|
||||||
|
a.Cond.Wait()
|
||||||
|
}
|
||||||
|
a.Cond.L.Unlock()
|
||||||
|
}
|
||||||
236
appservice/workers/transaction_scheduler.go
Normal file
236
appservice/workers/transaction_scheduler.go
Normal file
|
|
@ -0,0 +1,236 @@
|
||||||
|
// Copyright 2018 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 workers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/appservice/storage"
|
||||||
|
"github.com/matrix-org/dendrite/appservice/types"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Maximum size of events sent in each transaction.
|
||||||
|
transactionBatchSize = 50
|
||||||
|
)
|
||||||
|
|
||||||
|
// SetupTransactionWorkers spawns a separate goroutine for each application
|
||||||
|
// service. Each of these "workers" handle taking all events intended for their
|
||||||
|
// app service, batch them up into a single transaction (up to a max transaction
|
||||||
|
// size), then send that off to the AS's /transactions/{txnID} endpoint. It also
|
||||||
|
// handles exponentially backing off in case the AS isn't currently available.
|
||||||
|
func SetupTransactionWorkers(
|
||||||
|
client *http.Client,
|
||||||
|
appserviceDB storage.Database,
|
||||||
|
workerStates []types.ApplicationServiceWorkerState,
|
||||||
|
) error {
|
||||||
|
// Create a worker that handles transmitting events to a single homeserver
|
||||||
|
for _, workerState := range workerStates {
|
||||||
|
// Don't create a worker if this AS doesn't want to receive events
|
||||||
|
if workerState.AppService.URL != "" {
|
||||||
|
go worker(client, appserviceDB, workerState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// worker is a goroutine that sends any queued events to the application service
|
||||||
|
// it is given.
|
||||||
|
func worker(client *http.Client, db storage.Database, ws types.ApplicationServiceWorkerState) {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).Info("Starting application service")
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
// Initial check for any leftover events to send from last time
|
||||||
|
eventCount, err := db.CountEventsWithAppServiceID(ctx, ws.AppService.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).WithError(err).Fatal("appservice worker unable to read queued events from DB")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if eventCount > 0 {
|
||||||
|
ws.NotifyNewEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop forever and keep waiting for more events to send
|
||||||
|
for {
|
||||||
|
// Wait for more events if we've sent all the events in the database
|
||||||
|
ws.WaitForNewEvents()
|
||||||
|
|
||||||
|
// Batch events up into a transaction
|
||||||
|
transactionJSON, txnID, maxEventID, eventsRemaining, err := createTransaction(ctx, db, ws.AppService.ID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).WithError(err).Fatal("appservice worker unable to create transaction")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the events off to the application service
|
||||||
|
// Backoff if the application service does not respond
|
||||||
|
err = send(client, ws.AppService, txnID, transactionJSON)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).WithError(err).Error("unable to send event")
|
||||||
|
// Backoff
|
||||||
|
backoff(&ws, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// We sent successfully, hooray!
|
||||||
|
ws.Backoff = 0
|
||||||
|
|
||||||
|
// Transactions have a maximum event size, so there may still be some events
|
||||||
|
// left over to send. Keep sending until none are left
|
||||||
|
if !eventsRemaining {
|
||||||
|
ws.FinishEventProcessing()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove sent events from the DB
|
||||||
|
err = db.RemoveEventsBeforeAndIncludingID(ctx, ws.AppService.ID, maxEventID)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).WithError(err).Fatal("unable to remove appservice events from the database")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// backoff pauses the calling goroutine for a 2^some backoff exponent seconds
|
||||||
|
func backoff(ws *types.ApplicationServiceWorkerState, err error) {
|
||||||
|
// Calculate how long to backoff for
|
||||||
|
backoffDuration := time.Duration(math.Pow(2, float64(ws.Backoff)))
|
||||||
|
backoffSeconds := time.Second * backoffDuration
|
||||||
|
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": ws.AppService.ID,
|
||||||
|
}).WithError(err).Warnf("unable to send transactions successfully, backing off for %ds",
|
||||||
|
backoffDuration)
|
||||||
|
|
||||||
|
ws.Backoff++
|
||||||
|
if ws.Backoff > 6 {
|
||||||
|
ws.Backoff = 6
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backoff
|
||||||
|
time.Sleep(backoffSeconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createTransaction takes in a slice of AS events, stores them in an AS
|
||||||
|
// transaction, and JSON-encodes the results.
|
||||||
|
func createTransaction(
|
||||||
|
ctx context.Context,
|
||||||
|
db storage.Database,
|
||||||
|
appserviceID string,
|
||||||
|
) (
|
||||||
|
transactionJSON []byte,
|
||||||
|
txnID, maxID int,
|
||||||
|
eventsRemaining bool,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
// Retrieve the latest events from the DB (will return old events if they weren't successfully sent)
|
||||||
|
txnID, maxID, events, eventsRemaining, err := db.GetEventsWithAppServiceID(ctx, appserviceID, transactionBatchSize)
|
||||||
|
if err != nil {
|
||||||
|
log.WithFields(log.Fields{
|
||||||
|
"appservice": appserviceID,
|
||||||
|
}).WithError(err).Fatalf("appservice worker unable to read queued events from DB")
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if these events do not already have a transaction ID
|
||||||
|
if txnID == -1 {
|
||||||
|
// If not, grab next available ID from the DB
|
||||||
|
txnID, err = db.GetLatestTxnID(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark new events with current transactionID
|
||||||
|
if err = db.UpdateTxnIDForEvents(ctx, appserviceID, maxID, txnID); err != nil {
|
||||||
|
return nil, 0, 0, false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ev []*gomatrixserverlib.HeaderedEvent
|
||||||
|
for i := range events {
|
||||||
|
ev = append(ev, &events[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a transaction and store the events inside
|
||||||
|
transaction := gomatrixserverlib.ApplicationServiceTransaction{
|
||||||
|
Events: gomatrixserverlib.HeaderedToClientEvents(ev, gomatrixserverlib.FormatAll),
|
||||||
|
}
|
||||||
|
|
||||||
|
transactionJSON, err = json.Marshal(transaction)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// send sends events to an application service. Returns an error if an OK was not
|
||||||
|
// received back from the application service or the request timed out.
|
||||||
|
func send(
|
||||||
|
client *http.Client,
|
||||||
|
appservice config.ApplicationService,
|
||||||
|
txnID int,
|
||||||
|
transaction []byte,
|
||||||
|
) (err error) {
|
||||||
|
// PUT a transaction to our AS
|
||||||
|
// https://matrix.org/docs/spec/application_service/r0.1.2#put-matrix-app-v1-transactions-txnid
|
||||||
|
address := fmt.Sprintf("%s/transactions/%d?access_token=%s", appservice.URL, txnID, url.QueryEscape(appservice.HSToken))
|
||||||
|
req, err := http.NewRequest("PUT", address, bytes.NewBuffer(transaction))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer checkNamedErr(resp.Body.Close, &err)
|
||||||
|
|
||||||
|
// Check the AS received the events correctly
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
// TODO: Handle non-200 error codes from application services
|
||||||
|
return fmt.Errorf("non-OK status code %d returned from AS", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNamedErr calls fn and overwrite err if it was nil and fn returned non-nil
|
||||||
|
func checkNamedErr(fn func() error, err *error) {
|
||||||
|
if e := fn(); e != nil && *err == nil {
|
||||||
|
*err = e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -106,13 +106,10 @@ rst Users cannot set notifications powerlevel higher than their own (2 subtests)
|
||||||
rst Both GET and PUT work
|
rst Both GET and PUT work
|
||||||
rct POST /rooms/:room_id/receipt can create receipts
|
rct POST /rooms/:room_id/receipt can create receipts
|
||||||
red POST /rooms/:room_id/read_markers can create read marker
|
red POST /rooms/:room_id/read_markers can create read marker
|
||||||
med POST /media/v3/upload can create an upload
|
|
||||||
med POST /media/r0/upload can create an upload
|
med POST /media/r0/upload can create an upload
|
||||||
med GET /media/v3/download can fetch the value again
|
|
||||||
med GET /media/r0/download can fetch the value again
|
med GET /media/r0/download can fetch the value again
|
||||||
cap GET /capabilities is present and well formed for registered user
|
cap GET /capabilities is present and well formed for registered user
|
||||||
cap GET /r0/capabilities is not public
|
cap GET /r0/capabilities is not public
|
||||||
cap GET /v3/capabilities is not public
|
|
||||||
reg Register with a recaptcha
|
reg Register with a recaptcha
|
||||||
reg registration is idempotent, without username specified
|
reg registration is idempotent, without username specified
|
||||||
reg registration is idempotent, with username specified
|
reg registration is idempotent, with username specified
|
||||||
|
|
@ -155,13 +152,13 @@ jon Joining room twice is idempotent
|
||||||
syn New room members see their own join event
|
syn New room members see their own join event
|
||||||
v1s New room members see existing users' presence in room initialSync
|
v1s New room members see existing users' presence in room initialSync
|
||||||
syn Existing members see new members' join events
|
syn Existing members see new members' join events
|
||||||
pre Existing members see new members' presence
|
syn Existing members see new members' presence
|
||||||
v1s All room members see all room members' presence in global initialSync
|
v1s All room members see all room members' presence in global initialSync
|
||||||
f,jon Remote users can join room by alias
|
f,jon Remote users can join room by alias
|
||||||
syn New room members see their own join event
|
syn New room members see their own join event
|
||||||
v1s New room members see existing members' presence in room initialSync
|
v1s New room members see existing members' presence in room initialSync
|
||||||
syn Existing members see new members' join events
|
syn Existing members see new members' join events
|
||||||
pre Existing members see new member's presence
|
syn Existing members see new member's presence
|
||||||
v1s New room members see first user's profile information in global initialSync
|
v1s New room members see first user's profile information in global initialSync
|
||||||
v1s New room members see first user's profile information in per-room initialSync
|
v1s New room members see first user's profile information in per-room initialSync
|
||||||
f,jon Remote users may not join unfederated rooms
|
f,jon Remote users may not join unfederated rooms
|
||||||
|
|
@ -177,9 +174,7 @@ eph Ephemeral messages received from clients are correctly expired
|
||||||
ali Room aliases can contain Unicode
|
ali Room aliases can contain Unicode
|
||||||
f,ali Remote room alias queries can handle Unicode
|
f,ali Remote room alias queries can handle Unicode
|
||||||
ali Canonical alias can be set
|
ali Canonical alias can be set
|
||||||
ali Canonical alias can be set (3 subtests)
|
|
||||||
ali Canonical alias can include alt_aliases
|
ali Canonical alias can include alt_aliases
|
||||||
ali Canonical alias can include alt_aliases (4 subtests)
|
|
||||||
ali Regular users can add and delete aliases in the default room configuration
|
ali Regular users can add and delete aliases in the default room configuration
|
||||||
ali Regular users can add and delete aliases when m.room.aliases is restricted
|
ali Regular users can add and delete aliases when m.room.aliases is restricted
|
||||||
ali Deleting a non-existent alias should return a 404
|
ali Deleting a non-existent alias should return a 404
|
||||||
|
|
@ -212,12 +207,11 @@ plv Users cannot set kick powerlevel higher than their own (2 subtests)
|
||||||
plv Users cannot set redact powerlevel higher than their own (2 subtests)
|
plv Users cannot set redact powerlevel higher than their own (2 subtests)
|
||||||
v1s Check that event streams started after a client joined a room work (SYT-1)
|
v1s Check that event streams started after a client joined a room work (SYT-1)
|
||||||
v1s Event stream catches up fully after many messages
|
v1s Event stream catches up fully after many messages
|
||||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as power user redacts message
|
xxx POST /rooms/:room_id/redact/:event_id as power user redacts message
|
||||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as original message sender redacts message
|
xxx POST /rooms/:room_id/redact/:event_id as original message sender redacts message
|
||||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as random user does not redact message
|
xxx POST /rooms/:room_id/redact/:event_id as random user does not redact message
|
||||||
xxx PUT /redact disallows redaction of event in different room
|
xxx POST /redact disallows redaction of event in different room
|
||||||
xxx Redaction of a redaction redacts the redaction reason
|
xxx Redaction of a redaction redacts the redaction reason
|
||||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id is idempotent
|
|
||||||
v1s A departed room is still included in /initialSync (SPEC-216)
|
v1s A departed room is still included in /initialSync (SPEC-216)
|
||||||
v1s Can get rooms/{roomId}/initialSync for a departed room (SPEC-216)
|
v1s Can get rooms/{roomId}/initialSync for a departed room (SPEC-216)
|
||||||
rst Can get rooms/{roomId}/state for a departed room (SPEC-216)
|
rst Can get rooms/{roomId}/state for a departed room (SPEC-216)
|
||||||
|
|
@ -357,8 +351,8 @@ syn Can sync a joined room
|
||||||
syn Full state sync includes joined rooms
|
syn Full state sync includes joined rooms
|
||||||
syn Newly joined room is included in an incremental sync
|
syn Newly joined room is included in an incremental sync
|
||||||
syn Newly joined room has correct timeline in incremental sync
|
syn Newly joined room has correct timeline in incremental sync
|
||||||
pre Newly joined room includes presence in incremental sync
|
syn Newly joined room includes presence in incremental sync
|
||||||
pre Get presence for newly joined members in incremental sync
|
syn Get presence for newly joined members in incremental sync
|
||||||
syn Can sync a room with a single message
|
syn Can sync a room with a single message
|
||||||
syn Can sync a room with a message with a transaction id
|
syn Can sync a room with a message with a transaction id
|
||||||
syn A message sent after an initial sync appears in the timeline of an incremental sync.
|
syn A message sent after an initial sync appears in the timeline of an incremental sync.
|
||||||
|
|
@ -368,9 +362,9 @@ syn A full_state incremental update returns only recent timeline
|
||||||
syn A prev_batch token can be used in the v1 messages API
|
syn A prev_batch token can be used in the v1 messages API
|
||||||
syn A next_batch token can be used in the v1 messages API
|
syn A next_batch token can be used in the v1 messages API
|
||||||
syn A prev_batch token from incremental sync can be used in the v1 messages API
|
syn A prev_batch token from incremental sync can be used in the v1 messages API
|
||||||
pre User sees their own presence in a sync
|
syn User sees their own presence in a sync
|
||||||
pre User is offline if they set_presence=offline in their sync
|
syn User is offline if they set_presence=offline in their sync
|
||||||
pre User sees updates to presence from other users in the incremental sync.
|
syn User sees updates to presence from other users in the incremental sync.
|
||||||
syn State is included in the timeline in the initial sync
|
syn State is included in the timeline in the initial sync
|
||||||
f,syn State from remote users is included in the state in the initial sync
|
f,syn State from remote users is included in the state in the initial sync
|
||||||
syn Changes to state are included in an incremental sync
|
syn Changes to state are included in an incremental sync
|
||||||
|
|
@ -484,30 +478,6 @@ rmv Inbound federation rejects invites which include invalid JSON for room versi
|
||||||
rmv Outbound federation rejects invite response which include invalid JSON for room version 6
|
rmv Outbound federation rejects invite response which include invalid JSON for room version 6
|
||||||
rmv Inbound federation rejects invite rejections which include invalid JSON for room version 6
|
rmv Inbound federation rejects invite rejections which include invalid JSON for room version 6
|
||||||
rmv Server rejects invalid JSON in a version 6 room
|
rmv Server rejects invalid JSON in a version 6 room
|
||||||
rmv User can create and send/receive messages in a room with version 7 (2 subtests)
|
|
||||||
rmv local user can join room with version 7
|
|
||||||
rmv User can invite local user to room with version 7
|
|
||||||
rmv remote user can join room with version 7
|
|
||||||
rmv User can invite remote user to room with version 7
|
|
||||||
rmv Remote user can backfill in a room with version 7
|
|
||||||
rmv Can reject invites over federation for rooms with version 7
|
|
||||||
rmv Can receive redactions from regular users over federation in room version 7
|
|
||||||
rmv User can create and send/receive messages in a room with version 8 (2 subtests)
|
|
||||||
rmv local user can join room with version 8
|
|
||||||
rmv User can invite local user to room with version 8
|
|
||||||
rmv remote user can join room with version 8
|
|
||||||
rmv User can invite remote user to room with version 8
|
|
||||||
rmv Remote user can backfill in a room with version 8
|
|
||||||
rmv Can reject invites over federation for rooms with version 8
|
|
||||||
rmv Can receive redactions from regular users over federation in room version 8
|
|
||||||
rmv User can create and send/receive messages in a room with version 9 (2 subtests)
|
|
||||||
rmv local user can join room with version 9
|
|
||||||
rmv User can invite local user to room with version 9
|
|
||||||
rmv remote user can join room with version 9
|
|
||||||
rmv User can invite remote user to room with version 9
|
|
||||||
rmv Remote user can backfill in a room with version 9
|
|
||||||
rmv Can reject invites over federation for rooms with version 9
|
|
||||||
rmv Can receive redactions from regular users over federation in room version 9
|
|
||||||
pre Presence changes are reported to local room members
|
pre Presence changes are reported to local room members
|
||||||
f,pre Presence changes are also reported to remote room members
|
f,pre Presence changes are also reported to remote room members
|
||||||
pre Presence changes to UNAVAILABLE are reported to local room members
|
pre Presence changes to UNAVAILABLE are reported to local room members
|
||||||
|
|
@ -643,7 +613,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
|
||||||
|
|
@ -802,15 +772,12 @@ app AS can make room aliases
|
||||||
app Regular users cannot create room aliases within the AS namespace
|
app Regular users cannot create room aliases within the AS namespace
|
||||||
app AS-ghosted users can use rooms via AS
|
app AS-ghosted users can use rooms via AS
|
||||||
app AS-ghosted users can use rooms themselves
|
app AS-ghosted users can use rooms themselves
|
||||||
app AS-ghosted users can use rooms via AS (2 subtests)
|
|
||||||
app AS-ghosted users can use rooms themselves (3 subtests)
|
|
||||||
app Ghost user must register before joining room
|
app Ghost user must register before joining room
|
||||||
app AS can set avatar for ghosted users
|
app AS can set avatar for ghosted users
|
||||||
app AS can set displayname for ghosted users
|
app AS can set displayname for ghosted users
|
||||||
app AS can't set displayname for random users
|
app AS can't set displayname for random users
|
||||||
app Inviting an AS-hosted user asks the AS server
|
app Inviting an AS-hosted user asks the AS server
|
||||||
app Accesing an AS-hosted room alias asks the AS server
|
app Accesing an AS-hosted room alias asks the AS server
|
||||||
app Accesing an AS-hosted room alias asks the AS server (2 subtests)
|
|
||||||
app Events in rooms with AS-hosted room aliases are sent to AS server
|
app Events in rooms with AS-hosted room aliases are sent to AS server
|
||||||
app AS user (not ghost) can join room without registering
|
app AS user (not ghost) can join room without registering
|
||||||
app AS user (not ghost) can join room without registering, with user_id query param
|
app AS user (not ghost) can join room without registering, with user_id query param
|
||||||
|
|
@ -822,8 +789,6 @@ app AS can publish rooms in their own list
|
||||||
app AS and main public room lists are separate
|
app AS and main public room lists are separate
|
||||||
app AS can deactivate a user
|
app AS can deactivate a user
|
||||||
psh Test that a message is pushed
|
psh Test that a message is pushed
|
||||||
psh Test that a message is pushed (6 subtests)
|
|
||||||
psh Test that rejected pushers are removed. (4 subtests)
|
|
||||||
psh Invites are pushed
|
psh Invites are pushed
|
||||||
psh Rooms with names are correctly named in pushed
|
psh Rooms with names are correctly named in pushed
|
||||||
psh Rooms with canonical alias are correctly named in pushed
|
psh Rooms with canonical alias are correctly named in pushed
|
||||||
|
|
@ -892,12 +857,9 @@ pre Left room members do not cause problems for presence
|
||||||
crm Rooms can be created with an initial invite list (SYN-205) (1 subtests)
|
crm Rooms can be created with an initial invite list (SYN-205) (1 subtests)
|
||||||
typ Typing notifications don't leak
|
typ Typing notifications don't leak
|
||||||
ban Non-present room members cannot ban others
|
ban Non-present room members cannot ban others
|
||||||
ban Non-present room members cannot ban others (3 subtests)
|
|
||||||
psh Getting push rules doesn't corrupt the cache SYN-390
|
psh Getting push rules doesn't corrupt the cache SYN-390
|
||||||
psh Getting push rules doesn't corrupt the cache SYN-390 (3 subtests)
|
|
||||||
inv Test that we can be reinvited to a room we created
|
inv Test that we can be reinvited to a room we created
|
||||||
syn Multiple calls to /sync should not cause 500 errors
|
syn Multiple calls to /sync should not cause 500 errors
|
||||||
syn Multiple calls to /sync should not cause 500 errors (6 subtests)
|
|
||||||
gst Guest user can call /events on another world_readable room (SYN-606)
|
gst Guest user can call /events on another world_readable room (SYN-606)
|
||||||
gst Real user can call /events on another world_readable room (SYN-606)
|
gst Real user can call /events on another world_readable room (SYN-606)
|
||||||
gst Events come down the correct room
|
gst Events come down the correct room
|
||||||
|
|
@ -922,34 +884,3 @@ msc We can't peek into rooms with invited history_visibility
|
||||||
msc We can't peek into rooms with joined history_visibility
|
msc We can't peek into rooms with joined history_visibility
|
||||||
msc Local users can peek by room alias
|
msc Local users can peek by room alias
|
||||||
msc Peeked rooms only turn up in the sync for the device who peeked them
|
msc Peeked rooms only turn up in the sync for the device who peeked them
|
||||||
ban 'ban' event respects room powerlevel (2 subtests)
|
|
||||||
inv Test that we can be reinvited to a room we created (11 subtests)
|
|
||||||
fiv Rejecting invite over federation doesn't break incremental /sync
|
|
||||||
pre Presence can be set from sync
|
|
||||||
fst /state returns M_NOT_FOUND for an outlier
|
|
||||||
fst /state_ids returns M_NOT_FOUND for an outlier
|
|
||||||
fst /state returns M_NOT_FOUND for a rejected message event
|
|
||||||
fst /state_ids returns M_NOT_FOUND for a rejected message event
|
|
||||||
fst /state returns M_NOT_FOUND for a rejected state event
|
|
||||||
fst /state_ids returns M_NOT_FOUND for a rejected state event
|
|
||||||
fst Room state after a rejected message event is the same as before
|
|
||||||
fst Room state after a rejected state event is the same as before
|
|
||||||
fpb Federation publicRoom Name/topic keys are correct
|
|
||||||
fed New federated private chats get full presence information (SYN-115) (10 subtests)
|
|
||||||
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
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
from __future__ import division
|
from __future__ import division
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
import re
|
||||||
import os
|
import sys
|
||||||
|
|
||||||
# Usage: $ ./are-we-synapse-yet.py [-v] results.tap
|
# Usage: $ ./are-we-synapse-yet.py [-v] results.tap
|
||||||
# This script scans a results.tap file from Dendrite's CI process and spits out
|
# This script scans a results.tap file from Dendrite's CI process and spits out
|
||||||
|
|
@ -156,7 +156,6 @@ def parse_test_line(line):
|
||||||
# ✓ POST /register downcases capitals in usernames
|
# ✓ POST /register downcases capitals in usernames
|
||||||
# ...
|
# ...
|
||||||
def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
||||||
ci = os.getenv("CI") # When running from GHA, this groups the subsections
|
|
||||||
subsections = [] # Registration: 100% (13/13 tests)
|
subsections = [] # Registration: 100% (13/13 tests)
|
||||||
subsection_test_names = {} # 'subsection name': ["✓ Test 1", "✓ Test 2", "× Test 3"]
|
subsection_test_names = {} # 'subsection name': ["✓ Test 1", "✓ Test 2", "× Test 3"]
|
||||||
total_passing = 0
|
total_passing = 0
|
||||||
|
|
@ -170,7 +169,7 @@ def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
||||||
for name, passing in tests.items():
|
for name, passing in tests.items():
|
||||||
if passing:
|
if passing:
|
||||||
group_passing += 1
|
group_passing += 1
|
||||||
test_names_and_marks.append(f"{'✅' if passing else '❌'} {name}")
|
test_names_and_marks.append(f"{'✓' if passing else '×'} {name}")
|
||||||
|
|
||||||
total_tests += group_total
|
total_tests += group_total
|
||||||
total_passing += group_passing
|
total_passing += group_passing
|
||||||
|
|
@ -179,19 +178,15 @@ def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
||||||
subsections.append(line)
|
subsections.append(line)
|
||||||
subsection_test_names[line] = test_names_and_marks
|
subsection_test_names[line] = test_names_and_marks
|
||||||
|
|
||||||
# avoid errors when trying to divide by 0
|
|
||||||
if total_tests == 0:
|
|
||||||
return
|
|
||||||
|
|
||||||
pct = "{0:.0f}%".format(total_passing/total_tests * 100)
|
pct = "{0:.0f}%".format(total_passing/total_tests * 100)
|
||||||
print("%s: %s (%d/%d tests)" % (header_name, pct, total_passing, total_tests))
|
print("%s: %s (%d/%d tests)" % (header_name, pct, total_passing, total_tests))
|
||||||
print("-" * (len(header_name)+1))
|
print("-" * (len(header_name)+1))
|
||||||
for line in subsections:
|
for line in subsections:
|
||||||
print("%s%s" % ("::group::" if ci and verbose else "", line,))
|
print(" %s" % (line,))
|
||||||
if verbose:
|
if verbose:
|
||||||
for test_name_and_pass_mark in subsection_test_names[line]:
|
for test_name_and_pass_mark in subsection_test_names[line]:
|
||||||
print(" %s" % (test_name_and_pass_mark,))
|
print(" %s" % (test_name_and_pass_mark,))
|
||||||
print("%s" % ("::endgroup::" if ci else ""))
|
print("")
|
||||||
print("")
|
print("")
|
||||||
|
|
||||||
def main(results_tap_path, verbose):
|
def main(results_tap_path, verbose):
|
||||||
|
|
|
||||||
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" = master ]
|
||||||
|
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
|
||||||
|
|
@ -9,9 +9,9 @@ FROM golang:1.14-alpine AS gobuild
|
||||||
|
|
||||||
# Download and build dendrite
|
# Download and build dendrite
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD https://github.com/matrix-org/dendrite/archive/main.tar.gz /build/main.tar.gz
|
ADD https://github.com/matrix-org/dendrite/archive/master.tar.gz /build/master.tar.gz
|
||||||
RUN tar xvfz main.tar.gz
|
RUN tar xvfz master.tar.gz
|
||||||
WORKDIR /build/dendrite-main
|
WORKDIR /build/dendrite-master
|
||||||
RUN GOOS=js GOARCH=wasm go build -o main.wasm ./cmd/dendritejs
|
RUN GOOS=js GOARCH=wasm go build -o main.wasm ./cmd/dendritejs
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -21,7 +21,7 @@ RUN apt-get update && apt-get -y install python
|
||||||
|
|
||||||
# Download riot-web and libp2p repos
|
# Download riot-web and libp2p repos
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
ADD https://github.com/matrix-org/go-http-js-libp2p/archive/main.tar.gz /build/libp2p.tar.gz
|
ADD https://github.com/matrix-org/go-http-js-libp2p/archive/master.tar.gz /build/libp2p.tar.gz
|
||||||
RUN tar xvfz libp2p.tar.gz
|
RUN tar xvfz libp2p.tar.gz
|
||||||
ADD https://github.com/vector-im/element-web/archive/matthew/p2p.tar.gz /build/p2p.tar.gz
|
ADD https://github.com/vector-im/element-web/archive/matthew/p2p.tar.gz /build/p2p.tar.gz
|
||||||
RUN tar xvfz p2p.tar.gz
|
RUN tar xvfz p2p.tar.gz
|
||||||
|
|
@ -31,13 +31,13 @@ WORKDIR /build/element-web-matthew-p2p
|
||||||
RUN yarn install
|
RUN yarn install
|
||||||
RUN ln -s /build/go-http-js-libp2p-master /build/element-web-matthew-p2p/node_modules/go-http-js-libp2p
|
RUN ln -s /build/go-http-js-libp2p-master /build/element-web-matthew-p2p/node_modules/go-http-js-libp2p
|
||||||
RUN (cd node_modules/go-http-js-libp2p && yarn install)
|
RUN (cd node_modules/go-http-js-libp2p && yarn install)
|
||||||
COPY --from=gobuild /build/dendrite-main/main.wasm ./src/vector/dendrite.wasm
|
COPY --from=gobuild /build/dendrite-master/main.wasm ./src/vector/dendrite.wasm
|
||||||
# build it all
|
# build it all
|
||||||
RUN yarn build:p2p
|
RUN yarn build:p2p
|
||||||
|
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
RUN echo $'\
|
RUN echo $'\
|
||||||
{ \n\
|
{ \n\
|
||||||
"default_server_config": { \n\
|
"default_server_config": { \n\
|
||||||
"m.homeserver": { \n\
|
"m.homeserver": { \n\
|
||||||
"base_url": "https://p2p.riot.im", \n\
|
"base_url": "https://p2p.riot.im", \n\
|
||||||
|
|
@ -92,20 +92,20 @@ RUN echo $'\
|
||||||
"settingDefaults": { \n\
|
"settingDefaults": { \n\
|
||||||
"breadcrumbs": true \n\
|
"breadcrumbs": true \n\
|
||||||
} \n\
|
} \n\
|
||||||
}' > webapp/config.json
|
}' > webapp/config.json
|
||||||
|
|
||||||
FROM nginx
|
FROM nginx
|
||||||
# Add "Service-Worker-Allowed: /" header so the worker can sniff traffic on this domain rather
|
# Add "Service-Worker-Allowed: /" header so the worker can sniff traffic on this domain rather
|
||||||
# than just the path this gets hosted under. NB this newline echo syntax only works on bash.
|
# than just the path this gets hosted under. NB this newline echo syntax only works on bash.
|
||||||
SHELL ["/bin/bash", "-c"]
|
SHELL ["/bin/bash", "-c"]
|
||||||
RUN echo $'\
|
RUN echo $'\
|
||||||
server { \n\
|
server { \n\
|
||||||
listen 80; \n\
|
listen 80; \n\
|
||||||
add_header \'Service-Worker-Allowed\' \'/\'; \n\
|
add_header \'Service-Worker-Allowed\' \'/\'; \n\
|
||||||
location / { \n\
|
location / { \n\
|
||||||
root /usr/share/nginx/html; \n\
|
root /usr/share/nginx/html; \n\
|
||||||
index index.html index.htm; \n\
|
index index.html index.htm; \n\
|
||||||
} \n\
|
} \n\
|
||||||
}' > /etc/nginx/conf.d/default.conf
|
}' > /etc/nginx/conf.d/default.conf
|
||||||
RUN sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types
|
RUN sed -i 's/}/ application\/wasm wasm;\n}/g' /etc/nginx/mime.types
|
||||||
COPY --from=jsbuild /build/element-web-matthew-p2p/webapp /usr/share/nginx/html
|
COPY --from=jsbuild /build/element-web-matthew-p2p/webapp /usr/share/nginx/html
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
FROM docker.io/golang:1.22-alpine 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
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
COPY . /build
|
|
||||||
|
|
||||||
RUN mkdir -p bin
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-pinecone
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
RUN apk --update --no-cache add curl
|
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Pinecone demo)"
|
|
||||||
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"
|
|
||||||
|
|
||||||
COPY --from=base /build/bin/* /usr/bin/
|
|
||||||
|
|
||||||
VOLUME /etc/dendrite
|
|
||||||
WORKDIR /etc/dendrite
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-demo-pinecone"]
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
FROM docker.io/golang:1.22 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
|
|
||||||
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
COPY . /build
|
|
||||||
|
|
||||||
RUN mkdir -p bin
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/dendrite-demo-yggdrasil
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/create-account
|
|
||||||
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
|
||||||
|
|
||||||
FROM alpine:latest
|
|
||||||
LABEL org.opencontainers.image.title="Dendrite (Yggdrasil demo)"
|
|
||||||
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"
|
|
||||||
|
|
||||||
COPY --from=base /build/bin/* /usr/bin/
|
|
||||||
|
|
||||||
VOLUME /etc/dendrite
|
|
||||||
WORKDIR /etc/dendrite
|
|
||||||
|
|
||||||
ENTRYPOINT ["/usr/bin/dendrite-demo-yggdrasil"]
|
|
||||||
22
build/docker/Dockerfile.monolith
Normal file
22
build/docker/Dockerfile.monolith
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM docker.io/golang:1.17-alpine AS base
|
||||||
|
|
||||||
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY . /build
|
||||||
|
|
||||||
|
RUN mkdir -p bin
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-monolith-server
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/goose
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
COPY --from=base /build/bin/* /usr/bin/
|
||||||
|
|
||||||
|
VOLUME /etc/dendrite
|
||||||
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/dendrite-monolith-server"]
|
||||||
22
build/docker/Dockerfile.polylith
Normal file
22
build/docker/Dockerfile.polylith
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
FROM docker.io/golang:1.17-alpine AS base
|
||||||
|
|
||||||
|
RUN apk --update --no-cache add bash build-base
|
||||||
|
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
COPY . /build
|
||||||
|
|
||||||
|
RUN mkdir -p bin
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/dendrite-polylith-multi
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/goose
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/create-account
|
||||||
|
RUN go build -trimpath -o bin/ ./cmd/generate-keys
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
COPY --from=base /build/bin/* /usr/bin/
|
||||||
|
|
||||||
|
VOLUME /etc/dendrite
|
||||||
|
WORKDIR /etc/dendrite
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"]
|
||||||
|
|
@ -5,28 +5,31 @@ 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.deps.yml` which runs the Postgres and Kafka prerequisites
|
||||||
|
- `docker-compose.monolith.yml` which runs a monolith Dendrite deployment
|
||||||
- `docker-compose.yml` which runs a Dendrite deployment with Postgres
|
- `docker-compose.polylith.yml` which runs a polylith Dendrite deployment
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
||||||
runtime config should come from. The mounted folder must contain:
|
runtime config should come from. The mounted folder must contain:
|
||||||
|
|
||||||
- `dendrite.yaml` configuration file (based on one of the sample config files)
|
- `dendrite.yaml` configuration file (based on the [`dendrite-config.yaml`](https://raw.githubusercontent.com/matrix-org/dendrite/master/dendrite-config.yaml)
|
||||||
|
sample in the `build/docker/config` folder of this repository.)
|
||||||
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
||||||
- `server.crt` certificate file
|
- `server.crt` certificate file
|
||||||
- `server.key` private key file for the above certificate
|
- `server.key` private key file for the above certificate
|
||||||
|
|
@ -45,14 +48,36 @@ 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-config.yaml`](https://raw.githubusercontent.com/matrix-org/dendrite/master/dendrite-config.yaml) configuration file in the `build/docker/config` folder of this repository. And rename the config file to `dendrite.yml` (and put it in your `config` directory).
|
||||||
|
|
||||||
Then start the deployment:
|
Once in place, start the PostgreSQL dependency:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker-compose -f docker-compose.yml up
|
docker-compose -f docker-compose.deps.yml up postgres
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait a few seconds for PostgreSQL to finish starting up, and then start a monolith:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.monolith.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
## Starting Dendrite as a polylith deployment
|
||||||
|
|
||||||
|
Create your config based on the [`dendrite-config.yaml`](https://raw.githubusercontent.com/matrix-org/dendrite/master/dendrite-config.yaml) configuration file in the `build/docker/config` folder of this repository. And rename the config file to `dendrite.yml` (and put it in your `config` directory).
|
||||||
|
|
||||||
|
Once in place, start all the dependencies:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.deps.yml up
|
||||||
|
```
|
||||||
|
|
||||||
|
Wait a few seconds for PostgreSQL and Kafka to finish starting up, and then start a polylith:
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -f docker-compose.polylith.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
## Building the images
|
## Building the images
|
||||||
|
|
|
||||||
344
build/docker/config/dendrite-config.yaml
Normal file
344
build/docker/config/dendrite-config.yaml
Normal file
|
|
@ -0,0 +1,344 @@
|
||||||
|
# This is the Dendrite configuration file.
|
||||||
|
#
|
||||||
|
# The configuration is split up into sections - each Dendrite component has a
|
||||||
|
# configuration section, in addition to the "global" section which applies to
|
||||||
|
# all components.
|
||||||
|
#
|
||||||
|
# At a minimum, to get started, you will need to update the settings in the
|
||||||
|
# "global" section for your deployment, and you will need to check that the
|
||||||
|
# database "connection_string" line in each component section is correct.
|
||||||
|
#
|
||||||
|
# Each component with a "database" section can accept the following formats
|
||||||
|
# for "connection_string":
|
||||||
|
# SQLite: file:filename.db
|
||||||
|
# file:///path/to/filename.db
|
||||||
|
# PostgreSQL: postgresql://user:pass@hostname/database?params=...
|
||||||
|
#
|
||||||
|
# SQLite is embedded into Dendrite and therefore no further prerequisites are
|
||||||
|
# needed for the database when using SQLite mode. However, performance with
|
||||||
|
# PostgreSQL is significantly better and recommended for multi-user deployments.
|
||||||
|
# SQLite is typically around 20-30% slower than PostgreSQL when tested with a
|
||||||
|
# small number of users and likely will perform worse still with a higher volume
|
||||||
|
# of users.
|
||||||
|
#
|
||||||
|
# The "max_open_conns" and "max_idle_conns" settings configure the maximum
|
||||||
|
# number of open/idle database connections. The value 0 will use the database
|
||||||
|
# engine default, and a negative value will use unlimited connections. The
|
||||||
|
# "conn_max_lifetime" option controls the maximum length of time a database
|
||||||
|
# connection can be idle in seconds - a negative value is unlimited.
|
||||||
|
|
||||||
|
# The version of the configuration file.
|
||||||
|
version: 1
|
||||||
|
|
||||||
|
# Global Matrix configuration. This configuration applies to all components.
|
||||||
|
global:
|
||||||
|
# The domain name of this homeserver.
|
||||||
|
server_name: example.com
|
||||||
|
|
||||||
|
# The path to the signing private key file, used to sign requests and events.
|
||||||
|
private_key: matrix_key.pem
|
||||||
|
|
||||||
|
# The paths and expiry timestamps (as a UNIX timestamp in millisecond precision)
|
||||||
|
# to old signing private keys that were formerly in use on this domain. These
|
||||||
|
# keys will not be used for federation request or event signing, but will be
|
||||||
|
# provided to any other homeserver that asks when trying to verify old events.
|
||||||
|
# old_private_keys:
|
||||||
|
# - private_key: old_matrix_key.pem
|
||||||
|
# expired_at: 1601024554498
|
||||||
|
|
||||||
|
# How long a remote server can cache our server signing key before requesting it
|
||||||
|
# again. Increasing this number will reduce the number of requests made by other
|
||||||
|
# servers for our key but increases the period that a compromised key will be
|
||||||
|
# considered valid by other homeservers.
|
||||||
|
key_validity_period: 168h0m0s
|
||||||
|
|
||||||
|
# Lists of domains that the server will trust as identity servers to verify third
|
||||||
|
# party identifiers such as phone numbers and email addresses.
|
||||||
|
trusted_third_party_id_servers:
|
||||||
|
- matrix.org
|
||||||
|
- vector.im
|
||||||
|
|
||||||
|
# Configuration for Kafka/Naffka.
|
||||||
|
kafka:
|
||||||
|
# List of Kafka broker addresses to connect to. This is not needed if using
|
||||||
|
# Naffka in monolith mode.
|
||||||
|
addresses:
|
||||||
|
- kafka:9092
|
||||||
|
|
||||||
|
# The prefix to use for Kafka topic names for this homeserver. Change this only if
|
||||||
|
# you are running more than one Dendrite homeserver on the same Kafka deployment.
|
||||||
|
topic_prefix: Dendrite
|
||||||
|
|
||||||
|
# Whether to use Naffka instead of Kafka. This is only available in monolith
|
||||||
|
# mode, but means that you can run a single-process server without requiring
|
||||||
|
# Kafka.
|
||||||
|
use_naffka: false
|
||||||
|
|
||||||
|
# Naffka database options. Not required when using Kafka.
|
||||||
|
naffka_database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_naffka?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for Prometheus metric collection.
|
||||||
|
metrics:
|
||||||
|
# Whether or not Prometheus metrics are enabled.
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# HTTP basic authentication to protect access to monitoring.
|
||||||
|
basic_auth:
|
||||||
|
username: metrics
|
||||||
|
password: metrics
|
||||||
|
|
||||||
|
# DNS cache options. The DNS cache may reduce the load on DNS servers
|
||||||
|
# if there is no local caching resolver available for use.
|
||||||
|
dns_cache:
|
||||||
|
# Whether or not the DNS cache is enabled.
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# Maximum number of entries to hold in the DNS cache, and
|
||||||
|
# for how long those items should be considered valid in seconds.
|
||||||
|
cache_size: 256
|
||||||
|
cache_lifetime: 300
|
||||||
|
|
||||||
|
# Configuration for the Appservice API.
|
||||||
|
app_service_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7777
|
||||||
|
connect: http://appservice_api:7777
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_appservice?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Appservice configuration files to load into this homeserver.
|
||||||
|
config_files: []
|
||||||
|
|
||||||
|
# Configuration for the Client API.
|
||||||
|
client_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7771
|
||||||
|
connect: http://client_api:7771
|
||||||
|
external_api:
|
||||||
|
listen: http://0.0.0.0:8071
|
||||||
|
|
||||||
|
# Prevents new users from being able to register on this homeserver, except when
|
||||||
|
# using the registration shared secret below.
|
||||||
|
registration_disabled: false
|
||||||
|
|
||||||
|
# If set, allows registration by anyone who knows the shared secret, regardless of
|
||||||
|
# whether registration is otherwise disabled.
|
||||||
|
registration_shared_secret: ""
|
||||||
|
|
||||||
|
# Whether to require reCAPTCHA for registration.
|
||||||
|
enable_registration_captcha: false
|
||||||
|
|
||||||
|
# Settings for ReCAPTCHA.
|
||||||
|
recaptcha_public_key: ""
|
||||||
|
recaptcha_private_key: ""
|
||||||
|
recaptcha_bypass_secret: ""
|
||||||
|
recaptcha_siteverify_api: ""
|
||||||
|
|
||||||
|
# TURN server information that this homeserver should send to clients.
|
||||||
|
turn:
|
||||||
|
turn_user_lifetime: ""
|
||||||
|
turn_uris: []
|
||||||
|
turn_shared_secret: ""
|
||||||
|
turn_username: ""
|
||||||
|
turn_password: ""
|
||||||
|
|
||||||
|
# Settings for rate-limited endpoints. Rate limiting will kick in after the
|
||||||
|
# threshold number of "slots" have been taken by requests from a specific
|
||||||
|
# host. Each "slot" will be released after the cooloff time in milliseconds.
|
||||||
|
rate_limiting:
|
||||||
|
enabled: true
|
||||||
|
threshold: 5
|
||||||
|
cooloff_ms: 500
|
||||||
|
|
||||||
|
# Configuration for the EDU server.
|
||||||
|
edu_server:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7778
|
||||||
|
connect: http://edu_server:7778
|
||||||
|
|
||||||
|
# Configuration for the Federation API.
|
||||||
|
federation_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7772
|
||||||
|
connect: http://federation_api:7772
|
||||||
|
external_api:
|
||||||
|
listen: http://0.0.0.0:8072
|
||||||
|
|
||||||
|
# List of paths to X.509 certificates to be used by the external federation listeners.
|
||||||
|
# These certificates will be used to calculate the TLS fingerprints and other servers
|
||||||
|
# will expect the certificate to match these fingerprints. Certificates must be in PEM
|
||||||
|
# format.
|
||||||
|
federation_certificates: []
|
||||||
|
|
||||||
|
# Configuration for the Federation Sender.
|
||||||
|
federation_sender:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7775
|
||||||
|
connect: http://federation_sender:7775
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_federationsender?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# How many times we will try to resend a failed transaction to a specific server. The
|
||||||
|
# backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc.
|
||||||
|
send_max_retries: 16
|
||||||
|
|
||||||
|
# Disable the validation of TLS certificates of remote federated homeservers. Do not
|
||||||
|
# enable this option in production as it presents a security risk!
|
||||||
|
disable_tls_validation: false
|
||||||
|
|
||||||
|
# Use the following proxy server for outbound federation traffic.
|
||||||
|
proxy_outbound:
|
||||||
|
enabled: false
|
||||||
|
protocol: http
|
||||||
|
host: localhost
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
# Configuration for the Key Server (for end-to-end encryption).
|
||||||
|
key_server:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7779
|
||||||
|
connect: http://key_server:7779
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_keyserver?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for the Media API.
|
||||||
|
media_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7774
|
||||||
|
connect: http://media_api:7774
|
||||||
|
external_api:
|
||||||
|
listen: http://0.0.0.0:8074
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_mediaapi?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Storage path for uploaded media. May be relative or absolute.
|
||||||
|
base_path: /var/dendrite/media
|
||||||
|
|
||||||
|
# The maximum allowed file size (in bytes) for media uploads to this homeserver
|
||||||
|
# (0 = unlimited).
|
||||||
|
max_file_size_bytes: 10485760
|
||||||
|
|
||||||
|
# Whether to dynamically generate thumbnails if needed.
|
||||||
|
dynamic_thumbnails: false
|
||||||
|
|
||||||
|
# The maximum number of simultaneous thumbnail generators to run.
|
||||||
|
max_thumbnail_generators: 10
|
||||||
|
|
||||||
|
# A list of thumbnail sizes to be generated for media content.
|
||||||
|
thumbnail_sizes:
|
||||||
|
- width: 32
|
||||||
|
height: 32
|
||||||
|
method: crop
|
||||||
|
- width: 96
|
||||||
|
height: 96
|
||||||
|
method: crop
|
||||||
|
- width: 640
|
||||||
|
height: 480
|
||||||
|
method: scale
|
||||||
|
|
||||||
|
# Configuration for the Room Server.
|
||||||
|
room_server:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7770
|
||||||
|
connect: http://room_server:7770
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_roomserver?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for the Server Key API (for server signing keys).
|
||||||
|
signing_key_server:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7780
|
||||||
|
connect: http://signing_key_server:7780
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_signingkeyserver?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Perspective keyservers to use as a backup when direct key fetches fail. This may
|
||||||
|
# be required to satisfy key requests for servers that are no longer online when
|
||||||
|
# joining some rooms.
|
||||||
|
key_perspectives:
|
||||||
|
- server_name: matrix.org
|
||||||
|
keys:
|
||||||
|
- key_id: ed25519:auto
|
||||||
|
public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw
|
||||||
|
- key_id: ed25519:a_RXGa
|
||||||
|
public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ
|
||||||
|
|
||||||
|
# This option will control whether Dendrite will prefer to look up keys directly
|
||||||
|
# or whether it should try perspective servers first, using direct fetches as a
|
||||||
|
# last resort.
|
||||||
|
prefer_direct_fetch: false
|
||||||
|
|
||||||
|
# Configuration for the Sync API.
|
||||||
|
sync_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7773
|
||||||
|
connect: http://sync_api:7773
|
||||||
|
external_api:
|
||||||
|
listen: http://0.0.0.0:8073
|
||||||
|
database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_syncapi?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for the User API.
|
||||||
|
user_api:
|
||||||
|
internal_api:
|
||||||
|
listen: http://0.0.0.0:7781
|
||||||
|
connect: http://user_api:7781
|
||||||
|
account_database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_userapi_accounts?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
device_database:
|
||||||
|
connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_userapi_devices?sslmode=disable
|
||||||
|
max_open_conns: 10
|
||||||
|
max_idle_conns: 2
|
||||||
|
conn_max_lifetime: -1
|
||||||
|
|
||||||
|
# Configuration for Opentracing.
|
||||||
|
# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on
|
||||||
|
# how this works and how to set it up.
|
||||||
|
tracing:
|
||||||
|
enabled: false
|
||||||
|
jaeger:
|
||||||
|
serviceName: ""
|
||||||
|
disabled: false
|
||||||
|
rpc_metrics: false
|
||||||
|
tags: []
|
||||||
|
sampler: null
|
||||||
|
reporter: null
|
||||||
|
headers: null
|
||||||
|
baggage_restrictions: null
|
||||||
|
throttler: null
|
||||||
|
|
||||||
|
# Logging configuration, in addition to the standard logging that is sent to
|
||||||
|
# stdout by Dendrite.
|
||||||
|
logging:
|
||||||
|
- type: file
|
||||||
|
level: info
|
||||||
|
params:
|
||||||
|
path: /var/log/dendrite
|
||||||
42
build/docker/docker-compose.deps.yml
Normal file
42
build/docker/docker-compose.deps.yml
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
# PostgreSQL is needed for both polylith and monolith modes.
|
||||||
|
postgres:
|
||||||
|
hostname: postgres
|
||||||
|
image: postgres:11
|
||||||
|
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, you will need to add something like this:
|
||||||
|
# - ./path/to/persistent/storage:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_PASSWORD: itsasecret
|
||||||
|
POSTGRES_USER: dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
# Zookeeper is only needed for polylith mode!
|
||||||
|
zookeeper:
|
||||||
|
hostname: zookeeper
|
||||||
|
image: zookeeper
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
# Kafka is only needed for polylith mode!
|
||||||
|
kafka:
|
||||||
|
container_name: dendrite_kafka
|
||||||
|
hostname: kafka
|
||||||
|
image: wurstmeister/kafka
|
||||||
|
environment:
|
||||||
|
KAFKA_ADVERTISED_HOST_NAME: "kafka"
|
||||||
|
KAFKA_DELETE_TOPIC_ENABLE: "true"
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT: "zookeeper:2181"
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
||||||
21
build/docker/docker-compose.monolith.yml
Normal file
21
build/docker/docker-compose.monolith.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
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
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
networks:
|
||||||
|
internal:
|
||||||
|
attachable: true
|
||||||
108
build/docker/docker-compose.polylith.yml
Normal file
108
build/docker/docker-compose.polylith.yml
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
version: "3.4"
|
||||||
|
services:
|
||||||
|
client_api:
|
||||||
|
hostname: client_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: clientapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
media_api:
|
||||||
|
hostname: media_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: mediaapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
- ./media:/var/dendrite/media
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
sync_api:
|
||||||
|
hostname: sync_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: syncapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
room_server:
|
||||||
|
hostname: room_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: roomserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
edu_server:
|
||||||
|
hostname: edu_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: eduserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
federation_api:
|
||||||
|
hostname: federation_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: federationapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
federation_sender:
|
||||||
|
hostname: federation_sender
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: federationsender
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
key_server:
|
||||||
|
hostname: key_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: keyserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
signing_key_server:
|
||||||
|
hostname: signing_key_server
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: signingkeyserver
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
user_api:
|
||||||
|
hostname: user_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: userapi
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
|
||||||
|
appservice_api:
|
||||||
|
hostname: appservice_api
|
||||||
|
image: matrixdotorg/dendrite-polylith:latest
|
||||||
|
command: appservice
|
||||||
|
volumes:
|
||||||
|
- ./config:/etc/dendrite
|
||||||
|
networks:
|
||||||
|
- internal
|
||||||
|
depends_on:
|
||||||
|
- room_server
|
||||||
|
- user_api
|
||||||
|
|
||||||
|
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 userapi_devices mediaapi syncapi roomserver signingkeyserver keyserver federationsender appservice naffka; 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
|
||||||
|
|
@ -1,46 +1,49 @@
|
||||||
// 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
|
package gobind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"crypto/tls"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net"
|
"net"
|
||||||
"path/filepath"
|
"net/http"
|
||||||
"strings"
|
"os"
|
||||||
|
"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-yggdrasil/signing"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/signing"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/eduserver"
|
||||||
|
"github.com/matrix-org/dendrite/eduserver/cache"
|
||||||
|
"github.com/matrix-org/dendrite/federationsender"
|
||||||
|
"github.com/matrix-org/dendrite/federationsender/api"
|
||||||
"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/config"
|
||||||
"github.com/matrix-org/dendrite/setup/process"
|
"github.com/matrix-org/dendrite/setup/process"
|
||||||
|
"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"
|
||||||
|
|
||||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||||
|
"github.com/matrix-org/pinecone/router"
|
||||||
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 +52,114 @@ 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
|
||||||
|
PineconeMulticast *pineconeMulticast.Multicast
|
||||||
|
PineconeQUIC *pineconeSessions.Sessions
|
||||||
StorageDirectory string
|
StorageDirectory string
|
||||||
CacheDirectory string
|
CacheDirectory string
|
||||||
|
staticPeerURI string
|
||||||
|
staticPeerMutex sync.RWMutex
|
||||||
|
staticPeerAttempt chan struct{}
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
}
|
httpServer *http.Server
|
||||||
|
processContext *process.ProcessContext
|
||||||
func (m *DendriteMonolith) PublicKey() string {
|
userAPI userapiAPI.UserInternalAPI
|
||||||
return m.p2pMonolith.Router.PublicKey().String()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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.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(pineconeRouter.PeerTypeMulticast)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||||
m.p2pMonolith.ConnManager.RemovePeers()
|
m.staticPeerMutex.Lock()
|
||||||
for _, uri := range strings.Split(uri, ",") {
|
m.staticPeerURI = uri
|
||||||
m.p2pMonolith.ConnManager.AddPeer(strings.TrimSpace(uri))
|
m.staticPeerMutex.Unlock()
|
||||||
|
m.DisconnectType(pineconeRouter.PeerTypeRemote)
|
||||||
|
if uri != "" {
|
||||||
|
go func() {
|
||||||
|
m.staticPeerAttempt <- struct{}{}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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) error {
|
||||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(port), nil)
|
return 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()
|
||||||
|
loop:
|
||||||
|
for i := 1; i <= 10; i++ {
|
||||||
|
logrus.Errorf("Attempting authenticated connect (attempt %d)", i)
|
||||||
var err error
|
var err error
|
||||||
if port, err = m.p2pMonolith.Router.Connect(
|
conduit.port, err = m.PineconeRouter.AuthenticatedConnect(l, zone, peertype)
|
||||||
l,
|
switch err {
|
||||||
pineconeRouter.ConnectionZone(zone),
|
case io.ErrClosedPipe:
|
||||||
pineconeRouter.ConnectionPeerType(peertype),
|
logrus.Errorf("Authenticated connect failed due to closed pipe (attempt %d)", i)
|
||||||
); err != nil {
|
return
|
||||||
logrus.Errorf("Authenticated connect failed: %s", err)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
_ = l.Close()
|
_ = l.Close()
|
||||||
_ = r.Close()
|
_ = r.Close()
|
||||||
_ = newConduit.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
newConduit.SetPort(port)
|
|
||||||
logrus.Infof("Authenticated connect succeeded (port %d)", newConduit.Port())
|
|
||||||
}()
|
}()
|
||||||
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 +167,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 +185,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 +194,58 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
return loginRes.Device.AccessToken, nil
|
return loginRes.Device.AccessToken, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *DendriteMonolith) staticPeerConnect() {
|
||||||
|
attempt := func() {
|
||||||
|
if m.PineconeRouter.PeerCount(router.PeerTypeRemote) == 0 {
|
||||||
|
m.staticPeerMutex.RLock()
|
||||||
|
uri := m.staticPeerURI
|
||||||
|
m.staticPeerMutex.RUnlock()
|
||||||
|
if uri == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := conn.ConnectToPeer(m.PineconeRouter, uri); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to connect to static peer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.processContext.Context().Done():
|
||||||
|
case <-m.staticPeerAttempt:
|
||||||
|
attempt()
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
attempt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// nolint:gocyclo
|
||||||
func (m *DendriteMonolith) Start() {
|
func (m *DendriteMonolith) Start() {
|
||||||
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
var err error
|
||||||
oldKeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
var sk ed25519.PrivateKey
|
||||||
sk, pk := monolith.GetOrCreateKey(keyfile, oldKeyfile)
|
var pk ed25519.PublicKey
|
||||||
|
keyfile := fmt.Sprintf("%s/p2p.key", m.StorageDirectory)
|
||||||
|
if _, err = os.Stat(keyfile); os.IsNotExist(err) {
|
||||||
|
if pk, sk, err = ed25519.GenerateKey(nil); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err = ioutil.WriteFile(keyfile, sk, 0644); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
} else if err == nil {
|
||||||
|
if sk, err = ioutil.ReadFile(keyfile); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if len(sk) != ed25519.PrivateKeySize {
|
||||||
|
panic("the private key is not long enough")
|
||||||
|
}
|
||||||
|
pk = sk.Public().(ed25519.PublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 +253,177 @@ func (m *DendriteMonolith) Start() {
|
||||||
m.logger.SetOutput(BindLogger{})
|
m.logger.SetOutput(BindLogger{})
|
||||||
logrus.SetOutput(BindLogger{})
|
logrus.SetOutput(BindLogger{})
|
||||||
|
|
||||||
m.p2pMonolith = monolith.P2PMonolith{}
|
logger := log.New(os.Stdout, "PINECONE: ", 0)
|
||||||
m.p2pMonolith.SetupPinecone(sk)
|
m.PineconeRouter = pineconeRouter.NewRouter(logger, "dendrite", sk, pk, nil)
|
||||||
|
m.PineconeQUIC = pineconeSessions.NewSessions(logger, m.PineconeRouter)
|
||||||
|
m.PineconeMulticast = pineconeMulticast.NewMulticast(logger, m.PineconeRouter)
|
||||||
|
|
||||||
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()
|
||||||
|
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.Kafka.UseNaffka = true
|
||||||
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
cfg.Global.Kafka.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-naffka.db", 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.UserAPI.DeviceDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-device.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.MediaAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-mediaapi.db", m.CacheDirectory, prefix))
|
||||||
|
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.SigningKeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-signingkeyserver.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-keyserver.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.FederationSender.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-federationsender.db", m.StorageDirectory, prefix))
|
||||||
|
cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-appservice.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"}
|
||||||
|
if err := cfg.Derive(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
processCtx := process.NewProcessContext()
|
base := setup.NewBaseDendrite(cfg, "Monolith", false)
|
||||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
defer base.Close() // nolint: errcheck
|
||||||
routers := httputil.NewRouters()
|
|
||||||
|
|
||||||
enableRelaying := false
|
accountDB := base.CreateAccountsDB()
|
||||||
enableMetrics := false
|
federation := conn.CreateFederationClient(base, m.PineconeQUIC)
|
||||||
enableWebsockets := false
|
|
||||||
m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
m.p2pMonolith.StartMonolith()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
|
rsAPI := roomserver.NewInternalAPI(
|
||||||
|
base, keyRing,
|
||||||
|
)
|
||||||
|
|
||||||
|
fsAPI := federationsender.NewInternalAPI(
|
||||||
|
base, federation, rsAPI, keyRing, true,
|
||||||
|
)
|
||||||
|
|
||||||
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
|
m.userAPI = userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
||||||
|
keyAPI.SetUserAPI(m.userAPI)
|
||||||
|
|
||||||
|
eduInputAPI := eduserver.NewInternalAPI(
|
||||||
|
base, cache.New(), 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.SetFederationSenderAPI(fsAPI)
|
||||||
|
|
||||||
|
monolith := setup.Monolith{
|
||||||
|
Config: base.Cfg,
|
||||||
|
AccountDB: accountDB,
|
||||||
|
Client: conn.CreateClient(base, m.PineconeQUIC),
|
||||||
|
FedClient: federation,
|
||||||
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
AppserviceAPI: asAPI,
|
||||||
|
EDUInternalAPI: eduInputAPI,
|
||||||
|
FederationSenderAPI: fsAPI,
|
||||||
|
RoomserverAPI: rsAPI,
|
||||||
|
UserAPI: m.userAPI,
|
||||||
|
KeyAPI: keyAPI,
|
||||||
|
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(m.PineconeRouter, m.PineconeQUIC, fsAPI, federation),
|
||||||
|
}
|
||||||
|
monolith.AddAllPublicRoutes(
|
||||||
|
base.ProcessContext,
|
||||||
|
base.PublicClientAPIMux,
|
||||||
|
base.PublicFederationAPIMux,
|
||||||
|
base.PublicKeyAPIMux,
|
||||||
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
pMux := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
|
pMux.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
||||||
|
pMux.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||||
|
|
||||||
|
pHTTP := m.PineconeQUIC.HTTP()
|
||||||
|
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
|
||||||
|
|
||||||
|
m.staticPeerAttempt = make(chan struct{}, 1)
|
||||||
|
go m.staticPeerConnect()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
m.logger.Info("Listening on ", cfg.Global.ServerName)
|
||||||
|
m.logger.Fatal(m.httpServer.Serve(m.PineconeQUIC))
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
logrus.Info("Listening on ", m.listener.Addr())
|
||||||
|
logrus.Fatal(http.Serve(m.listener, httpRouter))
|
||||||
|
}()
|
||||||
|
go func() {
|
||||||
|
logrus.Info("Sending wake-up message to known nodes")
|
||||||
|
req := &api.PerformBroadcastEDURequest{}
|
||||||
|
res := &api.PerformBroadcastEDUResponse{}
|
||||||
|
if err := fsAPI.PerformBroadcastEDU(context.TODO(), req, res); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to send wake-up message to known nodes")
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Stop() {
|
func (m *DendriteMonolith) Stop() {
|
||||||
m.p2pMonolith.Stop()
|
_ = m.listener.Close()
|
||||||
|
m.PineconeMulticast.Stop()
|
||||||
|
_ = m.PineconeQUIC.Close()
|
||||||
|
m.processContext.ShutdownDendrite()
|
||||||
|
_ = m.PineconeRouter.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,18 +1,3 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
//go:build ios
|
|
||||||
// +build ios
|
// +build ios
|
||||||
|
|
||||||
package gobind
|
package gobind
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,3 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
//go:build !ios
|
|
||||||
// +build !ios
|
// +build !ios
|
||||||
|
|
||||||
package gobind
|
package gobind
|
||||||
|
|
|
||||||
|
|
@ -2,38 +2,28 @@ package gobind
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"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"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggconn"
|
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggconn"
|
||||||
"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/eduserver"
|
||||||
"github.com/matrix-org/dendrite/federationapi/api"
|
"github.com/matrix-org/dendrite/eduserver/cache"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/federationsender"
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
"github.com/matrix-org/dendrite/federationsender/api"
|
||||||
"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/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/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"
|
||||||
|
|
@ -45,7 +35,6 @@ type DendriteMonolith struct {
|
||||||
StorageDirectory string
|
StorageDirectory string
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
processContext *process.ProcessContext
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) BaseURL() string {
|
func (m *DendriteMonolith) BaseURL() string {
|
||||||
|
|
@ -73,183 +62,112 @@ func (m *DendriteMonolith) DisconnectMulticastPeers() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Start() {
|
func (m *DendriteMonolith) Start() {
|
||||||
var pk ed25519.PublicKey
|
|
||||||
var sk ed25519.PrivateKey
|
|
||||||
|
|
||||||
m.logger = logrus.Logger{
|
m.logger = logrus.Logger{
|
||||||
Out: BindLogger{},
|
Out: BindLogger{},
|
||||||
}
|
}
|
||||||
m.logger.SetOutput(BindLogger{})
|
m.logger.SetOutput(BindLogger{})
|
||||||
logrus.SetOutput(BindLogger{})
|
logrus.SetOutput(BindLogger{})
|
||||||
|
|
||||||
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
|
||||||
if _, err := os.Stat(keyfile); os.IsNotExist(err) {
|
|
||||||
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 {
|
|
||||||
var 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pk = sk.Public().(ed25519.PublicKey)
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
m.listener, err = net.Listen("tcp", "localhost:65432")
|
m.listener, err = net.Listen("tcp", "localhost:65432")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ygg, err := yggconn.Setup(sk, "dendrite", m.StorageDirectory, "", "")
|
ygg, err := yggconn.Setup("dendrite", m.StorageDirectory, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
m.YggdrasilNode = ygg
|
m.YggdrasilNode = ygg
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults(config.DefaultOpts{
|
cfg.Defaults()
|
||||||
Generate: true,
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
||||||
SingleDatabase: true,
|
cfg.Global.PrivateKey = ygg.PrivateKey()
|
||||||
})
|
|
||||||
cfg.Global.ServerName = spec.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.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
cfg.Global.Kafka.UseNaffka = true
|
||||||
cfg.Global.JetStream.InMemory = true
|
cfg.Global.Kafka.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-naffka.db", m.StorageDirectory))
|
||||||
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-account.db", m.StorageDirectory))
|
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-account.db", m.StorageDirectory))
|
||||||
|
cfg.UserAPI.DeviceDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-device.db", m.StorageDirectory))
|
||||||
cfg.MediaAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-mediaapi.db", m.StorageDirectory))
|
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/dendrite-p2p-syncapi.db", m.StorageDirectory))
|
cfg.SyncAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-syncapi.db", m.StorageDirectory))
|
||||||
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-roomserver.db", m.StorageDirectory))
|
cfg.RoomServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-roomserver.db", m.StorageDirectory))
|
||||||
|
cfg.SigningKeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-signingkeyserver.db", m.StorageDirectory))
|
||||||
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-keyserver.db", m.StorageDirectory))
|
cfg.KeyServer.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-keyserver.db", m.StorageDirectory))
|
||||||
cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-federationsender.db", m.StorageDirectory))
|
cfg.FederationSender.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-federationsender.db", m.StorageDirectory))
|
||||||
|
cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-appservice.db", m.StorageDirectory))
|
||||||
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
||||||
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
||||||
cfg.ClientAPI.RegistrationDisabled = false
|
|
||||||
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
|
||||||
if err = cfg.Derive(); err != nil {
|
if err = cfg.Derive(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
configErrors := &config.ConfigErrors{}
|
base := setup.NewBaseDendrite(cfg, "Monolith", false)
|
||||||
cfg.Verify(configErrors)
|
defer base.Close() // nolint: errcheck
|
||||||
if len(*configErrors) > 0 {
|
|
||||||
for _, err := range *configErrors {
|
|
||||||
logrus.Errorf("Configuration error: %s", err)
|
|
||||||
}
|
|
||||||
logrus.Fatalf("Failed to start due to configuration errors")
|
|
||||||
}
|
|
||||||
|
|
||||||
internal.SetupStdLogging()
|
accountDB := base.CreateAccountsDB()
|
||||||
internal.SetupHookLogging(cfg.Logging)
|
federation := ygg.CreateFederationClient(base)
|
||||||
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(
|
||||||
natsInstance := jetstream.NATSInstance{}
|
base, keyRing,
|
||||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
|
||||||
|
|
||||||
fsAPI := federationapi.NewInternalAPI(
|
|
||||||
processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
userAPI := userapi.NewInternalAPI(processCtx, cfg, cm, &natsInstance, rsAPI, federation, caching.EnableMetrics, fsAPI.IsBlacklistedOrBackingOff)
|
fsAPI := federationsender.NewInternalAPI(
|
||||||
|
base, federation, rsAPI, keyRing, true,
|
||||||
|
)
|
||||||
|
|
||||||
asAPI := appservice.NewInternalAPI(processCtx, cfg, &natsInstance, userAPI, rsAPI)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
||||||
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
eduInputAPI := eduserver.NewInternalAPI(
|
||||||
|
base, cache.New(), userAPI,
|
||||||
|
)
|
||||||
|
|
||||||
|
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.
|
||||||
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
||||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
rsAPI.SetFederationSenderAPI(fsAPI)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
Config: cfg,
|
Config: base.Cfg,
|
||||||
Client: ygg.CreateClient(),
|
AccountDB: accountDB,
|
||||||
|
Client: ygg.CreateClient(base),
|
||||||
FedClient: federation,
|
FedClient: federation,
|
||||||
KeyRing: keyRing,
|
KeyRing: keyRing,
|
||||||
|
|
||||||
AppserviceAPI: asAPI,
|
AppserviceAPI: asAPI,
|
||||||
FederationAPI: fsAPI,
|
EDUInternalAPI: eduInputAPI,
|
||||||
|
FederationSenderAPI: 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.ProcessContext,
|
||||||
|
base.PublicClientAPIMux,
|
||||||
|
base.PublicFederationAPIMux,
|
||||||
|
base.PublicKeyAPIMux,
|
||||||
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
|
)
|
||||||
|
|
||||||
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{
|
||||||
|
|
@ -266,11 +184,11 @@ func (m *DendriteMonolith) Start() {
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
m.logger.Info("Listening on ", ygg.DerivedServerName())
|
m.logger.Info("Listening on ", ygg.DerivedServerName())
|
||||||
m.logger.Error(m.httpServer.Serve(ygg))
|
m.logger.Fatal(m.httpServer.Serve(ygg))
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
logrus.Info("Listening on ", m.listener.Addr())
|
logrus.Info("Listening on ", m.listener.Addr())
|
||||||
logrus.Error(http.Serve(m.listener, httpRouter))
|
logrus.Fatal(http.Serve(m.listener, httpRouter))
|
||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
logrus.Info("Sending wake-up message to known nodes")
|
logrus.Info("Sending wake-up message to known nodes")
|
||||||
|
|
@ -282,12 +200,9 @@ func (m *DendriteMonolith) Start() {
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) Stop() {
|
func (m *DendriteMonolith) Suspend() {
|
||||||
|
m.logger.Info("Suspending monolith")
|
||||||
if err := m.httpServer.Close(); err != nil {
|
if err := m.httpServer.Close(); err != nil {
|
||||||
m.logger.Warn("Error stopping HTTP server:", err)
|
m.logger.Warn("Error stopping HTTP server:", err)
|
||||||
}
|
}
|
||||||
if m.processContext != nil {
|
|
||||||
m.processContext.ShutdownDendrite()
|
|
||||||
m.processContext.WaitForComponentsToFinish()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
//go:build ios
|
|
||||||
// +build ios
|
// +build ios
|
||||||
|
|
||||||
package gobind
|
package gobind
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
//go:build !ios
|
|
||||||
// +build !ios
|
// +build !ios
|
||||||
|
|
||||||
package gobind
|
package gobind
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,21 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
FROM golang:1.13-stretch as build
|
||||||
|
|
||||||
FROM golang:1.22-bookworm as build
|
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
RUN apt-get update && apt-get install -y sqlite3
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
# we will dump the binaries and config file to this location to ensure any local untracked files
|
|
||||||
# that come from the COPY . . file don't contaminate the build
|
|
||||||
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
|
COPY go.mod .
|
||||||
RUN --mount=target=. \
|
COPY go.sum .
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
RUN go mod download
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
|
||||||
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
|
COPY . .
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
RUN go build ./cmd/dendrite-monolith-server
|
||||||
|
RUN go build ./cmd/generate-keys
|
||||||
|
RUN go build ./cmd/generate-config
|
||||||
|
RUN ./generate-config --ci > dendrite.yaml
|
||||||
|
RUN ./generate-keys --private-key matrix_key.pem --tls-cert server.crt --tls-key server.key
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
ENV SERVER_NAME=localhost
|
||||||
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
|
CMD sed -i "s/server_name: localhost/server_name: ${SERVER_NAME}/g" dendrite.yaml && ./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
||||||
# At runtime, replace the SERVER_NAME with what we are told
|
|
||||||
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 && \
|
|
||||||
cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \
|
|
||||||
exec /complement-cmd.sh
|
|
||||||
|
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
|
||||||
|
|
||||||
# A local development Complement dockerfile, to be used with host mounts
|
|
||||||
# /cache -> Contains the entire dendrite code at Dockerfile build time. Builds binaries but only keeps the generate-* ones. Pre-compilation saves time.
|
|
||||||
# /dendrite -> Host-mounted sources
|
|
||||||
# /runtime -> Binaries and config go here and are run at runtime
|
|
||||||
# At runtime, dendrite is built from /dendrite and run in /runtime.
|
|
||||||
#
|
|
||||||
# Use these mounts to make use of this dockerfile:
|
|
||||||
# COMPLEMENT_HOST_MOUNTS='/your/local/dendrite:/dendrite:ro;/your/go/path:/go:ro'
|
|
||||||
FROM golang:1.22-bookworm
|
|
||||||
RUN apt-get update && apt-get install -y sqlite3
|
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
|
||||||
ENV COVER=0
|
|
||||||
EXPOSE 8008 8448
|
|
||||||
|
|
||||||
WORKDIR /runtime
|
|
||||||
# This script compiles Dendrite for us.
|
|
||||||
RUN echo '\
|
|
||||||
#!/bin/bash -eux \n\
|
|
||||||
if test -f "/runtime/dendrite" && test -f "/runtime/dendrite-cover"; then \n\
|
|
||||||
echo "Skipping compilation; binaries exist" \n\
|
|
||||||
exit 0 \n\
|
|
||||||
fi \n\
|
|
||||||
cd /dendrite \n\
|
|
||||||
go build -v -o /runtime /dendrite/cmd/dendrite \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
|
|
||||||
|
|
||||||
# This script runs Dendrite for us. Must be run in the /runtime directory.
|
|
||||||
RUN echo '\
|
|
||||||
#!/bin/bash -eu \n\
|
|
||||||
./generate-keys --private-key matrix_key.pem \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\
|
|
||||||
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 --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
|
||||||
' > run.sh && chmod +x run.sh
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /cache
|
|
||||||
# Build the monolith in /cache - we won't actually use this but will rely on build artifacts to speed
|
|
||||||
# up the real compilation. Build the generate-* binaries in the true /runtime locations.
|
|
||||||
# If the generate-* source is changed, this dockerfile needs re-running.
|
|
||||||
RUN --mount=target=. \
|
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
go build -o /runtime ./cmd/generate-config && \
|
|
||||||
go build -o /runtime ./cmd/generate-keys
|
|
||||||
|
|
||||||
|
|
||||||
WORKDIR /runtime
|
|
||||||
CMD /runtime/compile.sh && exec /runtime/run.sh
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
#syntax=docker/dockerfile:1.2
|
|
||||||
|
|
||||||
FROM golang:1.22-bookworm as build
|
|
||||||
RUN apt-get update && apt-get install -y postgresql
|
|
||||||
WORKDIR /build
|
|
||||||
|
|
||||||
# No password when connecting to Postgres
|
|
||||||
RUN sed -i "s%peer%trust%g" /etc/postgresql/15/main/pg_hba.conf && \
|
|
||||||
# Bump up max conns for moar concurrency
|
|
||||||
sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/15/main/postgresql.conf
|
|
||||||
|
|
||||||
# This entry script starts postgres, waits for it to be up then starts dendrite
|
|
||||||
RUN echo '\
|
|
||||||
#!/bin/bash -eu \n\
|
|
||||||
pg_lsclusters \n\
|
|
||||||
pg_ctlcluster 15 main start \n\
|
|
||||||
\n\
|
|
||||||
until pg_isready \n\
|
|
||||||
do \n\
|
|
||||||
echo "Waiting for postgres"; \n\
|
|
||||||
sleep 1; \n\
|
|
||||||
done \n\
|
|
||||||
' > run_postgres.sh && chmod +x run_postgres.sh
|
|
||||||
|
|
||||||
# we will dump the binaries and config file to this location to ensure any local untracked files
|
|
||||||
# that come from the COPY . . file don't contaminate the build
|
|
||||||
RUN mkdir /dendrite
|
|
||||||
|
|
||||||
# Utilise Docker caching when downloading dependencies, this stops us needlessly
|
|
||||||
# downloading dependencies every time.
|
|
||||||
ARG CGO
|
|
||||||
RUN --mount=target=. \
|
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
|
||||||
--mount=type=cache,target=/root/.cache/go-build \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-config && \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite ./cmd/generate-keys && \
|
|
||||||
CGO_ENABLED=${CGO} go build -o /dendrite/dendrite ./cmd/dendrite && \
|
|
||||||
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
|
|
||||||
RUN ./generate-keys --private-key matrix_key.pem
|
|
||||||
|
|
||||||
ENV SERVER_NAME=localhost
|
|
||||||
ENV API=0
|
|
||||||
ENV COVER=0
|
|
||||||
EXPOSE 8008 8448
|
|
||||||
|
|
||||||
|
|
||||||
# At runtime, generate TLS cert based on the CA now mounted at /ca
|
|
||||||
# At runtime, replace the SERVER_NAME with what we are told
|
|
||||||
CMD /build/run_postgres.sh && ./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 --db "user=postgres database=postgres host=/var/run/postgresql/" > dendrite.yaml && \
|
|
||||||
# Bump max_open_conns up here in the global database config
|
|
||||||
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 && \
|
|
||||||
exec /complement-cmd.sh
|
|
||||||
|
|
@ -13,4 +13,4 @@ go build ./cmd/...
|
||||||
./build/scripts/find-lint.sh
|
./build/scripts/find-lint.sh
|
||||||
|
|
||||||
echo "Testing..."
|
echo "Testing..."
|
||||||
go test --race -v ./...
|
go test -v ./...
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ echo "Installing golangci-lint..."
|
||||||
|
|
||||||
# Make a backup of go.{mod,sum} first
|
# Make a backup of go.{mod,sum} first
|
||||||
cp go.mod go.mod.bak && cp go.sum go.sum.bak
|
cp go.mod go.mod.bak && cp go.sum go.sum.bak
|
||||||
go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2
|
go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1
|
||||||
|
|
||||||
# Run linting
|
# Run linting
|
||||||
echo "Looking for lint..."
|
echo "Looking for lint..."
|
||||||
|
|
@ -33,7 +33,7 @@ echo "Looking for lint..."
|
||||||
# Capture exit code to ensure go.{mod,sum} is restored before exiting
|
# Capture exit code to ensure go.{mod,sum} is restored before exiting
|
||||||
exit_code=0
|
exit_code=0
|
||||||
|
|
||||||
PATH="$PATH:$(go env GOPATH)/bin" golangci-lint run $args || exit_code=1
|
golangci-lint run $args || exit_code=1
|
||||||
|
|
||||||
# Restore go.{mod,sum}
|
# Restore go.{mod,sum}
|
||||||
mv go.mod.bak go.mod && mv go.sum.bak go.sum
|
mv go.mod.bak go.mod && mv go.sum.bak go.sum
|
||||||
|
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -42,7 +42,6 @@ type DeviceDatabase interface {
|
||||||
type AccountDatabase interface {
|
type AccountDatabase interface {
|
||||||
// Look up the account matching the given localpart.
|
// Look up the account matching the given localpart.
|
||||||
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
|
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
|
||||||
GetAccountByPassword(ctx context.Context, localpart, password string) (*api.Account, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// VerifyUserFromRequest authenticates the HTTP request,
|
// VerifyUserFromRequest authenticates the HTTP request,
|
||||||
|
|
@ -51,14 +50,14 @@ type AccountDatabase interface {
|
||||||
// Note: For an AS user, AS dummy device is returned.
|
// Note: For an AS user, AS dummy device is returned.
|
||||||
// On failure returns an JSON error response which can be sent to the client.
|
// On failure returns an JSON error response which can be sent to the client.
|
||||||
func VerifyUserFromRequest(
|
func VerifyUserFromRequest(
|
||||||
req *http.Request, userAPI api.QueryAcccessTokenAPI,
|
req *http.Request, userAPI api.UserInternalAPI,
|
||||||
) (*api.Device, *util.JSONResponse) {
|
) (*api.Device, *util.JSONResponse) {
|
||||||
// Try to find the Application Service user
|
// Try to find the Application Service user
|
||||||
token, err := ExtractAccessToken(req)
|
token, err := ExtractAccessToken(req)
|
||||||
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 +67,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 != nil {
|
||||||
if res.Err != "" {
|
if forbidden, ok := res.Err.(*api.ErrorForbidden); ok {
|
||||||
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(forbidden.Message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -10,5 +10,4 @@ const (
|
||||||
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
||||||
LoginTypeRecaptcha = "m.login.recaptcha"
|
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||||
LoginTypeApplicationService = "m.login.application_service"
|
LoginTypeApplicationService = "m.login.application_service"
|
||||||
LoginTypeToken = "m.login.token"
|
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ package authtypes
|
||||||
// Profile represents the profile for a Matrix account.
|
// Profile represents the profile for a Matrix account.
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
Localpart string `json:"local_part"`
|
Localpart string `json:"local_part"`
|
||||||
ServerName string `json:"server_name,omitempty"` // NOTSPEC: only set by Pinecone user provider
|
|
||||||
DisplayName string `json:"display_name"`
|
DisplayName string `json:"display_name"`
|
||||||
AvatarURL string `json:"avatar_url"`
|
AvatarURL string `json:"avatar_url"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,4 @@ package authtypes
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
// Copyright 2021 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 (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginFromJSONReader performs authentication given a login request body reader and
|
|
||||||
// some context. It returns the basic login information and a cleanup function to be
|
|
||||||
// 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
|
|
||||||
// is nil.
|
|
||||||
func LoginFromJSONReader(
|
|
||||||
req *http.Request,
|
|
||||||
useraccountAPI uapi.UserLoginAPI,
|
|
||||||
userAPI UserInternalAPIForLogin,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
|
||||||
reqBytes, err := io.ReadAll(req.Body)
|
|
||||||
if err != nil {
|
|
||||||
err := &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
|
||||||
}
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var header struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(reqBytes, &header); err != nil {
|
|
||||||
err := &util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON("Reading request body failed: " + err.Error()),
|
|
||||||
}
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var typ Type
|
|
||||||
switch header.Type {
|
|
||||||
case authtypes.LoginTypePassword:
|
|
||||||
typ = &LoginTypePassword{
|
|
||||||
GetAccountByPassword: useraccountAPI.QueryAccountByPassword,
|
|
||||||
Config: cfg,
|
|
||||||
}
|
|
||||||
case authtypes.LoginTypeToken:
|
|
||||||
typ = &LoginTypeToken{
|
|
||||||
UserAPI: userAPI,
|
|
||||||
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:
|
|
||||||
err := util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.InvalidParam("unhandled login type: " + header.Type),
|
|
||||||
}
|
|
||||||
return nil, nil, &err
|
|
||||||
}
|
|
||||||
|
|
||||||
return typ.LoginFromJSON(req.Context(), reqBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// UserInternalAPIForLogin contains the aspects of UserAPI required for logging in.
|
|
||||||
type UserInternalAPIForLogin interface {
|
|
||||||
uapi.LoginTokenInternalAPI
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -1,311 +0,0 @@
|
||||||
// Copyright 2021 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"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"reflect"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestLoginFromJSONReader(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tsts := []struct {
|
|
||||||
Name string
|
|
||||||
Body string
|
|
||||||
Token string
|
|
||||||
|
|
||||||
WantUsername string
|
|
||||||
WantDeviceID string
|
|
||||||
WantDeletedTokens []string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
Name: "passwordWorks",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.password",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "alice" },
|
|
||||||
"password": "herpassword",
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantUsername: "@alice:example.com",
|
|
||||||
WantDeviceID: "adevice",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "tokenWorks",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.token",
|
|
||||||
"token": "atoken",
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantUsername: "@auser:example.com",
|
|
||||||
WantDeviceID: "adevice",
|
|
||||||
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 {
|
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
|
||||||
var userAPI fakeUserInternalAPI
|
|
||||||
cfg := &config.ClientAPI{
|
|
||||||
Matrix: &config.Global{
|
|
||||||
SigningIdentity: fclient.SigningIdentity{
|
|
||||||
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))
|
|
||||||
if tst.Token != "" {
|
|
||||||
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})
|
|
||||||
|
|
||||||
if login.Username() != tst.WantUsername {
|
|
||||||
t.Errorf("Username: got %q, want %q", login.Username(), tst.WantUsername)
|
|
||||||
}
|
|
||||||
|
|
||||||
if login.DeviceID == nil {
|
|
||||||
if tst.WantDeviceID != "" {
|
|
||||||
t.Errorf("DeviceID: got %v, want %q", login.DeviceID, tst.WantDeviceID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if *login.DeviceID != tst.WantDeviceID {
|
|
||||||
t.Errorf("DeviceID: got %q, want %q", *login.DeviceID, tst.WantDeviceID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reflect.DeepEqual(userAPI.DeletedTokens, tst.WantDeletedTokens) {
|
|
||||||
t.Errorf("DeletedTokens: got %+v, want %+v", userAPI.DeletedTokens, tst.WantDeletedTokens)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBadLoginFromJSONReader(t *testing.T) {
|
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
tsts := []struct {
|
|
||||||
Name string
|
|
||||||
Body string
|
|
||||||
Token string
|
|
||||||
|
|
||||||
WantErrCode spec.MatrixErrorCode
|
|
||||||
}{
|
|
||||||
{Name: "empty", WantErrCode: spec.ErrorBadJSON},
|
|
||||||
{
|
|
||||||
Name: "badUnmarshal",
|
|
||||||
Body: `badsyntaxJSON`,
|
|
||||||
WantErrCode: spec.ErrorBadJSON,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badPassword",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.password",
|
|
||||||
"identifier": { "type": "m.id.user", "user": "alice" },
|
|
||||||
"password": "invalidpassword",
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: spec.ErrorForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badToken",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.token",
|
|
||||||
"token": "invalidtoken",
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: spec.ErrorForbidden,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "badType",
|
|
||||||
Body: `{
|
|
||||||
"type": "m.login.invalid",
|
|
||||||
"device_id": "adevice"
|
|
||||||
}`,
|
|
||||||
WantErrCode: spec.ErrorInvalidParam,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 {
|
|
||||||
t.Run(tst.Name, func(t *testing.T) {
|
|
||||||
var userAPI fakeUserInternalAPI
|
|
||||||
cfg := &config.ClientAPI{
|
|
||||||
Matrix: &config.Global{
|
|
||||||
SigningIdentity: fclient.SigningIdentity{
|
|
||||||
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))
|
|
||||||
if tst.Token != "" {
|
|
||||||
req.Header.Add("Authorization", "Bearer "+tst.Token)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, cleanup, errRes := LoginFromJSONReader(req, &userAPI, &userAPI, cfg)
|
|
||||||
if errRes == nil {
|
|
||||||
cleanup(ctx, nil)
|
|
||||||
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 {
|
|
||||||
t.Fatalf("LoginFromJSONReader err: got %+v, want code %q", errRes, tst.WantErrCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type fakeUserInternalAPI struct {
|
|
||||||
UserInternalAPIForLogin
|
|
||||||
DeletedTokens []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) QueryAccountByPassword(ctx context.Context, req *uapi.QueryAccountByPasswordRequest, res *uapi.QueryAccountByPasswordResponse) error {
|
|
||||||
if req.PlaintextPassword == "invalidpassword" {
|
|
||||||
res.Account = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
res.Exists = true
|
|
||||||
res.Account = &uapi.Account{UserID: userutil.MakeUserID(req.Localpart, req.ServerName)}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, req *uapi.PerformLoginTokenDeletionRequest, res *uapi.PerformLoginTokenDeletionResponse) error {
|
|
||||||
ua.DeletedTokens = append(ua.DeletedTokens, req.Token)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ua *fakeUserInternalAPI) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error {
|
|
||||||
if req.Token == "invalidtoken" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
res.Data = &uapi.LoginTokenData{UserID: "@auser:example.com"}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
// Copyright 2021 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"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
uapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
||||||
"github.com/matrix-org/util"
|
|
||||||
)
|
|
||||||
|
|
||||||
// LoginTypeToken describes how to authenticate with a login token.
|
|
||||||
type LoginTypeToken struct {
|
|
||||||
UserAPI uapi.LoginTokenInternalAPI
|
|
||||||
Config *config.ClientAPI
|
|
||||||
}
|
|
||||||
|
|
||||||
// Name implements Type.
|
|
||||||
func (t *LoginTypeToken) Name() string {
|
|
||||||
return authtypes.LoginTypeToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginFromJSON implements Type. The cleanup function deletes the token from
|
|
||||||
// the database on success.
|
|
||||||
func (t *LoginTypeToken) LoginFromJSON(ctx context.Context, reqBytes []byte) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
|
||||||
var r loginTokenRequest
|
|
||||||
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var res uapi.QueryLoginTokenResponse
|
|
||||||
if err := t.UserAPI.QueryLoginToken(ctx, &uapi.QueryLoginTokenRequest{Token: r.Token}, &res); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("UserAPI.QueryLoginToken failed")
|
|
||||||
return nil, nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if res.Data == nil {
|
|
||||||
return nil, nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("invalid login token"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
r.Login.Identifier.Type = "m.id.user"
|
|
||||||
r.Login.Identifier.User = res.Data.UserID
|
|
||||||
|
|
||||||
cleanup := func(ctx context.Context, authRes *util.JSONResponse) {
|
|
||||||
if authRes == nil {
|
|
||||||
util.GetLogger(ctx).Error("No JSONResponse provided to LoginTokenType cleanup function")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if authRes.Code == http.StatusOK {
|
|
||||||
var res uapi.PerformLoginTokenDeletionResponse
|
|
||||||
if err := t.UserAPI.PerformLoginTokenDeletion(ctx, &uapi.PerformLoginTokenDeletionRequest{Token: r.Token}, &res); err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("UserAPI.PerformLoginTokenDeletion failed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return &r.Login, cleanup, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// loginTokenRequest struct to hold the possible parameters from an HTTP request.
|
|
||||||
type loginTokenRequest struct {
|
|
||||||
Login
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
@ -17,18 +17,15 @@ package auth
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
||||||
"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 GetAccountByPassword func(ctx context.Context, localpart, password string) (*api.Account, error)
|
||||||
|
|
||||||
type PasswordRequest struct {
|
type PasswordRequest struct {
|
||||||
Login
|
Login
|
||||||
|
|
@ -42,21 +39,11 @@ type LoginTypePassword struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Name() string {
|
func (t *LoginTypePassword) Name() string {
|
||||||
return authtypes.LoginTypePassword
|
return "m.login.password"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) LoginFromJSON(ctx context.Context, reqBytes []byte) (*Login, LoginCleanupFunc, *util.JSONResponse) {
|
func (t *LoginTypePassword) Request() interface{} {
|
||||||
var r PasswordRequest
|
return &PasswordRequest{}
|
||||||
if err := httputil.UnmarshalJSON(reqBytes, &r); err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
login, err := t.Login(ctx, &r)
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return login, func(context.Context, *util.JSONResponse) {}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login, *util.JSONResponse) {
|
||||||
|
|
@ -65,67 +52,24 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
|
||||||
if 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(r.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."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
localpart, domain, err := userutil.ParseUsernameParam(username, 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) {
|
_, err = t.GetAccountByPassword(ctx, localpart, r.Password)
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusUnauthorized,
|
|
||||||
JSON: spec.InvalidUsername("The server name is not known."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Squash username to all lowercase letters
|
|
||||||
res := &api.QueryAccountByPasswordResponse{}
|
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
|
||||||
Localpart: strings.ToLower(localpart),
|
|
||||||
ServerName: domain,
|
|
||||||
PlaintextPassword: r.Password,
|
|
||||||
}, res)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("Unable to fetch account by password."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we couldn't find the user by the lower cased localpart, try the provided
|
|
||||||
// localpart as is.
|
|
||||||
if !res.Exists {
|
|
||||||
err = t.GetAccountByPassword(ctx, &api.QueryAccountByPasswordRequest{
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: domain,
|
|
||||||
PlaintextPassword: r.Password,
|
|
||||||
}, res)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &util.JSONResponse{
|
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.Unknown("Unable to fetch account by password."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
|
// 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.
|
// but that would leak the existence of the user.
|
||||||
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."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Set the user, so login.Username() can do the right thing
|
|
||||||
r.Identifier.User = res.Account.UserID
|
|
||||||
r.User = res.Account.UserID
|
|
||||||
return &r.Login, nil
|
return &r.Login, nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,11 +18,10 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"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"
|
||||||
|
|
@ -33,29 +32,27 @@ import (
|
||||||
type Type interface {
|
type Type interface {
|
||||||
// Name returns the name of the auth type e.g `m.login.password`
|
// Name returns the name of the auth type e.g `m.login.password`
|
||||||
Name() string
|
Name() string
|
||||||
|
// Request returns a pointer to a new request body struct to unmarshal into.
|
||||||
|
Request() interface{}
|
||||||
// Login with the auth type, returning an error response on failure.
|
// Login with the auth type, returning an error response on failure.
|
||||||
// Not all types support login, only m.login.password and m.login.token
|
// Not all types support login, only m.login.password and m.login.token
|
||||||
// See https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login
|
// See https://matrix.org/docs/spec/client_server/r0.6.1#post-matrix-client-r0-login
|
||||||
|
// `req` is guaranteed to be the type returned from Request()
|
||||||
// This function will be called when doing login and when doing 'sudo' style
|
// This function will be called when doing login and when doing 'sudo' style
|
||||||
// actions e.g deleting devices. The response must be a 401 as per:
|
// actions e.g deleting devices. The response must be a 401 as per:
|
||||||
// "If the homeserver decides that an attempt on a stage was unsuccessful, but the
|
// "If the homeserver decides that an attempt on a stage was unsuccessful, but the
|
||||||
// client may make a second attempt, it returns the same HTTP status 401 response as above,
|
// client may make a second attempt, it returns the same HTTP status 401 response as above,
|
||||||
// with the addition of the standard errcode and error fields describing the error."
|
// with the addition of the standard errcode and error fields describing the error."
|
||||||
//
|
Login(ctx context.Context, req interface{}) (login *Login, errRes *util.JSONResponse)
|
||||||
// The returned cleanup function must be non-nil on success, and will be called after
|
|
||||||
// authorization has been completed. Its argument is the final result of authorization.
|
|
||||||
LoginFromJSON(ctx context.Context, reqBytes []byte) (login *Login, cleanup LoginCleanupFunc, errRes *util.JSONResponse)
|
|
||||||
// TODO: Extend to support Register() flow
|
// TODO: Extend to support Register() flow
|
||||||
// Register(ctx context.Context, sessionID string, req interface{})
|
// Register(ctx context.Context, sessionID string, req interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
|
||||||
|
|
||||||
// LoginIdentifier represents identifier types
|
// LoginIdentifier represents identifier types
|
||||||
// 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"`
|
||||||
|
|
@ -64,8 +61,11 @@ type LoginIdentifier struct {
|
||||||
|
|
||||||
// Login represents the shared fields used in all forms of login/sudo endpoints.
|
// Login represents the shared fields used in all forms of login/sudo endpoints.
|
||||||
type Login struct {
|
type Login struct {
|
||||||
LoginIdentifier // Flat fields deprecated in favour of `identifier`.
|
Type string `json:"type"`
|
||||||
Identifier LoginIdentifier `json:"identifier"`
|
Identifier LoginIdentifier `json:"identifier"`
|
||||||
|
User string `json:"user"` // deprecated in favour of identifier
|
||||||
|
Medium string `json:"medium"` // deprecated in favour of identifier
|
||||||
|
Address string `json:"address"` // deprecated in favour of identifier
|
||||||
|
|
||||||
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
||||||
// Thus a pointer is needed to differentiate between the two
|
// Thus a pointer is needed to differentiate between the two
|
||||||
|
|
@ -103,7 +103,7 @@ type userInteractiveFlow struct {
|
||||||
// the user already has a valid access token, but we want to double-check
|
// the user already has a valid access token, but we want to double-check
|
||||||
// that it isn't stolen by re-authenticating them.
|
// that it isn't stolen by re-authenticating them.
|
||||||
type UserInteractive struct {
|
type UserInteractive struct {
|
||||||
sync.RWMutex
|
Completed []string
|
||||||
Flows []userInteractiveFlow
|
Flows []userInteractiveFlow
|
||||||
// Map of login type to implementation
|
// Map of login type to implementation
|
||||||
Types map[string]Type
|
Types map[string]Type
|
||||||
|
|
@ -111,12 +111,14 @@ type UserInteractive struct {
|
||||||
Sessions map[string][]string
|
Sessions map[string][]string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
func NewUserInteractive(getAccByPass GetAccountByPassword, cfg *config.ClientAPI) *UserInteractive {
|
||||||
typePassword := &LoginTypePassword{
|
typePassword := &LoginTypePassword{
|
||||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
GetAccountByPassword: getAccByPass,
|
||||||
Config: cfg,
|
Config: cfg,
|
||||||
}
|
}
|
||||||
|
// TODO: Add SSO login
|
||||||
return &UserInteractive{
|
return &UserInteractive{
|
||||||
|
Completed: []string{},
|
||||||
Flows: []userInteractiveFlow{
|
Flows: []userInteractiveFlow{
|
||||||
{
|
{
|
||||||
Stages: []string{typePassword.Name()},
|
Stages: []string{typePassword.Name()},
|
||||||
|
|
@ -130,8 +132,6 @@ func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||||
u.RLock()
|
|
||||||
defer u.RUnlock()
|
|
||||||
for _, f := range u.Flows {
|
for _, f := range u.Flows {
|
||||||
if len(f.Stages) == 1 && f.Stages[0] == authType {
|
if len(f.Stages) == 1 && f.Stages[0] == authType {
|
||||||
return true
|
return true
|
||||||
|
|
@ -141,34 +141,26 @@ func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
||||||
u.Lock()
|
|
||||||
// TODO: Handle multi-stage flows
|
// TODO: Handle multi-stage flows
|
||||||
|
u.Completed = append(u.Completed, authType)
|
||||||
delete(u.Sessions, sessionID)
|
delete(u.Sessions, sessionID)
|
||||||
u.Unlock()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Challenge struct {
|
// Challenge returns an HTTP 401 with the supported flows for authenticating
|
||||||
|
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: 401,
|
||||||
|
JSON: struct {
|
||||||
Completed []string `json:"completed"`
|
Completed []string `json:"completed"`
|
||||||
Flows []userInteractiveFlow `json:"flows"`
|
Flows []userInteractiveFlow `json:"flows"`
|
||||||
Session string `json:"session"`
|
Session string `json:"session"`
|
||||||
// TODO: Return any additional `params`
|
// TODO: Return any additional `params`
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
}
|
}{
|
||||||
|
u.Completed,
|
||||||
// Challenge returns an HTTP 401 with the supported flows for authenticating
|
u.Flows,
|
||||||
func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse {
|
sessionID,
|
||||||
u.RLock()
|
make(map[string]interface{}),
|
||||||
completed := u.Sessions[sessionID]
|
|
||||||
flows := u.Flows
|
|
||||||
u.RUnlock()
|
|
||||||
|
|
||||||
return &util.JSONResponse{
|
|
||||||
Code: 401,
|
|
||||||
JSON: Challenge{
|
|
||||||
Completed: completed,
|
|
||||||
Flows: flows,
|
|
||||||
Session: sessionID,
|
|
||||||
Params: make(map[string]interface{}),
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -178,15 +170,11 @@ 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.Sessions[sessionID] = []string{}
|
u.Sessions[sessionID] = []string{}
|
||||||
u.Unlock()
|
return u.Challenge(sessionID)
|
||||||
return u.challenge(sessionID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResponseWithChallenge mixes together a JSON body (e.g an error with errcode/message) with the
|
// ResponseWithChallenge mixes together a JSON body (e.g an error with errcode/message) with the
|
||||||
|
|
@ -195,19 +183,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)
|
||||||
|
|
||||||
|
|
@ -232,42 +216,38 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
|
|
||||||
// extract the type so we know which login type to use
|
// extract the type so we know which login type to use
|
||||||
authType := gjson.GetBytes(bodyBytes, "auth.type").Str
|
authType := gjson.GetBytes(bodyBytes, "auth.type").Str
|
||||||
|
|
||||||
u.RLock()
|
|
||||||
loginType, ok := u.Types[authType]
|
loginType, ok := u.Types[authType]
|
||||||
u.RUnlock()
|
|
||||||
|
|
||||||
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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieve the session
|
// retrieve the session
|
||||||
sessionID := gjson.GetBytes(bodyBytes, "auth.session").Str
|
sessionID := gjson.GetBytes(bodyBytes, "auth.session").Str
|
||||||
|
if _, ok = u.Sessions[sessionID]; !ok {
|
||||||
u.RLock()
|
|
||||||
_, ok = u.Sessions[sessionID]
|
|
||||||
u.RUnlock()
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
// if the login type is part of a single stage flow then allow them to omit the session ID
|
// if the login type is part of a single stage flow then allow them to omit the session ID
|
||||||
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."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
login, cleanup, resErr := loginType.LoginFromJSON(ctx, []byte(gjson.GetBytes(bodyBytes, "auth").Raw))
|
r := loginType.Request()
|
||||||
if resErr != nil {
|
if err := json.Unmarshal([]byte(gjson.GetBytes(bodyBytes, "auth").Raw), r); err != nil {
|
||||||
return nil, u.ResponseWithChallenge(sessionID, resErr.JSON)
|
return nil, &util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
login, resErr := loginType.Login(ctx, r)
|
||||||
|
if resErr == nil {
|
||||||
u.AddCompletedStage(sessionID, authType)
|
u.AddCompletedStage(sessionID, authType)
|
||||||
cleanup(ctx, nil)
|
|
||||||
// TODO: Check if there's more stages to go and return an error
|
// TODO: Check if there's more stages to go and return an error
|
||||||
return login, nil
|
return login, nil
|
||||||
|
}
|
||||||
|
return nil, u.ResponseWithChallenge(sessionID, resErr.JSON)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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{
|
||||||
|
|
@ -25,35 +24,21 @@ var (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
type fakeAccountDatabase struct{}
|
func getAccountByPassword(ctx context.Context, localpart, plaintextPassword string) (*api.Account, error) {
|
||||||
|
acc, ok := lookup[localpart+" "+plaintextPassword]
|
||||||
func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) PerformAccountDeactivation(ctx context.Context, req *api.PerformAccountDeactivationRequest, res *api.PerformAccountDeactivationResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *fakeAccountDatabase) QueryAccountByPassword(ctx context.Context, req *api.QueryAccountByPasswordRequest, res *api.QueryAccountByPasswordResponse) error {
|
|
||||||
acc, ok := lookup[req.Localpart+" "+req.PlaintextPassword]
|
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unknown user/password")
|
return nil, fmt.Errorf("unknown user/password")
|
||||||
}
|
}
|
||||||
res.Account = acc
|
return acc, nil
|
||||||
res.Exists = true
|
|
||||||
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(getAccountByPassword, cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserInteractiveChallenge(t *testing.T) {
|
func TestUserInteractiveChallenge(t *testing.T) {
|
||||||
|
|
@ -190,38 +175,3 @@ func TestUserInteractivePasswordBadLogin(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUserInteractive_AddCompletedStage(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
sessionID string
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "first user",
|
|
||||||
sessionID: util.RandomString(8),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "second user",
|
|
||||||
sessionID: util.RandomString(8),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "third user",
|
|
||||||
sessionID: util.RandomString(8),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
u := setup()
|
|
||||||
ctx := context.Background()
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
_, resp := u.Verify(ctx, []byte("{}"), nil)
|
|
||||||
challenge, ok := resp.JSON.(Challenge)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("expected a Challenge, got %T", resp.JSON)
|
|
||||||
}
|
|
||||||
if len(challenge.Completed) > 0 {
|
|
||||||
t.Fatalf("expected 0 completed stages, got %d", len(challenge.Completed))
|
|
||||||
}
|
|
||||||
u.AddCompletedStage(tt.sessionID, "")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -15,54 +15,50 @@
|
||||||
package clientapi
|
package clientapi
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/gorilla/mux"
|
||||||
"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"
|
eduServerAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||||
|
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/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/jetstream"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
"github.com/matrix-org/dendrite/setup/kafka"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
|
"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,
|
router *mux.Router,
|
||||||
routers httputil.Routers,
|
synapseAdminRouter *mux.Router,
|
||||||
cfg *config.Dendrite,
|
cfg *config.ClientAPI,
|
||||||
natsInstance *jetstream.NATSInstance,
|
accountsDB accounts.Database,
|
||||||
federation fclient.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
eduInputAPI eduServerAPI.EDUServerInputAPI,
|
||||||
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
transactionsCache *transactions.Cache,
|
transactionsCache *transactions.Cache,
|
||||||
fsAPI federationAPI.ClientFederationAPI,
|
fsAPI federationSenderAPI.FederationSenderInternalAPI,
|
||||||
userAPI userapi.ClientUserAPI,
|
userAPI userapi.UserInternalAPI,
|
||||||
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
keyAPI keyserverAPI.KeyInternalAPI,
|
||||||
extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
|
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||||
|
mscCfg *config.MSCs,
|
||||||
) {
|
) {
|
||||||
js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
_, producer := kafka.SetupConsumerProducer(&cfg.Matrix.Kafka)
|
||||||
|
|
||||||
syncProducer := &producers.SyncAPIProducer{
|
syncProducer := &producers.SyncAPIProducer{
|
||||||
JetStream: js,
|
Producer: producer,
|
||||||
TopicReceiptEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
Topic: cfg.Matrix.Kafka.TopicFor(config.TopicOutputClientData),
|
||||||
TopicSendToDeviceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputSendToDeviceEvent),
|
|
||||||
TopicTypingEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputTypingEvent),
|
|
||||||
TopicPresenceEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputPresenceEvent),
|
|
||||||
UserAPI: userAPI,
|
|
||||||
ServerName: cfg.Global.ServerName,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
routers,
|
router, synapseAdminRouter, cfg, eduInputAPI, rsAPI, asAPI,
|
||||||
cfg, rsAPI, asAPI,
|
accountsDB, userAPI, federation,
|
||||||
userAPI, userDirectoryProvider, federation,
|
syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
|
||||||
syncProducer, transactionsCache, fsAPI,
|
|
||||||
extRoomsProvider, natsClient, enableMetrics,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -16,11 +16,11 @@ package httputil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -29,23 +29,17 @@ import (
|
||||||
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
|
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
|
||||||
// encoding/json allows invalid utf-8, matrix does not
|
// encoding/json allows invalid utf-8, matrix does not
|
||||||
// https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
|
// https://matrix.org/docs/spec/client_server/r0.6.1#api-standards
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := ioutil.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("ioutil.ReadAll failed")
|
||||||
return &util.JSONResponse{
|
resp := jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
return &resp
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return UnmarshalJSON(body, iface)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 +49,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
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ func ParseTSParam(req *http.Request) (time.Time, error) {
|
||||||
// The parameter exists, parse into a Time object
|
// The parameter exists, parse into a Time object
|
||||||
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return time.Time{}, fmt.Errorf("param 'ts' is no valid int (%s)", err.Error())
|
return time.Time{}, fmt.Errorf("Param 'ts' is no valid int (%s)", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
return time.UnixMilli(ts), nil
|
return time.Unix(ts/1000, 0), nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
195
clientapi/jsonerror/jsonerror.go
Normal file
195
clientapi/jsonerror/jsonerror.go
Normal file
|
|
@ -0,0 +1,195 @@
|
||||||
|
// 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 (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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}
|
||||||
|
}
|
||||||
|
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,148 +15,41 @@
|
||||||
package producers
|
package producers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/Shopify/sarama"
|
||||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||||
"github.com/nats-io/nats.go"
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
|
||||||
"github.com/matrix-org/dendrite/syncapi/types"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// SyncAPIProducer produces events for the sync API server to consume
|
// SyncAPIProducer produces events for the sync API server to consume
|
||||||
type SyncAPIProducer struct {
|
type SyncAPIProducer struct {
|
||||||
TopicReceiptEvent string
|
Topic string
|
||||||
TopicSendToDeviceEvent string
|
Producer sarama.SyncProducer
|
||||||
TopicTypingEvent string
|
|
||||||
TopicPresenceEvent string
|
|
||||||
JetStream nats.JetStreamContext
|
|
||||||
ServerName spec.ServerName
|
|
||||||
UserAPI userapi.ClientUserAPI
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendReceipt(
|
// SendData sends account data to the sync API server
|
||||||
ctx context.Context,
|
func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string) error {
|
||||||
userID, roomID, eventID, receiptType string, timestamp spec.Timestamp,
|
var m sarama.ProducerMessage
|
||||||
) error {
|
|
||||||
m := &nats.Msg{
|
data := eventutil.AccountData{
|
||||||
Subject: p.TopicReceiptEvent,
|
RoomID: roomID,
|
||||||
Header: nats.Header{},
|
Type: dataType,
|
||||||
}
|
}
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
value, err := json.Marshal(data)
|
||||||
m.Header.Set(jetstream.RoomID, roomID)
|
|
||||||
m.Header.Set(jetstream.EventID, eventID)
|
|
||||||
m.Header.Set("type", receiptType)
|
|
||||||
m.Header.Set("timestamp", fmt.Sprintf("%d", timestamp))
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{}).Tracef("Producing to topic '%s'", p.TopicReceiptEvent)
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendToDevice(
|
|
||||||
ctx context.Context, sender, userID, deviceID, eventType string,
|
|
||||||
message json.RawMessage,
|
|
||||||
) error {
|
|
||||||
devices := []string{}
|
|
||||||
_, domain, err := gomatrixserverlib.SplitID('@', userID)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the event is targeted locally then we want to expand the wildcard
|
m.Topic = string(p.Topic)
|
||||||
// out into individual device IDs so that we can send them to each respective
|
m.Key = sarama.StringEncoder(userID)
|
||||||
// device. If the event isn't targeted locally then we can't expand the
|
m.Value = sarama.ByteEncoder(value)
|
||||||
// wildcard as we don't know about the remote devices, so instead we leave it
|
|
||||||
// as-is, so that the federation sender can send it on with the wildcard intact.
|
|
||||||
if domain == p.ServerName && deviceID == "*" {
|
|
||||||
var res userapi.QueryDevicesResponse
|
|
||||||
err = p.UserAPI.QueryDevices(context.TODO(), &userapi.QueryDevicesRequest{
|
|
||||||
UserID: userID,
|
|
||||||
}, &res)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, dev := range res.Devices {
|
|
||||||
devices = append(devices, dev.ID)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
devices = append(devices, deviceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.WithFields(log.Fields{
|
log.WithFields(log.Fields{
|
||||||
"user_id": userID,
|
"user_id": userID,
|
||||||
"num_devices": len(devices),
|
"room_id": roomID,
|
||||||
"type": eventType,
|
"data_type": dataType,
|
||||||
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
}).Infof("Producing to topic '%s'", p.Topic)
|
||||||
for i, device := range devices {
|
|
||||||
ote := &types.OutputSendToDeviceEvent{
|
|
||||||
UserID: userID,
|
|
||||||
DeviceID: device,
|
|
||||||
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
|
||||||
Sender: sender,
|
|
||||||
Type: eventType,
|
|
||||||
Content: message,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
eventJSON, err := json.Marshal(ote)
|
_, _, err = p.Producer.SendMessage(&m)
|
||||||
if err != nil {
|
|
||||||
log.WithError(err).Error("sendToDevice failed json.Marshal")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
m := nats.NewMsg(p.TopicSendToDeviceEvent)
|
|
||||||
m.Data = eventJSON
|
|
||||||
m.Header.Set("sender", sender)
|
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
|
||||||
|
|
||||||
if _, err = p.JetStream.PublishMsg(m, nats.Context(ctx)); err != nil {
|
|
||||||
if i < len(devices)-1 {
|
|
||||||
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 nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendTyping(
|
|
||||||
ctx context.Context, userID, roomID string, typing bool, timeoutMS int64,
|
|
||||||
) error {
|
|
||||||
m := &nats.Msg{
|
|
||||||
Subject: p.TopicTypingEvent,
|
|
||||||
Header: nats.Header{},
|
|
||||||
}
|
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
|
||||||
m.Header.Set(jetstream.RoomID, roomID)
|
|
||||||
m.Header.Set("typing", strconv.FormatBool(typing))
|
|
||||||
m.Header.Set("timeout_ms", strconv.Itoa(int(timeoutMS)))
|
|
||||||
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *SyncAPIProducer) SendPresence(
|
|
||||||
ctx context.Context, userID string, presence types.Presence, statusMsg *string,
|
|
||||||
) error {
|
|
||||||
m := nats.NewMsg(p.TopicPresenceEvent)
|
|
||||||
m.Header.Set(jetstream.UserID, userID)
|
|
||||||
m.Header.Set("presence", presence.String())
|
|
||||||
if statusMsg != nil {
|
|
||||||
m.Header.Set("status_msg", *statusMsg)
|
|
||||||
}
|
|
||||||
|
|
||||||
m.Header.Set("last_active_ts", strconv.Itoa(int(spec.AsTimestamp(time.Now()))))
|
|
||||||
|
|
||||||
_, err := p.JetStream.PublishMsg(m, nats.Context(ctx))
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,28 +17,28 @@ package routing
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io/ioutil"
|
||||||
"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"
|
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type}
|
// GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type}
|
||||||
func GetAccountData(
|
func GetAccountData(
|
||||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
||||||
userID string, roomID string, dataType string,
|
userID string, roomID string, dataType string,
|
||||||
) 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -69,19 +69,19 @@ 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type}
|
// SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type}
|
||||||
func SaveAccountData(
|
func SaveAccountData(
|
||||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
||||||
userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer,
|
userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer,
|
||||||
) 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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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" {
|
||||||
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("Unable to set read marker"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
body, err := io.ReadAll(req.Body)
|
body, err := ioutil.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("ioutil.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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,12 +126,23 @@ func SaveAccountData(
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: user API should do this since it's account data
|
||||||
|
if err := syncProducer.SendData(userID, roomID, dataType); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type readMarkerJSON struct {
|
||||||
|
FullyRead string `json:"m.fully_read"`
|
||||||
|
Read string `json:"m.read"`
|
||||||
|
}
|
||||||
|
|
||||||
type fullyReadEvent struct {
|
type fullyReadEvent struct {
|
||||||
EventID string `json:"event_id"`
|
EventID string `json:"event_id"`
|
||||||
}
|
}
|
||||||
|
|
@ -142,36 +150,31 @@ type fullyReadEvent struct {
|
||||||
// SaveReadMarker implements POST /rooms/{roomId}/read_markers
|
// SaveReadMarker implements POST /rooms/{roomId}/read_markers
|
||||||
func SaveReadMarker(
|
func SaveReadMarker(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
userAPI api.UserInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI, eduAPI eduserverAPI.EDUServerInputAPI,
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
var r eventutil.ReadMarkerJSON
|
var r readMarkerJSON
|
||||||
resErr = httputil.UnmarshalJSONRequest(req, &r)
|
resErr = httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
|
|
||||||
if r.FullyRead != "" {
|
if r.FullyRead == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("Missing m.fully_read mandatory field"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.JSONResponse{
|
return jsonerror.InternalServerError()
|
||||||
Code: http.StatusInternalServerError,
|
|
||||||
JSON: spec.InternalServerError{},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dataReq := api.InputAccountDataRequest{
|
dataReq := api.InputAccountDataRequest{
|
||||||
|
|
@ -185,14 +188,15 @@ func SaveReadMarker(
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := syncProducer.SendData(device.UserID, roomID, "m.fully_read"); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("syncProducer.SendData failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle the read receipts that may be included in the read marker.
|
// 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, eduAPI, 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,587 +0,0 @@
|
||||||
package routing
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
"github.com/matrix-org/dendrite/internal"
|
|
||||||
"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"
|
|
||||||
roomserverAPI "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/userapi/api"
|
|
||||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
var validRegistrationTokenRegex = regexp.MustCompile("^[[:ascii:][:digit:]_]*$")
|
|
||||||
|
|
||||||
func AdminCreateNewRegistrationToken(req *http.Request, cfg *config.ClientAPI, userAPI userapi.ClientUserAPI) util.JSONResponse {
|
|
||||||
if !cfg.RegistrationRequiresToken {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: spec.Forbidden("Registration via tokens is not enabled on this homeserver"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
request := struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
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 {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.BadJSON(fmt.Sprintf("Failed to decode request body: %s", err)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 userapi.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 == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.MissingParam("Expecting non-empty password."),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = internal.ValidatePassword(request.Password); err != nil {
|
|
||||||
return *internal.PasswordResponse(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
updateReq := &api.PerformPasswordUpdateRequest{
|
|
||||||
Localpart: localpart,
|
|
||||||
ServerName: serverName,
|
|
||||||
Password: request.Password,
|
|
||||||
LogoutDevices: request.LogoutDevices,
|
|
||||||
}
|
|
||||||
updateRes := &api.PerformPasswordUpdateResponse{}
|
|
||||||
if err := userAPI.PerformPasswordUpdate(req.Context(), updateReq, updateRes); err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: spec.Unknown("Failed to perform password update: " + err.Error()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusOK,
|
|
||||||
JSON: struct {
|
|
||||||
Updated bool `json:"password_updated"`
|
|
||||||
}{
|
|
||||||
Updated: updateRes.PasswordUpdated,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 userapi.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"
|
||||||
)
|
)
|
||||||
|
|
@ -44,14 +44,14 @@ type connectionInfo struct {
|
||||||
|
|
||||||
// GetAdminWhois implements GET /admin/whois/{userId}
|
// GetAdminWhois implements GET /admin/whois/{userId}
|
||||||
func GetAdminWhois(
|
func GetAdminWhois(
|
||||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
||||||
userID string,
|
userID string,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
allowed := device.AccountType == api.AccountTypeAdmin || userID == device.UserID
|
if userID != device.UserID {
|
||||||
if !allowed {
|
// TODO: Still allow if user is admin
|
||||||
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,23 +15,23 @@
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
||||||
func GetAliases(
|
func GetAliases(
|
||||||
req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string,
|
req *http.Request, rsAPI api.RoomserverInternalAPI, 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{
|
||||||
|
|
@ -44,40 +44,29 @@ func GetAliases(
|
||||||
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
visibility := "invite"
|
||||||
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 {
|
|
||||||
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"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if visibility != gomatrixserverlib.WorldReadable {
|
||||||
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
|
||||||
|
if authType == authtypes.LoginTypeRecaptcha {
|
||||||
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
serveRecaptcha()
|
serveRecaptcha()
|
||||||
return
|
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
|
||||||
|
if authType == authtypes.LoginTypeRecaptcha {
|
||||||
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
clientIP := req.RemoteAddr
|
clientIP := req.RemoteAddr
|
||||||
err := req.ParseForm()
|
err := req.ParseForm()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
||||||
w.WriteHeader(http.StatusBadRequest)
|
res := jsonerror.InternalServerError()
|
||||||
serveRecaptcha()
|
return &res
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := req.Form.Get(cfg.RecaptchaFormField)
|
response := req.Form.Get("g-recaptcha-response")
|
||||||
err = validateRecaptcha(cfg, response, clientIP)
|
if err := validateRecaptcha(cfg, response, clientIP); err != nil {
|
||||||
switch err {
|
util.GetLogger(req.Context()).Error(err)
|
||||||
case ErrMissingResponse:
|
return err
|
||||||
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
|
// Success. Add recaptcha as a completed login flow
|
||||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||||
|
|
||||||
serveSuccess()
|
serveSuccess()
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
writeHTTPMessage(w, req, "Bad method", http.StatusMethodNotAllowed)
|
|
||||||
|
return &util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.RoomserverInternalAPI,
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
package routing
|
package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -26,10 +25,12 @@ 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/dendrite/userapi/storage/accounts"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
|
@ -43,13 +44,26 @@ type createRoomRequest struct {
|
||||||
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"`
|
||||||
|
GuestCanJoin bool `json:"guest_can_join"`
|
||||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
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 +71,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 +104,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 +113,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,131 +126,450 @@ 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,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
accountDB accounts.Database, rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
var createRequest createRoomRequest
|
// TODO (#267): Check room ID doesn't clash with an existing one, and we
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &createRequest)
|
// probably shouldn't be using pseudo-random strings, maybe GUIDs?
|
||||||
|
roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName)
|
||||||
|
return createRoom(req, device, cfg, roomID, accountDB, rsAPI, asAPI)
|
||||||
|
}
|
||||||
|
|
||||||
|
// createRoom implements /createRoom
|
||||||
|
// nolint: gocyclo
|
||||||
|
func createRoom(
|
||||||
|
req *http.Request, device *api.Device,
|
||||||
|
cfg *config.ClientAPI, roomID string,
|
||||||
|
accountDB accounts.Database, rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
logger := util.GetLogger(req.Context())
|
||||||
|
userID := device.UserID
|
||||||
|
var r createRoomRequest
|
||||||
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
return *resErr
|
return *resErr
|
||||||
}
|
}
|
||||||
if resErr = createRequest.Validate(); resErr != nil {
|
// TODO: apply rate-limit
|
||||||
|
|
||||||
|
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// createRoom implements /createRoom
|
|
||||||
func createRoom(
|
|
||||||
ctx context.Context,
|
|
||||||
createRequest createRoomRequest, device *api.Device,
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
||||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
|
||||||
evTime time.Time,
|
|
||||||
) util.JSONResponse {
|
|
||||||
userID, err := spec.NewUserID(device.UserID, true)
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(ctx).WithError(err).Error("invalid userID")
|
|
||||||
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)
|
|
||||||
|
|
||||||
// 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(req.Context(), userID, asAPI, accountDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
util.GetLogger(req.Context()).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
createContent := map[string]interface{}{}
|
||||||
|
if len(r.CreationContent) > 0 {
|
||||||
|
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
||||||
|
util.GetLogger(req.Context()).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(req.Context()).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"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Preset {
|
||||||
|
case presetPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
case presetTrustedPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
// TODO If trusted_private_chat, all invitees are given the same power level as the room creator.
|
||||||
|
case presetPublicChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Public
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
}
|
||||||
|
|
||||||
|
createEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomCreate,
|
||||||
|
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(req.Context(), &hasAliasReq, &aliasResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).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(req.Context()).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(req.Context()).WithError(err).Error("buildEvent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = gomatrixserverlib.Allowed(ev, &authEvents); err != nil {
|
||||||
|
util.GetLogger(req.Context()).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(req.Context()).WithError(err).Error("authEvents.AddEvent failed")
|
||||||
|
return jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
accumulated := gomatrixserverlib.UnwrapEventHeaders(builtEvents)
|
||||||
|
if err = roomserverAPI.SendEventWithState(
|
||||||
|
req.Context(),
|
||||||
|
rsAPI,
|
||||||
|
roomserverAPI.KindNew,
|
||||||
|
&gomatrixserverlib.RespState{
|
||||||
|
StateEvents: accumulated,
|
||||||
|
AuthEvents: accumulated,
|
||||||
|
},
|
||||||
|
ev.Headered(roomVersion),
|
||||||
|
nil,
|
||||||
|
); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("SendEventWithState 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(req.Context(), &aliasReq, &aliasResp)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).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 {
|
||||||
|
switch event.Type() {
|
||||||
|
case gomatrixserverlib.MRoomName:
|
||||||
|
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(
|
||||||
|
req.Context(), invitee, "", accountDB, device, gomatrixserverlib.Invite,
|
||||||
|
roomID, true, cfg, evTime, rsAPI, asAPI,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("buildMembershipEvent failed")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inviteStrippedState := append(
|
||||||
|
globalStrippedState,
|
||||||
|
gomatrixserverlib.NewInviteV2StrippedState(inviteEvent.Event),
|
||||||
|
)
|
||||||
|
// Send the invite event to the roomserver.
|
||||||
|
err = roomserverAPI.SendInvite(
|
||||||
|
req.Context(),
|
||||||
|
rsAPI,
|
||||||
|
inviteEvent.Headered(roomVersion),
|
||||||
|
inviteStrippedState, // invite room state
|
||||||
|
cfg.Matrix.ServerName, // send as server
|
||||||
|
nil, // transaction ID
|
||||||
|
)
|
||||||
|
switch e := err.(type) {
|
||||||
|
case *roomserverAPI.PerformError:
|
||||||
|
return e.JSONResponse()
|
||||||
|
case nil:
|
||||||
|
default:
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInvite failed")
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusInternalServerError,
|
Code: http.StatusInternalServerError,
|
||||||
JSON: spec.InternalServerError{},
|
JSON: jsonerror.InternalServerError(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userDisplayName := profile.DisplayName
|
if r.Visibility == "public" {
|
||||||
userAvatarURL := profile.AvatarURL
|
// expose this room in the published room list
|
||||||
|
var pubRes roomserverAPI.PerformPublishResponse
|
||||||
keyID := cfg.Matrix.KeyID
|
rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||||
privateKey := cfg.Matrix.PrivateKey
|
RoomID: roomID,
|
||||||
|
Visibility: "public",
|
||||||
req := roomserverAPI.PerformCreateRoomRequest{
|
}, &pubRes)
|
||||||
InvitedUsers: createRequest.Invite,
|
if pubRes.Error != nil {
|
||||||
RoomName: createRequest.Name,
|
// treat as non-fatal since the room is already made by this point
|
||||||
Visibility: createRequest.Visibility,
|
util.GetLogger(req.Context()).WithError(pubRes.Error).Error("failed to visibility:public")
|
||||||
Topic: createRequest.Topic,
|
|
||||||
StatePreset: createRequest.Preset,
|
|
||||||
CreationContent: createRequest.CreationContent,
|
|
||||||
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)
|
|
||||||
if createRes != nil {
|
|
||||||
return *createRes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
response := createRoomResponse{
|
response := createRoomResponse{
|
||||||
RoomID: roomID.String(),
|
RoomID: roomID,
|
||||||
RoomAlias: roomAlias,
|
RoomAlias: roomAlias,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,3 +578,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
|
||||||
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue