mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-06 14:33:10 -06:00
Compare commits
No commits in common. "main" and "v0.5.0rc1" have entirely different histories.
|
|
@ -1,2 +1,3 @@
|
|||
bin
|
||||
*.wasm
|
||||
*.wasm
|
||||
.git
|
||||
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
|
@ -1 +0,0 @@
|
|||
* @matrix-org/dendrite-core
|
||||
62
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
62
.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
|
||||
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/
|
||||
-->
|
||||
<!-- All bug reports must provide the following 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**:
|
||||
- **SQLite3 or Postgres?**:
|
||||
- **Running in Docker?**:
|
||||
- **`go version`**:
|
||||
- **Client used (if applicable)**:
|
||||
|
||||
- **Dendrite version or git SHA**:
|
||||
- **Monolith or Polylith?**:
|
||||
- **SQLite3 or Postgres?**:
|
||||
- **Running in Docker?**:
|
||||
- **`go version`**:
|
||||
|
||||
<!--
|
||||
|
||||
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
|
||||
|
||||
- **What** is the problem:
|
||||
- **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.
|
||||
-->
|
||||
<!-- Describe here the problem that you are experiencing -->
|
||||
|
||||
### Steps to reproduce
|
||||
<!-- Please try reproducing this bug before submitting it. Issues which cannot be reproduced risk being closed. -->
|
||||
|
||||
- list the steps
|
||||
- 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
|
||||
``` (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!
|
||||
-->
|
||||
|
|
|
|||
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
6
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -1,8 +1,8 @@
|
|||
### 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
|
||||
* [ ] 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
|
||||
* [ ] 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](https://github.com/matrix-org/dendrite/blob/master/docs/CONTRIBUTING.md#sign-off)
|
||||
|
||||
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
|
||||
23
.gitignore
vendored
23
.gitignore
vendored
|
|
@ -23,7 +23,6 @@
|
|||
/vendor/bin
|
||||
/docker/build
|
||||
/logs
|
||||
/jetstream
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
|
|
@ -41,10 +40,6 @@ _testmain.go
|
|||
*.test
|
||||
*.prof
|
||||
*.wasm
|
||||
*.aar
|
||||
*.jar
|
||||
*.framework
|
||||
*.xcframework
|
||||
|
||||
# Generated keys
|
||||
*.pem
|
||||
|
|
@ -56,28 +51,12 @@ dendrite.yaml
|
|||
|
||||
# Database files
|
||||
*.db
|
||||
*.db-journal
|
||||
|
||||
# Log files
|
||||
*.log*
|
||||
*.log*
|
||||
|
||||
# Generated code
|
||||
cmd/dendrite-demo-yggdrasil/embed/fs*.go
|
||||
|
||||
# Test dependencies
|
||||
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
|
||||
|
||||
# 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
|
||||
issues-exit-code: 1
|
||||
|
|
@ -18,6 +18,24 @@ run:
|
|||
#build-tags:
|
||||
# - 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":
|
||||
# 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
|
||||
|
|
@ -32,8 +50,7 @@ run:
|
|||
# output configuration options
|
||||
output:
|
||||
# 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-issued-lines: true
|
||||
|
|
@ -62,8 +79,9 @@ linters-settings:
|
|||
# see https://github.com/kisielk/errcheck#excluding-functions for details
|
||||
#exclude: /path/to/file.txt
|
||||
govet:
|
||||
enable:
|
||||
- shadow
|
||||
# report about shadowed variables
|
||||
check-shadowing: true
|
||||
|
||||
# settings per analyzer
|
||||
settings:
|
||||
printf: # analyzer name, run `go tool vet help` to see all analyzers
|
||||
|
|
@ -161,7 +179,9 @@ linters-settings:
|
|||
|
||||
linters:
|
||||
enable:
|
||||
- deadcode
|
||||
- errcheck
|
||||
- goconst
|
||||
- gocyclo
|
||||
- goimports # Does everything gofmt does
|
||||
- gosimple
|
||||
|
|
@ -171,8 +191,10 @@ linters:
|
|||
- misspell # Check code comments, whereas misspell in CI checks *.md files
|
||||
- nakedret
|
||||
- staticcheck
|
||||
- structcheck
|
||||
- unparam
|
||||
- unused
|
||||
- varcheck
|
||||
enable-all: false
|
||||
disable:
|
||||
- bodyclose
|
||||
|
|
@ -192,31 +214,12 @@ linters:
|
|||
- stylecheck
|
||||
- typecheck # Should turn back on soon
|
||||
- unconvert # Should turn back on soon
|
||||
- goconst # Slightly annoying, as it reports "issues" in SQL statements
|
||||
disable-all: false
|
||||
presets:
|
||||
fast: false
|
||||
|
||||
|
||||
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.
|
||||
# But independently from this option we use default exclude patterns,
|
||||
# it can be disabled by `exclude-use-default: false`. To list all
|
||||
|
|
|
|||
1020
CHANGES.md
1020
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
|
||||
|
||||
149
README.md
149
README.md
|
|
@ -1,37 +1,27 @@
|
|||
# Dendrite
|
||||
|
||||
## 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 [](https://buildkite.com/matrix-dot-org/dendrite) [](https://matrix.to/#/#dendrite:matrix.org) [](https://matrix.to/#/#dendrite-dev:matrix.org)
|
||||
|
||||
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):
|
||||
- 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
|
||||
[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).
|
||||
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
||||
|
||||
- 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
|
||||
[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).
|
||||
- Scalable: can run on multiple machines and eventually scale to massive homeserver deployments.
|
||||
|
||||
Dendrite is **beta** software, which means:
|
||||
|
||||
- Dendrite is ready for early adopters. We recommend running Dendrite with a PostgreSQL database.
|
||||
- Dendrite has periodic releases. We intend to release new versions as we fix bugs and land significant features.
|
||||
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 has periodic semver releases. We intend to release new versions as we land significant features.
|
||||
- Dendrite supports database schema upgrades between releases. This means you should never lose your messages when upgrading Dendrite.
|
||||
- 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:
|
||||
|
||||
- Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially.
|
||||
- Dendrite is feature-complete. There may be client or federation APIs that are not implemented.
|
||||
- Dendrite is ready for massive homeserver deployments. There is no high-availability/clustering support.
|
||||
- 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,
|
||||
presence and push notifications are entirely missing from Dendrite. See [CHANGES.md](CHANGES.md) for updates.
|
||||
- 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.
|
||||
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:
|
||||
|
||||
|
|
@ -41,34 +31,29 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
|||
|
||||
## Requirements
|
||||
|
||||
See the [Planning your Installation](https://matrix-org.github.io/dendrite/installation/planning) page for
|
||||
more information on requirements.
|
||||
|
||||
To build Dendrite, you will need Go 1.21 or later.
|
||||
To build Dendrite, you will need Go 1.15 or later.
|
||||
|
||||
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
|
||||
- SRV records or a well-known file pointing to your deployment
|
||||
|
||||
Also recommended are:
|
||||
|
||||
- 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.
|
||||
|
||||
## 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:
|
||||
|
||||
```bash
|
||||
$ git clone https://github.com/matrix-org/dendrite
|
||||
$ cd dendrite
|
||||
$ go build -o bin/ ./cmd/...
|
||||
$ ./build.sh
|
||||
|
||||
# Generate a Matrix signing key for federation (required)
|
||||
$ ./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
|
||||
# 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:
|
||||
$ ./bin/dendrite --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
|
||||
$ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml
|
||||
```
|
||||
|
||||
Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`.
|
||||
|
||||
## 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
|
||||
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
|
||||
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
|
||||
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:
|
||||
- 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
|
||||
|
||||
|
|
@ -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
|
||||
code, you can quickly run Sytest to ensure that the test names are now passing.
|
||||
|
||||
If you're new to the project, see our
|
||||
[Contributing page](https://matrix-org.github.io/dendrite/development/contributing) to get up to speed, then
|
||||
For example, if the test `Local device key changes get to remote servers` was marked as failing, find the
|
||||
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
|
||||
familiar with the project, look for [Help Wanted](https://github.com/matrix-org/dendrite/labels/help-wanted)
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"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
|
||||
// about whether a room alias exists
|
||||
type RoomAliasExistsRequest struct {
|
||||
|
|
@ -81,97 +61,42 @@ type UserIDExistsResponse struct {
|
|||
UserIDExists bool `json:"exists"`
|
||||
}
|
||||
|
||||
const (
|
||||
ASProtocolLegacyPath = "/_matrix/app/unstable/thirdparty/protocol/"
|
||||
ASUserLegacyPath = "/_matrix/app/unstable/thirdparty/user"
|
||||
ASLocationLegacyPath = "/_matrix/app/unstable/thirdparty/location"
|
||||
ASRoomAliasExistsLegacyPath = "/rooms/"
|
||||
ASUserExistsLegacyPath = "/users/"
|
||||
|
||||
ASProtocolPath = "/_matrix/app/v1/thirdparty/protocol/"
|
||||
ASUserPath = "/_matrix/app/v1/thirdparty/user"
|
||||
ASLocationPath = "/_matrix/app/v1/thirdparty/location"
|
||||
ASRoomAliasExistsPath = "/_matrix/app/v1/rooms/"
|
||||
ASUserExistsPath = "/_matrix/app/v1/users/"
|
||||
)
|
||||
|
||||
type ProtocolRequest struct {
|
||||
Protocol string `json:"protocol,omitempty"`
|
||||
// AppServiceQueryAPI is used to query user and room alias data from application
|
||||
// services
|
||||
type AppServiceQueryAPI 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
|
||||
}
|
||||
|
||||
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
|
||||
// application services for a given user's profile
|
||||
// TODO: Remove this, it's called from federationapi and clientapi but is a pure function
|
||||
func RetrieveUserProfile(
|
||||
ctx context.Context,
|
||||
userID string,
|
||||
asAPI AppServiceInternalAPI,
|
||||
profileAPI userapi.ProfileAPI,
|
||||
asAPI AppServiceQueryAPI,
|
||||
accountDB accounts.Database,
|
||||
) (*authtypes.Profile, error) {
|
||||
localpart, _, err := gomatrixserverlib.SplitID('@', userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Try to query the user from the local database
|
||||
profile, err := profileAPI.QueryProfile(ctx, userID)
|
||||
if err == nil {
|
||||
profile, err := accountDB.GetProfileByLocalpart(ctx, localpart)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return nil, err
|
||||
} else if profile != nil {
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
|
|
@ -184,11 +109,11 @@ func RetrieveUserProfile(
|
|||
|
||||
// If no user exists, return
|
||||
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
|
||||
profile, err = profileAPI.QueryProfile(ctx, userID)
|
||||
profile, err = accountDB.GetProfileByLocalpart(ctx, localpart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,66 +16,99 @@ package appservice
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/dendrite/setup/jetstream"
|
||||
"github.com/matrix-org/dendrite/setup/process"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||
"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/storage"
|
||||
"github.com/matrix-org/dendrite/appservice/types"
|
||||
"github.com/matrix-org/dendrite/appservice/workers"
|
||||
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/kafka"
|
||||
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
|
||||
// can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes.
|
||||
func NewInternalAPI(
|
||||
processContext *process.ProcessContext,
|
||||
cfg *config.Dendrite,
|
||||
natsInstance *jetstream.NATSInstance,
|
||||
userAPI userapi.AppserviceUserAPI,
|
||||
base *setup.BaseDendrite,
|
||||
userAPI userapi.UserInternalAPI,
|
||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
) appserviceAPI.AppServiceInternalAPI {
|
||||
|
||||
// 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{
|
||||
Cfg: &cfg.AppServiceAPI,
|
||||
ProtocolCache: map[string]appserviceAPI.ASProtocolResponse{},
|
||||
CacheMu: sync.Mutex{},
|
||||
) appserviceAPI.AppServiceQueryAPI {
|
||||
client := &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &http.Transport{
|
||||
DisableKeepAlives: true,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation,
|
||||
},
|
||||
},
|
||||
}
|
||||
consumer, _ := kafka.SetupConsumerProducer(&base.Cfg.Global.Kafka)
|
||||
|
||||
if len(cfg.Derived.ApplicationServices) == 0 {
|
||||
return appserviceQueryAPI
|
||||
// Create a connection to the appservice postgres DB
|
||||
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
|
||||
// a sync.Cond object that can be used to notify workers when there are new
|
||||
// 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
|
||||
if err := generateAppServiceAccount(userAPI, appservice, cfg.Global.ServerName); err != nil {
|
||||
if err = generateAppServiceAccount(userAPI, appservice); err != nil {
|
||||
logrus.WithFields(logrus.Fields{
|
||||
"appservice": appservice.ID,
|
||||
}).WithError(err).Panicf("failed to generate bot account for appservice")
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
js, _ := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||
consumer := consumers.NewOutputRoomEventConsumer(
|
||||
processContext, &cfg.AppServiceAPI,
|
||||
js, rsAPI,
|
||||
)
|
||||
if err := consumer.Start(); err != nil {
|
||||
logrus.WithError(err).Panicf("failed to start appservice roomserver consumer")
|
||||
// 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.
|
||||
// We can't add ASes at runtime so this is safe to do.
|
||||
if len(workerStates) > 0 {
|
||||
consumer := consumers.NewOutputRoomEventConsumer(
|
||||
base.ProcessContext, base.Cfg, consumer, appserviceDB,
|
||||
rsAPI, workerStates,
|
||||
)
|
||||
if err := consumer.Start(); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
@ -83,15 +116,13 @@ func NewInternalAPI(
|
|||
// `sender_localpart` field of each application service if it doesn't
|
||||
// exist already
|
||||
func generateAppServiceAccount(
|
||||
userAPI userapi.AppserviceUserAPI,
|
||||
userAPI userapi.UserInternalAPI,
|
||||
as config.ApplicationService,
|
||||
serverName spec.ServerName,
|
||||
) error {
|
||||
var accRes userapi.PerformAccountCreationResponse
|
||||
err := userAPI.PerformAccountCreation(context.Background(), &userapi.PerformAccountCreationRequest{
|
||||
AccountType: userapi.AccountTypeAppService,
|
||||
AccountType: userapi.AccountTypeUser,
|
||||
Localpart: as.SenderLocalpart,
|
||||
ServerName: serverName,
|
||||
AppServiceID: as.ID,
|
||||
OnConflict: userapi.ConflictUpdate,
|
||||
}, &accRes)
|
||||
|
|
@ -100,12 +131,10 @@ func generateAppServiceAccount(
|
|||
}
|
||||
var devRes userapi.PerformDeviceCreationResponse
|
||||
err = userAPI.PerformDeviceCreation(context.Background(), &userapi.PerformDeviceCreationRequest{
|
||||
Localpart: as.SenderLocalpart,
|
||||
ServerName: serverName,
|
||||
AccessToken: as.ASToken,
|
||||
DeviceID: &as.SenderLocalpart,
|
||||
DeviceDisplayName: &as.SenderLocalpart,
|
||||
NoDeviceListUpdate: true,
|
||||
Localpart: as.SenderLocalpart,
|
||||
AccessToken: as.ASToken,
|
||||
DeviceID: &as.SenderLocalpart,
|
||||
DeviceDisplayName: &as.SenderLocalpart,
|
||||
}, &devRes)
|
||||
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"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/types"
|
||||
"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/syncapi/synctypes"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
|
||||
"github.com/Shopify/sarama"
|
||||
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.
|
||||
type OutputRoomEventConsumer struct {
|
||||
ctx context.Context
|
||||
cfg *config.AppServiceAPI
|
||||
jetstream nats.JetStreamContext
|
||||
topic string
|
||||
rsAPI api.AppserviceRoomserverAPI
|
||||
}
|
||||
|
||||
type appserviceState struct {
|
||||
*config.ApplicationService
|
||||
backoff int
|
||||
roomServerConsumer *internal.ContinualConsumer
|
||||
asDB storage.Database
|
||||
rsAPI api.RoomserverInternalAPI
|
||||
serverName string
|
||||
workerStates []types.ApplicationServiceWorkerState
|
||||
}
|
||||
|
||||
// NewOutputRoomEventConsumer creates a new OutputRoomEventConsumer. Call
|
||||
// Start() to begin consuming from room servers.
|
||||
func NewOutputRoomEventConsumer(
|
||||
process *process.ProcessContext,
|
||||
cfg *config.AppServiceAPI,
|
||||
js nats.JetStreamContext,
|
||||
rsAPI api.AppserviceRoomserverAPI,
|
||||
cfg *config.Dendrite,
|
||||
kafkaConsumer sarama.Consumer,
|
||||
appserviceDB storage.Database,
|
||||
rsAPI api.RoomserverInternalAPI,
|
||||
workerStates []types.ApplicationServiceWorkerState,
|
||||
) *OutputRoomEventConsumer {
|
||||
return &OutputRoomEventConsumer{
|
||||
ctx: process.Context(),
|
||||
cfg: cfg,
|
||||
jetstream: js,
|
||||
topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputAppserviceEvent),
|
||||
rsAPI: rsAPI,
|
||||
consumer := internal.ContinualConsumer{
|
||||
Process: process,
|
||||
ComponentName: "appservice/roomserver",
|
||||
Topic: cfg.Global.Kafka.TopicFor(config.TopicOutputRoomEvent),
|
||||
Consumer: kafkaConsumer,
|
||||
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
|
||||
func (s *OutputRoomEventConsumer) Start() error {
|
||||
durableNames := make([]string, 0, len(s.cfg.Derived.ApplicationServices))
|
||||
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
|
||||
return s.roomServerConsumer.Start()
|
||||
}
|
||||
|
||||
// onMessage is called when the appservice component receives a new event from
|
||||
// the room server output log.
|
||||
func (s *OutputRoomEventConsumer) onMessage(
|
||||
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
|
||||
var output api.OutputEvent
|
||||
if err := json.Unmarshal(msg.Data, &output); err != nil {
|
||||
// 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")
|
||||
continue
|
||||
}
|
||||
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:
|
||||
continue
|
||||
}
|
||||
func (s *OutputRoomEventConsumer) onMessage(msg *sarama.ConsumerMessage) error {
|
||||
// Parse out the event JSON
|
||||
var output api.OutputEvent
|
||||
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
|
||||
log.WithError(err).Errorf("roomserver output log: message parse failure")
|
||||
return nil
|
||||
}
|
||||
|
||||
// If there are no events selected for sending then we should
|
||||
// ack the messages so that we don't get sent them again in the
|
||||
// future.
|
||||
if len(events) == 0 {
|
||||
return true
|
||||
if output.Type != api.OutputTypeNewRoomEvent {
|
||||
return nil
|
||||
}
|
||||
|
||||
txnID := ""
|
||||
// Try to get the message metadata, if we're able to, use the timestamp as the txnID
|
||||
metadata, err := msgs[0].Metadata()
|
||||
if err == nil {
|
||||
txnID = strconv.Itoa(int(metadata.Timestamp.UnixNano()))
|
||||
}
|
||||
events := []*gomatrixserverlib.HeaderedEvent{output.NewRoomEvent.Event}
|
||||
events = append(events, output.NewRoomEvent.AddStateEvents...)
|
||||
|
||||
// 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
|
||||
// Send event to any relevant application services
|
||||
return s.filterRoomserverEvents(context.TODO(), events)
|
||||
}
|
||||
|
||||
// sendEvents passes events to the appservice by using the transactions
|
||||
// endpoint. It will block for the backoff period if necessary.
|
||||
func (s *OutputRoomEventConsumer) sendEvents(
|
||||
ctx context.Context, state *appserviceState,
|
||||
events []*types.HeaderedEvent,
|
||||
txnID string,
|
||||
// filterRoomserverEvents takes in events and decides whether any of them need
|
||||
// to be passed on to an external application service. It does this by checking
|
||||
// each namespace of each registered application service, and if there is a
|
||||
// match, adds the event to the queue for events to be sent to a particular
|
||||
// application service.
|
||||
func (s *OutputRoomEventConsumer) filterRoomserverEvents(
|
||||
ctx context.Context,
|
||||
events []*gomatrixserverlib.HeaderedEvent,
|
||||
) error {
|
||||
// Create the transaction body.
|
||||
transaction, err := json.Marshal(
|
||||
ApplicationServiceTransaction{
|
||||
Events: synctypes.ToClientEvents(gomatrixserverlib.ToPDUs(events), synctypes.FormatAll, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) {
|
||||
return s.rsAPI.QueryUserIDForSender(ctx, roomID, senderID)
|
||||
}),
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
for _, ws := range s.workerStates {
|
||||
for _, event := range events {
|
||||
// Check if this event is interesting to this application service
|
||||
if s.appserviceIsInterestedInEvent(ctx, event, ws.AppService) {
|
||||
// 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")
|
||||
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
|
||||
}
|
||||
|
||||
// backoff pauses the calling goroutine for a 2^some backoff exponent seconds
|
||||
func (s *appserviceState) backoffAndPause(err error) error {
|
||||
if s.backoff < 6 {
|
||||
s.backoff++
|
||||
// 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 *gomatrixserverlib.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(),
|
||||
JoinedOnly: true,
|
||||
}
|
||||
duration := time.Second * time.Duration(math.Pow(2, float64(s.backoff)))
|
||||
log.WithField("appservice", s.ID).WithError(err).Errorf("Unable to send transaction to appservice, backing off for %s", duration.String())
|
||||
time.Sleep(duration)
|
||||
return err
|
||||
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 {
|
||||
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
|
||||
// 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
|
||||
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *types.HeaderedEvent, appservice *config.ApplicationService) bool {
|
||||
user := ""
|
||||
userID, err := s.rsAPI.QueryUserIDForSender(ctx, event.RoomID(), event.SenderID())
|
||||
if err == nil {
|
||||
user = userID.String()
|
||||
}
|
||||
|
||||
switch {
|
||||
case appservice.URL == "":
|
||||
func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Context, event *gomatrixserverlib.HeaderedEvent, appservice config.ApplicationService) bool {
|
||||
// No reason to queue events if they'll never be sent to the application
|
||||
// service
|
||||
if appservice.URL == "" {
|
||||
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
|
||||
}
|
||||
|
||||
if event.Type() == spec.MRoomMember && event.StateKey() != nil {
|
||||
if event.Type() == gomatrixserverlib.MRoomMember && event.StateKey() != nil {
|
||||
if appservice.IsInterestedInUserID(*event.StateKey()) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
if err := s.rsAPI.GetAliasesForRoomID(ctx, &queryReq, &queryRes); err == nil {
|
||||
for _, alias := range queryRes.Aliases {
|
||||
|
|
@ -285,54 +191,10 @@ func (s *OutputRoomEventConsumer) appserviceIsInterestedInEvent(ctx context.Cont
|
|||
}
|
||||
} else {
|
||||
log.WithFields(log.Fields{
|
||||
"appservice": appservice.ID,
|
||||
"room_id": event.RoomID().String(),
|
||||
"room_id": event.RoomID(),
|
||||
}).WithError(err).Errorf("Unable to get aliases for room")
|
||||
}
|
||||
|
||||
// Check if any of the members in the room match the 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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sync"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/matrix-org/dendrite/appservice/api"
|
||||
"github.com/matrix-org/dendrite/internal"
|
||||
"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
|
||||
type AppServiceQueryAPI struct {
|
||||
Cfg *config.AppServiceAPI
|
||||
ProtocolCache map[string]api.ASProtocolResponse
|
||||
CacheMu sync.Mutex
|
||||
HTTPClient *http.Client
|
||||
Cfg *config.Dendrite
|
||||
}
|
||||
|
||||
// RoomAliasExists performs a request to '/room/{roomAlias}' on all known
|
||||
|
|
@ -46,29 +43,20 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
|||
request *api.RoomAliasExistsRequest,
|
||||
response *api.RoomAliasExistsResponse,
|
||||
) error {
|
||||
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceRoomAlias")
|
||||
defer trace.EndRegion()
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceRoomAlias")
|
||||
defer span.Finish()
|
||||
|
||||
// Determine which application service should handle this request
|
||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||
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
|
||||
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||
URL, err := url.Parse(appservice.URL + roomAliasExistsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
URL.Path += request.Alias
|
||||
if a.Cfg.LegacyAuth {
|
||||
q := URL.Query()
|
||||
q.Set("access_token", appservice.HSToken)
|
||||
URL.RawQuery = q.Encode()
|
||||
}
|
||||
apiURL := URL.String()
|
||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
||||
|
||||
// Send a request to each application service. If one responds that it has
|
||||
// created the room, immediately return.
|
||||
|
|
@ -76,10 +64,9 @@ func (a *AppServiceQueryAPI) RoomAliasExists(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp, err := appservice.HTTPClient.Do(req)
|
||||
resp, err := a.HTTPClient.Do(req)
|
||||
if resp != nil {
|
||||
defer func() {
|
||||
err = resp.Body.Close()
|
||||
|
|
@ -123,28 +110,19 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
|||
request *api.UserIDExistsRequest,
|
||||
response *api.UserIDExistsResponse,
|
||||
) error {
|
||||
trace, ctx := internal.StartRegion(ctx, "ApplicationServiceUserID")
|
||||
defer trace.EndRegion()
|
||||
span, ctx := opentracing.StartSpanFromContext(ctx, "ApplicationServiceUserID")
|
||||
defer span.Finish()
|
||||
|
||||
// Determine which application service should handle this request
|
||||
for _, appservice := range a.Cfg.Derived.ApplicationServices {
|
||||
if appservice.URL != "" && appservice.IsInterestedInUserID(request.UserID) {
|
||||
// The full path to the rooms API, includes hs token
|
||||
path := api.ASUserExistsPath
|
||||
if a.Cfg.LegacyPaths {
|
||||
path = api.ASUserExistsLegacyPath
|
||||
}
|
||||
URL, err := url.Parse(appservice.RequestUrl() + path)
|
||||
URL, err := url.Parse(appservice.URL + userIDExistsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
URL.Path += request.UserID
|
||||
if a.Cfg.LegacyAuth {
|
||||
q := URL.Query()
|
||||
q.Set("access_token", appservice.HSToken)
|
||||
URL.RawQuery = q.Encode()
|
||||
}
|
||||
apiURL := URL.String()
|
||||
apiURL := URL.String() + "?access_token=" + appservice.HSToken
|
||||
|
||||
// Send a request to each application service. If one responds that it has
|
||||
// created the user, immediately return.
|
||||
|
|
@ -152,8 +130,7 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", appservice.HSToken))
|
||||
resp, err := appservice.HTTPClient.Do(req.WithContext(ctx))
|
||||
resp, err := a.HTTPClient.Do(req.WithContext(ctx))
|
||||
if resp != nil {
|
||||
defer func() {
|
||||
err = resp.Body.Close()
|
||||
|
|
@ -188,191 +165,3 @@ func (a *AppServiceQueryAPI) UserIDExists(
|
|||
response.UserIDExists = false
|
||||
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
|
||||
rct POST /rooms/:room_id/receipt can create receipts
|
||||
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 GET /media/v3/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 /r0/capabilities is not public
|
||||
cap GET /v3/capabilities is not public
|
||||
reg Register with a recaptcha
|
||||
reg registration is idempotent, without username specified
|
||||
reg registration is idempotent, with username specified
|
||||
|
|
@ -155,222 +152,219 @@ jon Joining room twice is idempotent
|
|||
syn New room members see their own join event
|
||||
v1s New room members see existing users' presence in room initialSync
|
||||
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
|
||||
f,jon Remote users can join room by alias
|
||||
syn New room members see their own join event
|
||||
v1s New room members see existing members' presence in room initialSync
|
||||
syn Existing members see new members' join events
|
||||
pre 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 per-room initialSync
|
||||
f,jon Remote users may not join unfederated rooms
|
||||
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 per-room initialSync
|
||||
f,jon Remote users may not join unfederated rooms
|
||||
syn Local room members see posted message events
|
||||
v1s Fetching eventstream a second time doesn't yield the message again
|
||||
syn Local non-members don't see posted message events
|
||||
get Local room members can get room messages
|
||||
get Local room members can get room messages
|
||||
f,syn Remote room members also see posted message events
|
||||
f,get Remote room members can get room messages
|
||||
f,get Remote room members can get room messages
|
||||
get Message history can be paginated
|
||||
f,get Message history can be paginated over federation
|
||||
eph Ephemeral messages received from clients are correctly expired
|
||||
eph Ephemeral messages received from clients are correctly expired
|
||||
ali Room aliases can contain Unicode
|
||||
f,ali Remote room alias queries can handle Unicode
|
||||
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 (4 subtests)
|
||||
ali Canonical alias can be set
|
||||
ali Canonical alias can include alt_aliases
|
||||
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 Deleting a non-existent alias should return a 404
|
||||
ali Users can't delete other's aliases
|
||||
ali Users with sufficient power-level can delete other's aliases
|
||||
ali Can delete canonical alias
|
||||
ali Alias creators can delete alias with no ops
|
||||
ali Alias creators can delete canonical alias with no ops
|
||||
msc Only room members can list aliases of a room
|
||||
inv Can invite users to invite-only rooms
|
||||
inv Uninvited users cannot join the room
|
||||
inv Invited user can reject invite
|
||||
f,inv Invited user can reject invite over federation
|
||||
f,inv Invited user can reject invite over federation several times
|
||||
inv Invited user can reject invite for empty room
|
||||
f,inv Invited user can reject invite over federation for empty room
|
||||
inv Invited user can reject local invite after originator leaves
|
||||
inv Invited user can see room metadata
|
||||
f,inv Remote invited user can see room metadata
|
||||
inv Users cannot invite themselves to a room
|
||||
inv Users cannot invite a user that is already in the room
|
||||
ban Banned user is kicked and may not rejoin until unbanned
|
||||
f,ban Remote banned user is kicked and may not rejoin until unbanned
|
||||
ban 'ban' event respects room powerlevel
|
||||
plv setting 'm.room.name' respects room powerlevel
|
||||
ali Users with sufficient power-level can delete other's aliases
|
||||
ali Can delete canonical alias
|
||||
ali Alias creators can delete alias with no ops
|
||||
ali Alias creators can delete canonical alias with no ops
|
||||
msc Only room members can list aliases of a room
|
||||
inv Can invite users to invite-only rooms
|
||||
inv Uninvited users cannot join the room
|
||||
inv Invited user can reject invite
|
||||
f,inv Invited user can reject invite over federation
|
||||
f,inv Invited user can reject invite over federation several times
|
||||
inv Invited user can reject invite for empty room
|
||||
f,inv Invited user can reject invite over federation for empty room
|
||||
inv Invited user can reject local invite after originator leaves
|
||||
inv Invited user can see room metadata
|
||||
f,inv Remote invited user can see room metadata
|
||||
inv Users cannot invite themselves to a room
|
||||
inv Users cannot invite a user that is already in the room
|
||||
ban Banned user is kicked and may not rejoin until unbanned
|
||||
f,ban Remote banned user is kicked and may not rejoin until unbanned
|
||||
ban 'ban' event respects room powerlevel
|
||||
plv setting 'm.room.name' respects room powerlevel
|
||||
plv setting 'm.room.power_levels' respects room powerlevel (2 subtests)
|
||||
plv Unprivileged users can set m.room.topic if it only needs level 0
|
||||
plv Users cannot set ban powerlevel higher than their own (2 subtests)
|
||||
plv Users cannot set kick 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 Event stream catches up fully after many messages
|
||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_id as power user redacts message
|
||||
xxx PUT /rooms/:room_id/redact/:event_id/:txn_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 PUT /redact disallows redaction of event in different room
|
||||
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 Can get rooms/{roomId}/initialSync for a departed room (SPEC-216)
|
||||
rst Can get rooms/{roomId}/state for a departed room (SPEC-216)
|
||||
v1s Check that event streams started after a client joined a room work (SYT-1)
|
||||
v1s Event stream catches up fully after many messages
|
||||
xxx POST /rooms/:room_id/redact/:event_id as power user redacts message
|
||||
xxx POST /rooms/:room_id/redact/:event_id as original message sender redacts message
|
||||
xxx POST /rooms/:room_id/redact/:event_id as random user does not redact message
|
||||
xxx POST /redact disallows redaction of event in different room
|
||||
xxx Redaction of a redaction redacts the redaction reason
|
||||
v1s A departed room is still included in /initialSync (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)
|
||||
mem Can get rooms/{roomId}/members for a departed room (SPEC-216)
|
||||
get Can get rooms/{roomId}/messages for a departed room (SPEC-216)
|
||||
rst Can get 'm.room.name' state for a departed room (SPEC-216)
|
||||
get Can get rooms/{roomId}/messages for a departed room (SPEC-216)
|
||||
rst Can get 'm.room.name' state for a departed room (SPEC-216)
|
||||
syn Getting messages going forward is limited for a departed room (SPEC-216)
|
||||
3pd Can invite existing 3pid
|
||||
3pd Can invite existing 3pid with no ops into a private room
|
||||
3pd Can invite existing 3pid in createRoom
|
||||
3pd Can invite unbound 3pid
|
||||
f,3pd Can invite unbound 3pid over federation
|
||||
3pd Can invite unbound 3pid with no ops into a private room
|
||||
f,3pd Can invite unbound 3pid over federation with no ops into a private room
|
||||
f,3pd Can invite unbound 3pid over federation with users from both servers
|
||||
3pd Can accept unbound 3pid invite after inviter leaves
|
||||
3pd Can accept third party invite with /join
|
||||
3pd Can invite existing 3pid
|
||||
3pd Can invite existing 3pid with no ops into a private room
|
||||
3pd Can invite existing 3pid in createRoom
|
||||
3pd Can invite unbound 3pid
|
||||
f,3pd Can invite unbound 3pid over federation
|
||||
3pd Can invite unbound 3pid with no ops into a private room
|
||||
f,3pd Can invite unbound 3pid over federation with no ops into a private room
|
||||
f,3pd Can invite unbound 3pid over federation with users from both servers
|
||||
3pd Can accept unbound 3pid invite after inviter leaves
|
||||
3pd Can accept third party invite with /join
|
||||
3pd 3pid invite join with wrong but valid signature are rejected
|
||||
3pd 3pid invite join valid signature but revoked keys are rejected
|
||||
3pd 3pid invite join valid signature but unreachable ID server are rejected
|
||||
gst Guest user cannot call /events globally
|
||||
gst Guest users can join guest_access rooms
|
||||
gst Guest users can send messages to guest_access rooms if joined
|
||||
gst Guest user calling /events doesn't tightloop
|
||||
gst Guest users are kicked from guest_access rooms on revocation of guest_access
|
||||
gst Guest users can send messages to guest_access rooms if joined
|
||||
gst Guest user calling /events doesn't tightloop
|
||||
gst Guest users are kicked from guest_access rooms on revocation of guest_access
|
||||
gst Guest user can set display names
|
||||
gst Guest users are kicked from guest_access rooms on revocation of guest_access over federation
|
||||
gst Guest user can upgrade to fully featured user
|
||||
gst Guest users are kicked from guest_access rooms on revocation of guest_access over federation
|
||||
gst Guest user can upgrade to fully featured user
|
||||
gst Guest user cannot upgrade other users
|
||||
pub GET /publicRooms lists rooms
|
||||
pub GET /publicRooms includes avatar URLs
|
||||
gst Guest users can accept invites to private rooms over federation
|
||||
gst Guest users denied access over federation if guest access prohibited
|
||||
mem Room members can override their displayname on a room-specific basis
|
||||
pub GET /publicRooms lists rooms
|
||||
pub GET /publicRooms includes avatar URLs
|
||||
gst Guest users can accept invites to private rooms over federation
|
||||
gst Guest users denied access over federation if guest access prohibited
|
||||
mem Room members can override their displayname on a room-specific basis
|
||||
mem Room members can join a room with an overridden displayname
|
||||
mem Users cannot kick users from a room they are not in
|
||||
mem Users cannot kick users who have already left a room
|
||||
typ Typing notification sent to local room members
|
||||
f,typ Typing notifications also sent to remote room members
|
||||
typ Typing can be explicitly stopped
|
||||
rct Read receipts are visible to /initialSync
|
||||
rct Read receipts are sent as events
|
||||
rct Receipts must be m.read
|
||||
pro displayname updates affect room member events
|
||||
pro avatar_url updates affect room member events
|
||||
mem Users cannot kick users from a room they are not in
|
||||
mem Users cannot kick users who have already left a room
|
||||
typ Typing notification sent to local room members
|
||||
f,typ Typing notifications also sent to remote room members
|
||||
typ Typing can be explicitly stopped
|
||||
rct Read receipts are visible to /initialSync
|
||||
rct Read receipts are sent as events
|
||||
rct Receipts must be m.read
|
||||
pro displayname updates affect room member events
|
||||
pro avatar_url updates affect room member events
|
||||
gst m.room.history_visibility == "world_readable" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "shared" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "invited" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "joined" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "default" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "shared" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "invited" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "joined" allows/forbids appropriately for Guest users
|
||||
gst m.room.history_visibility == "default" allows/forbids appropriately for Guest users
|
||||
gst Guest non-joined user cannot call /events on shared room
|
||||
gst Guest non-joined user cannot call /events on invited room
|
||||
gst Guest non-joined user cannot call /events on joined room
|
||||
gst Guest non-joined user cannot call /events on default room
|
||||
gst Guest non-joined user can call /events on world_readable room
|
||||
gst Guest non-joined user can call /events on world_readable room
|
||||
gst Guest non-joined users can get state for world_readable rooms
|
||||
gst Guest non-joined users can get individual state for world_readable rooms
|
||||
gst Guest non-joined users cannot room initalSync for non-world_readable rooms
|
||||
gst Guest non-joined users can room initialSync for world_readable rooms
|
||||
gst Guest non-joined users can room initialSync for world_readable rooms
|
||||
gst Guest non-joined users can get individual state for world_readable rooms after leaving
|
||||
gst Guest non-joined users cannot send messages to guest_access rooms if not joined
|
||||
gst Guest users can sync from world_readable guest_access rooms if joined
|
||||
gst Guest users can sync from shared guest_access rooms if joined
|
||||
gst Guest users can sync from invited guest_access rooms if joined
|
||||
gst Guest users can sync from joined guest_access rooms if joined
|
||||
gst Guest users can sync from shared guest_access rooms if joined
|
||||
gst Guest users can sync from invited guest_access rooms if joined
|
||||
gst Guest users can sync from joined guest_access rooms if joined
|
||||
gst Guest users can sync from default guest_access rooms if joined
|
||||
ath m.room.history_visibility == "world_readable" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "shared" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "invited" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "joined" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "default" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "shared" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "invited" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "joined" allows/forbids appropriately for Real users
|
||||
ath m.room.history_visibility == "default" allows/forbids appropriately for Real users
|
||||
ath Real non-joined user cannot call /events on shared room
|
||||
ath Real non-joined user cannot call /events on invited room
|
||||
ath Real non-joined user cannot call /events on joined room
|
||||
ath Real non-joined user cannot call /events on default room
|
||||
ath Real non-joined user can call /events on world_readable room
|
||||
ath Real non-joined user can call /events on world_readable room
|
||||
ath Real non-joined users can get state for world_readable rooms
|
||||
ath Real non-joined users can get individual state for world_readable rooms
|
||||
ath Real non-joined users cannot room initalSync for non-world_readable rooms
|
||||
ath Real non-joined users can room initialSync for world_readable rooms
|
||||
ath Real non-joined users can get individual state for world_readable rooms after leaving
|
||||
ath Real non-joined users can room initialSync for world_readable rooms
|
||||
ath Real non-joined users can get individual state for world_readable rooms after leaving
|
||||
ath Real non-joined users cannot send messages to guest_access rooms if not joined
|
||||
ath Real users can sync from world_readable guest_access rooms if joined
|
||||
ath Real users can sync from shared guest_access rooms if joined
|
||||
ath Real users can sync from invited guest_access rooms if joined
|
||||
ath Real users can sync from joined guest_access rooms if joined
|
||||
ath Real users can sync from shared guest_access rooms if joined
|
||||
ath Real users can sync from invited guest_access rooms if joined
|
||||
ath Real users can sync from joined guest_access rooms if joined
|
||||
ath Real users can sync from default guest_access rooms if joined
|
||||
ath Only see history_visibility changes on boundaries
|
||||
ath Only see history_visibility changes on boundaries
|
||||
f,ath Backfill works correctly with history visibility set to joined
|
||||
fgt Forgotten room messages cannot be paginated
|
||||
fgt Forgetting room does not show up in v2 /sync
|
||||
fgt Can forget room you've been kicked from
|
||||
fgt Forgotten room messages cannot be paginated
|
||||
fgt Forgetting room does not show up in v2 /sync
|
||||
fgt Can forget room you've been kicked from
|
||||
fgt Can't forget room you're still in
|
||||
fgt Can re-join room if re-invited
|
||||
ath Only original members of the room can see messages from erased users
|
||||
ath Only original members of the room can see messages from erased users
|
||||
mem /joined_rooms returns only joined rooms
|
||||
mem /joined_members return joined members
|
||||
ctx /context/ on joined room works
|
||||
ctx /context/ on non world readable room does not work
|
||||
ctx /context/ returns correct number of events
|
||||
ctx /context/ with lazy_load_members filter works
|
||||
mem /joined_members return joined members
|
||||
ctx /context/ on joined room works
|
||||
ctx /context/ on non world readable room does not work
|
||||
ctx /context/ returns correct number of events
|
||||
ctx /context/ with lazy_load_members filter works
|
||||
get /event/ on joined room works
|
||||
get /event/ on non world readable room does not work
|
||||
get /event/ does not allow access to events before the user joined
|
||||
mem Can get rooms/{roomId}/members
|
||||
mem Can get rooms/{roomId}/members at a given point
|
||||
mem Can filter rooms/{roomId}/members
|
||||
upg /upgrade creates a new room
|
||||
upg /upgrade should preserve room visibility for public rooms
|
||||
upg /upgrade should preserve room visibility for private rooms
|
||||
upg /upgrade copies >100 power levels to the new room
|
||||
upg /upgrade copies the power levels to the new room
|
||||
upg /upgrade preserves the power level of the upgrading user in old and new rooms
|
||||
upg /upgrade copies important state to the new room
|
||||
upg /upgrade copies ban events to the new room
|
||||
upg local user has push rules copied to upgraded room
|
||||
f,upg remote user has push rules copied to upgraded room
|
||||
upg /upgrade moves aliases to the new room
|
||||
upg /upgrade moves remote aliases to the new room
|
||||
upg /upgrade preserves direct room state
|
||||
upg /upgrade preserves room federation ability
|
||||
upg /upgrade restricts power levels in the old room
|
||||
upg /upgrade restricts power levels in the old room when the old PLs are unusual
|
||||
upg /upgrade to an unknown version is rejected
|
||||
upg /upgrade is rejected if the user can't send state events
|
||||
upg /upgrade of a bogus room fails gracefully
|
||||
upg Cannot send tombstone event that points to the same room
|
||||
f,upg Local and remote users' homeservers remove a room from their public directory on upgrade
|
||||
rst Name/topic keys are correct
|
||||
mem Can get rooms/{roomId}/members at a given point
|
||||
mem Can filter rooms/{roomId}/members
|
||||
upg /upgrade creates a new room
|
||||
upg /upgrade should preserve room visibility for public rooms
|
||||
upg /upgrade should preserve room visibility for private rooms
|
||||
upg /upgrade copies >100 power levels to the new room
|
||||
upg /upgrade copies the power levels to the new room
|
||||
upg /upgrade preserves the power level of the upgrading user in old and new rooms
|
||||
upg /upgrade copies important state to the new room
|
||||
upg /upgrade copies ban events to the new room
|
||||
upg local user has push rules copied to upgraded room
|
||||
f,upg remote user has push rules copied to upgraded room
|
||||
upg /upgrade moves aliases to the new room
|
||||
upg /upgrade moves remote aliases to the new room
|
||||
upg /upgrade preserves direct room state
|
||||
upg /upgrade preserves room federation ability
|
||||
upg /upgrade restricts power levels in the old room
|
||||
upg /upgrade restricts power levels in the old room when the old PLs are unusual
|
||||
upg /upgrade to an unknown version is rejected
|
||||
upg /upgrade is rejected if the user can't send state events
|
||||
upg /upgrade of a bogus room fails gracefully
|
||||
upg Cannot send tombstone event that points to the same room
|
||||
f,upg Local and remote users' homeservers remove a room from their public directory on upgrade
|
||||
rst Name/topic keys are correct
|
||||
f,pub Can get remote public room list
|
||||
pub Can paginate public room list
|
||||
pub Can search public room list
|
||||
pub Can search public room list
|
||||
syn Can create filter
|
||||
syn Can download filter
|
||||
syn Can sync
|
||||
syn Can sync a joined room
|
||||
syn Full state sync includes joined rooms
|
||||
syn Newly joined room is included in an incremental sync
|
||||
syn Newly joined room has correct timeline in incremental sync
|
||||
pre Newly joined room includes presence in incremental sync
|
||||
pre 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 message with a transaction id
|
||||
syn Newly joined room has correct timeline in incremental sync
|
||||
syn Newly joined room includes presence 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 message with a transaction id
|
||||
syn A message sent after an initial sync appears in the timeline of an incremental sync.
|
||||
syn A filtered timeline reaches its limit
|
||||
syn Syncing a new room with a large timeline limit isn't limited
|
||||
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 next_batch token can be used in the v1 messages API
|
||||
syn A filtered timeline reaches its limit
|
||||
syn Syncing a new room with a large timeline limit isn't limited
|
||||
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 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
|
||||
pre User sees their own presence in a sync
|
||||
pre 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 their own presence in a sync
|
||||
syn User is offline if they set_presence=offline in their 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
|
||||
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
|
||||
|
|
@ -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 Inbound federation rejects invite rejections which include invalid JSON for room version 6
|
||||
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
|
||||
f,pre Presence changes are also reported to remote 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 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 invited visibility
|
||||
fme Inbound federation can return missing events for invite visibility
|
||||
fme Inbound federation can return missing events for joined visibility
|
||||
fme outliers whose auth_events are in a different room are correctly rejected
|
||||
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 AS-ghosted users can use rooms via AS
|
||||
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 AS can set avatar for ghosted users
|
||||
app AS can set displayname for ghosted users
|
||||
app AS can't set displayname for random users
|
||||
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 (2 subtests)
|
||||
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, 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 can deactivate a user
|
||||
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 Rooms with names 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)
|
||||
typ Typing notifications don't leak
|
||||
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 (3 subtests)
|
||||
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 (6 subtests)
|
||||
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 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 Local users can peek by room alias
|
||||
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
|
||||
import argparse
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Usage: $ ./are-we-synapse-yet.py [-v] results.tap
|
||||
# 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
|
||||
# ...
|
||||
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)
|
||||
subsection_test_names = {} # 'subsection name': ["✓ Test 1", "✓ Test 2", "× Test 3"]
|
||||
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():
|
||||
if passing:
|
||||
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_passing += group_passing
|
||||
|
|
@ -178,20 +177,16 @@ def print_stats(header_name, gid_to_tests, gid_to_name, verbose):
|
|||
line = "%s: %s (%d/%d tests)" % (gid_to_name[gid].ljust(25, ' '), pct.rjust(4, ' '), group_passing, group_total)
|
||||
subsections.append(line)
|
||||
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)
|
||||
print("%s: %s (%d/%d tests)" % (header_name, pct, total_passing, total_tests))
|
||||
print("-" * (len(header_name)+1))
|
||||
for line in subsections:
|
||||
print("%s%s" % ("::group::" if ci and verbose else "", line,))
|
||||
print(" %s" % (line,))
|
||||
if verbose:
|
||||
for test_name_and_pass_mark in subsection_test_names[line]:
|
||||
print(" %s" % (test_name_and_pass_mark,))
|
||||
print("%s" % ("::endgroup::" if ci else ""))
|
||||
print("")
|
||||
print("")
|
||||
|
||||
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
|
||||
WORKDIR /build
|
||||
ADD https://github.com/matrix-org/dendrite/archive/main.tar.gz /build/main.tar.gz
|
||||
RUN tar xvfz main.tar.gz
|
||||
WORKDIR /build/dendrite-main
|
||||
ADD https://github.com/matrix-org/dendrite/archive/master.tar.gz /build/master.tar.gz
|
||||
RUN tar xvfz master.tar.gz
|
||||
WORKDIR /build/dendrite-master
|
||||
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
|
||||
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
|
||||
ADD https://github.com/vector-im/element-web/archive/matthew/p2p.tar.gz /build/p2p.tar.gz
|
||||
RUN tar xvfz p2p.tar.gz
|
||||
|
|
@ -31,21 +31,21 @@ WORKDIR /build/element-web-matthew-p2p
|
|||
RUN yarn install
|
||||
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)
|
||||
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
|
||||
RUN yarn build:p2p
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN echo $'\
|
||||
{ \n\
|
||||
{ \n\
|
||||
"default_server_config": { \n\
|
||||
"m.homeserver": { \n\
|
||||
"base_url": "https://p2p.riot.im", \n\
|
||||
"server_name": "p2p.riot.im" \n\
|
||||
}, \n\
|
||||
"m.identity_server": { \n\
|
||||
"base_url": "https://vector.im" \n\
|
||||
} \n\
|
||||
"m.homeserver": { \n\
|
||||
"base_url": "https://p2p.riot.im", \n\
|
||||
"server_name": "p2p.riot.im" \n\
|
||||
}, \n\
|
||||
"m.identity_server": { \n\
|
||||
"base_url": "https://vector.im" \n\
|
||||
} \n\
|
||||
}, \n\
|
||||
"disable_custom_urls": false, \n\
|
||||
"disable_guests": true, \n\
|
||||
|
|
@ -55,57 +55,57 @@ RUN echo $'\
|
|||
"integrations_ui_url": "https://scalar.vector.im/", \n\
|
||||
"integrations_rest_url": "https://scalar.vector.im/api", \n\
|
||||
"integrations_widgets_urls": [ \n\
|
||||
"https://scalar.vector.im/_matrix/integrations/v1", \n\
|
||||
"https://scalar.vector.im/api", \n\
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1", \n\
|
||||
"https://scalar-staging.vector.im/api", \n\
|
||||
"https://scalar-staging.riot.im/scalar/api" \n\
|
||||
"https://scalar.vector.im/_matrix/integrations/v1", \n\
|
||||
"https://scalar.vector.im/api", \n\
|
||||
"https://scalar-staging.vector.im/_matrix/integrations/v1", \n\
|
||||
"https://scalar-staging.vector.im/api", \n\
|
||||
"https://scalar-staging.riot.im/scalar/api" \n\
|
||||
], \n\
|
||||
"integrations_jitsi_widget_url": "https://scalar.vector.im/api/widgets/jitsi.html", \n\
|
||||
"bug_report_endpoint_url": "https://riot.im/bugreports/submit", \n\
|
||||
"defaultCountryCode": "GB", \n\
|
||||
"showLabsSettings": false, \n\
|
||||
"features": { \n\
|
||||
"feature_pinning": "labs", \n\
|
||||
"feature_custom_status": "labs", \n\
|
||||
"feature_custom_tags": "labs", \n\
|
||||
"feature_state_counters": "labs" \n\
|
||||
"feature_pinning": "labs", \n\
|
||||
"feature_custom_status": "labs", \n\
|
||||
"feature_custom_tags": "labs", \n\
|
||||
"feature_state_counters": "labs" \n\
|
||||
}, \n\
|
||||
"default_federate": true, \n\
|
||||
"default_theme": "light", \n\
|
||||
"roomDirectory": { \n\
|
||||
"servers": [ \n\
|
||||
"matrix.org" \n\
|
||||
] \n\
|
||||
"servers": [ \n\
|
||||
"matrix.org" \n\
|
||||
] \n\
|
||||
}, \n\
|
||||
"welcomeUserId": "", \n\
|
||||
"piwik": { \n\
|
||||
"url": "https://piwik.riot.im/", \n\
|
||||
"whitelistedHSUrls": ["https://matrix.org"], \n\
|
||||
"whitelistedISUrls": ["https://vector.im", "https://matrix.org"], \n\
|
||||
"siteId": 1 \n\
|
||||
"url": "https://piwik.riot.im/", \n\
|
||||
"whitelistedHSUrls": ["https://matrix.org"], \n\
|
||||
"whitelistedISUrls": ["https://vector.im", "https://matrix.org"], \n\
|
||||
"siteId": 1 \n\
|
||||
}, \n\
|
||||
"enable_presence_by_hs_url": { \n\
|
||||
"https://matrix.org": false, \n\
|
||||
"https://matrix-client.matrix.org": false \n\
|
||||
"https://matrix.org": false, \n\
|
||||
"https://matrix-client.matrix.org": false \n\
|
||||
}, \n\
|
||||
"settingDefaults": { \n\
|
||||
"breadcrumbs": true \n\
|
||||
"breadcrumbs": true \n\
|
||||
} \n\
|
||||
}' > webapp/config.json
|
||||
}' > webapp/config.json
|
||||
|
||||
FROM nginx
|
||||
# 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.
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
RUN echo $'\
|
||||
server { \n\
|
||||
server { \n\
|
||||
listen 80; \n\
|
||||
add_header \'Service-Worker-Allowed\' \'/\'; \n\
|
||||
location / { \n\
|
||||
root /usr/share/nginx/html; \n\
|
||||
index index.html index.htm; \n\
|
||||
root /usr/share/nginx/html; \n\
|
||||
index index.html index.htm; \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
|
||||
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:
|
||||
|
||||
- [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
|
||||
repository, run:
|
||||
The `Dockerfile` builds the base image which contains all of the Dendrite
|
||||
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.
|
||||
|
||||
```
|
||||
docker build . -t matrixdotorg/dendrite-monolith
|
||||
```
|
||||
## Compose files
|
||||
|
||||
## Compose file
|
||||
There are three sample `docker-compose` files:
|
||||
|
||||
There is one sample `docker-compose` files:
|
||||
|
||||
- `docker-compose.yml` which runs a Dendrite deployment with Postgres
|
||||
- `docker-compose.deps.yml` which runs the Postgres and Kafka prerequisites
|
||||
- `docker-compose.monolith.yml` which runs a monolith Dendrite deployment
|
||||
- `docker-compose.polylith.yml` which runs a polylith Dendrite deployment
|
||||
|
||||
## Configuration
|
||||
|
||||
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
||||
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`
|
||||
- `server.crt` certificate file
|
||||
- `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.
|
||||
|
||||
## 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
|
||||
|
|
|
|||
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)
|
||||
|
||||
|
|
@ -6,6 +6,5 @@ TAG=${1:-latest}
|
|||
|
||||
echo "Building tag '${TAG}'"
|
||||
|
||||
docker build . --target monolith -t matrixdotorg/dendrite-monolith:${TAG}
|
||||
docker build . --target demo-pinecone -t matrixdotorg/dendrite-demo-pinecone:${TAG}
|
||||
docker build . --target demo-yggdrasil -t matrixdotorg/dendrite-demo-yggdrasil:${TAG}
|
||||
docker build -t matrixdotorg/dendrite-monolith:${TAG} -f build/docker/Dockerfile.monolith .
|
||||
docker build -t matrixdotorg/dendrite-polylith:${TAG} -f build/docker/Dockerfile.polylith .
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
#!/usr/bin/env bash
|
||||
#!/bin/bash
|
||||
|
||||
TAG=${1:-latest}
|
||||
|
||||
echo "Pulling tag '${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}
|
||||
|
||||
echo "Pushing tag '${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"
|
||||
in
|
||||
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 ;;
|
||||
esac
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"net/http"
|
||||
"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/cmd/dendrite-demo-pinecone/conduit"
|
||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/monolith"
|
||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/relay"
|
||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/conn"
|
||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-pinecone/rooms"
|
||||
"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/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/userapi"
|
||||
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/pinecone/types"
|
||||
"github.com/sirupsen/logrus"
|
||||
"golang.org/x/net/http2"
|
||||
"golang.org/x/net/http2/h2c"
|
||||
|
||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||
"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"
|
||||
)
|
||||
|
|
@ -49,250 +52,114 @@ const (
|
|||
PeerTypeRemote = pineconeRouter.PeerTypeRemote
|
||||
PeerTypeMulticast = pineconeRouter.PeerTypeMulticast
|
||||
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 {
|
||||
logger logrus.Logger
|
||||
p2pMonolith monolith.P2PMonolith
|
||||
StorageDirectory string
|
||||
CacheDirectory string
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) PublicKey() string {
|
||||
return m.p2pMonolith.Router.PublicKey().String()
|
||||
logger logrus.Logger
|
||||
PineconeRouter *pineconeRouter.Router
|
||||
PineconeMulticast *pineconeMulticast.Multicast
|
||||
PineconeQUIC *pineconeSessions.Sessions
|
||||
StorageDirectory string
|
||||
CacheDirectory string
|
||||
staticPeerURI string
|
||||
staticPeerMutex sync.RWMutex
|
||||
staticPeerAttempt chan struct{}
|
||||
listener net.Listener
|
||||
httpServer *http.Server
|
||||
processContext *process.ProcessContext
|
||||
userAPI userapiAPI.UserInternalAPI
|
||||
}
|
||||
|
||||
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 {
|
||||
return m.p2pMonolith.Router.PeerCount(peertype)
|
||||
return m.PineconeRouter.PeerCount(peertype)
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) SessionCount() int {
|
||||
return len(m.p2pMonolith.Sessions.Protocol(monolith.SessionProtocol).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)
|
||||
return len(m.PineconeQUIC.Sessions())
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
||||
if enabled {
|
||||
m.p2pMonolith.Multicast.Start()
|
||||
m.PineconeMulticast.Start()
|
||||
} else {
|
||||
m.p2pMonolith.Multicast.Stop()
|
||||
m.DisconnectType(int(pineconeRouter.PeerTypeMulticast))
|
||||
m.PineconeMulticast.Stop()
|
||||
m.DisconnectType(pineconeRouter.PeerTypeMulticast)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||
m.p2pMonolith.ConnManager.RemovePeers()
|
||||
for _, uri := range strings.Split(uri, ",") {
|
||||
m.p2pMonolith.ConnManager.AddPeer(strings.TrimSpace(uri))
|
||||
m.staticPeerMutex.Lock()
|
||||
m.staticPeerURI = 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) {
|
||||
for _, p := range m.p2pMonolith.Router.Peers() {
|
||||
if int(peertype) == p.PeerType {
|
||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||
for _, p := range m.PineconeRouter.Peers() {
|
||||
if peertype == p.PeerType {
|
||||
_ = m.PineconeRouter.Disconnect(types.SwitchPortID(p.Port), nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) DisconnectZone(zone string) {
|
||||
for _, p := range m.p2pMonolith.Router.Peers() {
|
||||
for _, p := range m.PineconeRouter.Peers() {
|
||||
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) {
|
||||
m.p2pMonolith.Router.Disconnect(types.SwitchPortID(port), nil)
|
||||
func (m *DendriteMonolith) DisconnectPort(port int) error {
|
||||
return m.PineconeRouter.Disconnect(types.SwitchPortID(port), nil)
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) Conduit(zone string, peertype int) (*Conduit, error) {
|
||||
l, r := net.Pipe()
|
||||
newConduit := Conduit{conduit.NewConduit(r, 0)}
|
||||
conduit := &Conduit{conn: r, port: 0}
|
||||
go func() {
|
||||
logrus.Errorf("Attempting authenticated connect")
|
||||
var port types.SwitchPortID
|
||||
var err error
|
||||
if port, err = m.p2pMonolith.Router.Connect(
|
||||
l,
|
||||
pineconeRouter.ConnectionZone(zone),
|
||||
pineconeRouter.ConnectionPeerType(peertype),
|
||||
); err != nil {
|
||||
logrus.Errorf("Authenticated connect failed: %s", err)
|
||||
_ = l.Close()
|
||||
_ = r.Close()
|
||||
_ = newConduit.Close()
|
||||
return
|
||||
conduit.portMutex.Lock()
|
||||
defer conduit.portMutex.Unlock()
|
||||
loop:
|
||||
for i := 1; i <= 10; i++ {
|
||||
logrus.Errorf("Attempting authenticated connect (attempt %d)", i)
|
||||
var err error
|
||||
conduit.port, err = m.PineconeRouter.AuthenticatedConnect(l, zone, peertype)
|
||||
switch err {
|
||||
case io.ErrClosedPipe:
|
||||
logrus.Errorf("Authenticated connect failed due to closed pipe (attempt %d)", i)
|
||||
return
|
||||
case io.EOF:
|
||||
logrus.Errorf("Authenticated connect failed due to EOF (attempt %d)", i)
|
||||
break loop
|
||||
case nil:
|
||||
logrus.Errorf("Authenticated connect succeeded, connected to port %d (attempt %d)", conduit.port, i)
|
||||
return
|
||||
default:
|
||||
logrus.WithError(err).Errorf("Authenticated connect failed (attempt %d)", i)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
}
|
||||
newConduit.SetPort(port)
|
||||
logrus.Infof("Authenticated connect succeeded (port %d)", newConduit.Port())
|
||||
_ = l.Close()
|
||||
_ = r.Close()
|
||||
}()
|
||||
return &newConduit, nil
|
||||
return conduit, nil
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, error) {
|
||||
pubkey := m.p2pMonolith.Router.PublicKey()
|
||||
pubkey := m.PineconeRouter.PublicKey()
|
||||
userID := userutil.MakeUserID(
|
||||
localpart,
|
||||
spec.ServerName(hex.EncodeToString(pubkey[:])),
|
||||
gomatrixserverlib.ServerName(hex.EncodeToString(pubkey[:])),
|
||||
)
|
||||
userReq := &userapiAPI.PerformAccountCreationRequest{
|
||||
AccountType: userapiAPI.AccountTypeUser,
|
||||
|
|
@ -300,7 +167,7 @@ func (m *DendriteMonolith) RegisterUser(localpart, password string) (string, err
|
|||
Password: password,
|
||||
}
|
||||
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, nil
|
||||
|
|
@ -318,7 +185,7 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
|||
AccessToken: hex.EncodeToString(accessTokenBytes[:n]),
|
||||
}
|
||||
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)
|
||||
}
|
||||
if !loginRes.DeviceCreated {
|
||||
|
|
@ -327,10 +194,58 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
|||
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() {
|
||||
keyfile := filepath.Join(m.StorageDirectory, "p2p.pem")
|
||||
oldKeyfile := filepath.Join(m.StorageDirectory, "p2p.key")
|
||||
sk, pk := monolith.GetOrCreateKey(keyfile, oldKeyfile)
|
||||
var err error
|
||||
var sk ed25519.PrivateKey
|
||||
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{
|
||||
Out: BindLogger{},
|
||||
|
|
@ -338,29 +253,177 @@ func (m *DendriteMonolith) Start() {
|
|||
m.logger.SetOutput(BindLogger{})
|
||||
logrus.SetOutput(BindLogger{})
|
||||
|
||||
m.p2pMonolith = monolith.P2PMonolith{}
|
||||
m.p2pMonolith.SetupPinecone(sk)
|
||||
logger := log.New(os.Stdout, "PINECONE: ", 0)
|
||||
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)
|
||||
cfg := monolith.GenerateDefaultConfig(sk, m.StorageDirectory, m.CacheDirectory, prefix)
|
||||
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||
cfg := &config.Dendrite{}
|
||||
cfg.Defaults()
|
||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
||||
cfg.Global.PrivateKey = sk
|
||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||
cfg.Global.JetStream.InMemory = false
|
||||
// NOTE : disabled for now since there is a 64 bit alignment panic on 32 bit systems
|
||||
// This isn't actually fixed: https://github.com/blevesearch/zapx/pull/147
|
||||
cfg.SyncAPI.Fulltext.Enabled = false
|
||||
cfg.Global.Kafka.UseNaffka = true
|
||||
cfg.Global.Kafka.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-naffka.db", m.StorageDirectory, prefix))
|
||||
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/%s-account.db", m.StorageDirectory, prefix))
|
||||
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()
|
||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||
routers := httputil.NewRouters()
|
||||
base := setup.NewBaseDendrite(cfg, "Monolith", false)
|
||||
defer base.Close() // nolint: errcheck
|
||||
|
||||
enableRelaying := false
|
||||
enableMetrics := false
|
||||
enableWebsockets := false
|
||||
m.p2pMonolith.SetupDendrite(processCtx, cfg, cm, routers, 65432, enableRelaying, enableMetrics, enableWebsockets)
|
||||
m.p2pMonolith.StartMonolith()
|
||||
accountDB := base.CreateAccountsDB()
|
||||
federation := conn.CreateFederationClient(base, m.PineconeQUIC)
|
||||
|
||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||
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() {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
package gobind
|
||||
|
|
|
|||
|
|
@ -2,38 +2,28 @@ package gobind
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
"github.com/gorilla/mux"
|
||||
"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/yggconn"
|
||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/yggrooms"
|
||||
"github.com/matrix-org/dendrite/federationapi"
|
||||
"github.com/matrix-org/dendrite/federationapi/api"
|
||||
"github.com/matrix-org/dendrite/internal"
|
||||
"github.com/matrix-org/dendrite/internal/caching"
|
||||
"github.com/matrix-org/dendrite/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/sqlutil"
|
||||
"github.com/matrix-org/dendrite/keyserver"
|
||||
"github.com/matrix-org/dendrite/roomserver"
|
||||
"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/jetstream"
|
||||
"github.com/matrix-org/dendrite/setup/process"
|
||||
"github.com/matrix-org/dendrite/test"
|
||||
"github.com/matrix-org/dendrite/userapi"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
_ "golang.org/x/mobile/bind"
|
||||
|
|
@ -45,7 +35,6 @@ type DendriteMonolith struct {
|
|||
StorageDirectory string
|
||||
listener net.Listener
|
||||
httpServer *http.Server
|
||||
processContext *process.ProcessContext
|
||||
}
|
||||
|
||||
func (m *DendriteMonolith) BaseURL() string {
|
||||
|
|
@ -73,183 +62,112 @@ func (m *DendriteMonolith) DisconnectMulticastPeers() {
|
|||
}
|
||||
|
||||
func (m *DendriteMonolith) Start() {
|
||||
var pk ed25519.PublicKey
|
||||
var sk ed25519.PrivateKey
|
||||
|
||||
m.logger = logrus.Logger{
|
||||
Out: BindLogger{},
|
||||
}
|
||||
m.logger.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
|
||||
m.listener, err = net.Listen("tcp", "localhost:65432")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ygg, err := yggconn.Setup(sk, "dendrite", m.StorageDirectory, "", "")
|
||||
ygg, err := yggconn.Setup("dendrite", m.StorageDirectory, "")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
m.YggdrasilNode = ygg
|
||||
|
||||
cfg := &config.Dendrite{}
|
||||
cfg.Defaults(config.DefaultOpts{
|
||||
Generate: true,
|
||||
SingleDatabase: true,
|
||||
})
|
||||
cfg.Global.ServerName = spec.ServerName(hex.EncodeToString(pk))
|
||||
cfg.Global.PrivateKey = sk
|
||||
cfg.Defaults()
|
||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
||||
cfg.Global.PrivateKey = ygg.PrivateKey()
|
||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||
cfg.Global.JetStream.StoragePath = config.Path(fmt.Sprintf("%s/", m.StorageDirectory))
|
||||
cfg.Global.JetStream.InMemory = true
|
||||
cfg.Global.Kafka.UseNaffka = 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.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.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.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.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.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory))
|
||||
cfg.ClientAPI.RegistrationDisabled = false
|
||||
cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true
|
||||
if err = cfg.Derive(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
configErrors := &config.ConfigErrors{}
|
||||
cfg.Verify(configErrors)
|
||||
if len(*configErrors) > 0 {
|
||||
for _, err := range *configErrors {
|
||||
logrus.Errorf("Configuration error: %s", err)
|
||||
}
|
||||
logrus.Fatalf("Failed to start due to configuration errors")
|
||||
}
|
||||
base := setup.NewBaseDendrite(cfg, "Monolith", false)
|
||||
defer base.Close() // nolint: errcheck
|
||||
|
||||
internal.SetupStdLogging()
|
||||
internal.SetupHookLogging(cfg.Logging)
|
||||
internal.SetupPprof()
|
||||
|
||||
logrus.Infof("Dendrite version %s", internal.VersionString())
|
||||
|
||||
if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled {
|
||||
logrus.Warn("Open registration is enabled")
|
||||
}
|
||||
|
||||
closer, err := cfg.SetupTracing()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Panicf("failed to start opentracing")
|
||||
}
|
||||
defer closer.Close()
|
||||
|
||||
if cfg.Global.Sentry.Enabled {
|
||||
logrus.Info("Setting up Sentry for debugging...")
|
||||
err = sentry.Init(sentry.ClientOptions{
|
||||
Dsn: cfg.Global.Sentry.DSN,
|
||||
Environment: cfg.Global.Sentry.Environment,
|
||||
Debug: true,
|
||||
ServerName: string(cfg.Global.ServerName),
|
||||
Release: "dendrite@" + internal.VersionString(),
|
||||
AttachStacktrace: true,
|
||||
})
|
||||
if err != nil {
|
||||
logrus.WithError(err).Panic("failed to start Sentry")
|
||||
}
|
||||
}
|
||||
processCtx := process.NewProcessContext()
|
||||
cm := sqlutil.NewConnectionManager(processCtx, cfg.Global.DatabaseOptions)
|
||||
routers := httputil.NewRouters()
|
||||
basepkg.ConfigureAdminEndpoints(processCtx, routers)
|
||||
m.processContext = processCtx
|
||||
defer func() {
|
||||
processCtx.ShutdownDendrite()
|
||||
processCtx.WaitForShutdown()
|
||||
}() // nolint: errcheck
|
||||
|
||||
federation := ygg.CreateFederationClient(cfg)
|
||||
accountDB := base.CreateAccountsDB()
|
||||
federation := ygg.CreateFederationClient(base)
|
||||
|
||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||
keyRing := serverKeyAPI.KeyRing()
|
||||
|
||||
caches := caching.NewRistrettoCache(cfg.Global.Cache.EstimatedMaxSize, cfg.Global.Cache.MaxAge, caching.EnableMetrics)
|
||||
natsInstance := jetstream.NATSInstance{}
|
||||
rsAPI := roomserver.NewInternalAPI(processCtx, cfg, cm, &natsInstance, caches, caching.EnableMetrics)
|
||||
|
||||
fsAPI := federationapi.NewInternalAPI(
|
||||
processCtx, cfg, cm, &natsInstance, federation, rsAPI, caches, keyRing, true,
|
||||
rsAPI := roomserver.NewInternalAPI(
|
||||
base, keyRing,
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
||||
rsAPI.SetFederationAPI(fsAPI, keyRing)
|
||||
rsAPI.SetFederationSenderAPI(fsAPI)
|
||||
|
||||
monolith := setup.Monolith{
|
||||
Config: cfg,
|
||||
Client: ygg.CreateClient(),
|
||||
Config: base.Cfg,
|
||||
AccountDB: accountDB,
|
||||
Client: ygg.CreateClient(base),
|
||||
FedClient: federation,
|
||||
KeyRing: keyRing,
|
||||
|
||||
AppserviceAPI: asAPI,
|
||||
FederationAPI: fsAPI,
|
||||
RoomserverAPI: rsAPI,
|
||||
UserAPI: userAPI,
|
||||
AppserviceAPI: asAPI,
|
||||
EDUInternalAPI: eduInputAPI,
|
||||
FederationSenderAPI: fsAPI,
|
||||
RoomserverAPI: rsAPI,
|
||||
UserAPI: userAPI,
|
||||
KeyAPI: keyAPI,
|
||||
ExtPublicRoomsProvider: yggrooms.NewYggdrasilRoomProvider(
|
||||
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.PathPrefix(httputil.PublicClientPathPrefix).Handler(routers.Client)
|
||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||
httpRouter.PathPrefix(httputil.DendriteAdminPathPrefix).Handler(routers.DendriteAdmin)
|
||||
httpRouter.PathPrefix(httputil.SynapseAdminPathPrefix).Handler(routers.SynapseAdmin)
|
||||
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
||||
httpRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||
|
||||
yggRouter := mux.NewRouter()
|
||||
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(routers.Federation)
|
||||
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(routers.Media)
|
||||
yggRouter.PathPrefix(httputil.PublicFederationPathPrefix).Handler(base.PublicFederationAPIMux)
|
||||
yggRouter.PathPrefix(httputil.PublicMediaPathPrefix).Handler(base.PublicMediaAPIMux)
|
||||
|
||||
// Build both ends of a HTTP multiplex.
|
||||
m.httpServer = &http.Server{
|
||||
|
|
@ -266,11 +184,11 @@ func (m *DendriteMonolith) Start() {
|
|||
|
||||
go func() {
|
||||
m.logger.Info("Listening on ", ygg.DerivedServerName())
|
||||
m.logger.Error(m.httpServer.Serve(ygg))
|
||||
m.logger.Fatal(m.httpServer.Serve(ygg))
|
||||
}()
|
||||
go func() {
|
||||
logrus.Info("Listening on ", m.listener.Addr())
|
||||
logrus.Error(http.Serve(m.listener, httpRouter))
|
||||
logrus.Fatal(http.Serve(m.listener, httpRouter))
|
||||
}()
|
||||
go func() {
|
||||
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 {
|
||||
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
|
||||
|
||||
package gobind
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
//go:build !ios
|
||||
// +build !ios
|
||||
|
||||
package gobind
|
||||
|
|
|
|||
|
|
@ -1,36 +1,21 @@
|
|||
#syntax=docker/dockerfile:1.2
|
||||
|
||||
FROM golang:1.22-bookworm as build
|
||||
FROM golang:1.13-stretch as build
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
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
|
||||
# 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
|
||||
COPY go.mod .
|
||||
COPY go.sum .
|
||||
RUN go mod download
|
||||
|
||||
WORKDIR /dendrite
|
||||
RUN ./generate-keys --private-key matrix_key.pem
|
||||
COPY . .
|
||||
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 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 ./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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
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!
|
||||
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
|
||||
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
|
||||
echo "Looking for lint..."
|
||||
|
|
@ -33,7 +33,7 @@ echo "Looking for lint..."
|
|||
# Capture exit code to ensure go.{mod,sum} is restored before exiting
|
||||
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}
|
||||
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
|
||||
|
||||
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.
|
||||
type ExtraPublicRoomsProvider interface {
|
||||
// Rooms returns the extra rooms. This is called on-demand by clients, so cache appropriately.
|
||||
Rooms() []fclient.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"`
|
||||
Rooms() []gomatrixserverlib.PublicRoom
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,8 +23,8 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
|
|
@ -42,7 +42,6 @@ type DeviceDatabase interface {
|
|||
type AccountDatabase interface {
|
||||
// Look up the account matching the given localpart.
|
||||
GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error)
|
||||
GetAccountByPassword(ctx context.Context, localpart, password string) (*api.Account, error)
|
||||
}
|
||||
|
||||
// VerifyUserFromRequest authenticates the HTTP request,
|
||||
|
|
@ -51,14 +50,14 @@ type AccountDatabase interface {
|
|||
// Note: For an AS user, AS dummy device is returned.
|
||||
// On failure returns an JSON error response which can be sent to the client.
|
||||
func VerifyUserFromRequest(
|
||||
req *http.Request, userAPI api.QueryAcccessTokenAPI,
|
||||
req *http.Request, userAPI api.UserInternalAPI,
|
||||
) (*api.Device, *util.JSONResponse) {
|
||||
// Try to find the Application Service user
|
||||
token, err := ExtractAccessToken(req)
|
||||
if err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.MissingToken(err.Error()),
|
||||
JSON: jsonerror.MissingToken(err.Error()),
|
||||
}
|
||||
}
|
||||
var res api.QueryAccessTokenResponse
|
||||
|
|
@ -68,23 +67,21 @@ func VerifyUserFromRequest(
|
|||
}, &res)
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.QueryAccessToken failed")
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
jsonErr := jsonerror.InternalServerError()
|
||||
return nil, &jsonErr
|
||||
}
|
||||
if res.Err != "" {
|
||||
if strings.HasPrefix(strings.ToLower(res.Err), "forbidden:") { // TODO: use actual error and no string comparison
|
||||
if res.Err != nil {
|
||||
if forbidden, ok := res.Err.(*api.ErrorForbidden); ok {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
JSON: spec.Forbidden(res.Err),
|
||||
JSON: jsonerror.Forbidden(forbidden.Message),
|
||||
}
|
||||
}
|
||||
}
|
||||
if res.Device == nil {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.UnknownToken("Unknown token"),
|
||||
JSON: jsonerror.UnknownToken("Unknown token"),
|
||||
}
|
||||
}
|
||||
return res.Device, nil
|
||||
|
|
|
|||
|
|
@ -10,5 +10,4 @@ const (
|
|||
LoginTypeSharedSecret = "org.matrix.login.shared_secret"
|
||||
LoginTypeRecaptcha = "m.login.recaptcha"
|
||||
LoginTypeApplicationService = "m.login.application_service"
|
||||
LoginTypeToken = "m.login.token"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ package authtypes
|
|||
// Profile represents the profile for a Matrix account.
|
||||
type Profile struct {
|
||||
Localpart string `json:"local_part"`
|
||||
ServerName string `json:"server_name,omitempty"` // NOTSPEC: only set by Pinecone user provider
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,6 @@ package authtypes
|
|||
|
||||
// ThreePID represents a third-party identifier
|
||||
type ThreePID struct {
|
||||
Address string `json:"address"`
|
||||
Medium string `json:"medium"`
|
||||
AddedAt int64 `json:"added_at"`
|
||||
ValidatedAt int64 `json:"validated_at"`
|
||||
Address string `json:"address"`
|
||||
Medium string `json:"medium"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
||||
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/clientapi/userutil"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"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 {
|
||||
Login
|
||||
|
|
@ -42,21 +39,11 @@ type LoginTypePassword struct {
|
|||
}
|
||||
|
||||
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) {
|
||||
var r 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) Request() interface{} {
|
||||
return &PasswordRequest{}
|
||||
}
|
||||
|
||||
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 == "" {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("A username must be supplied."),
|
||||
JSON: jsonerror.BadJSON("A username must be supplied."),
|
||||
}
|
||||
}
|
||||
if len(r.Password) == 0 {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.BadJSON("A password must be supplied."),
|
||||
}
|
||||
}
|
||||
localpart, domain, err := userutil.ParseUsernameParam(username, t.Config.Matrix)
|
||||
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
|
||||
if err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusUnauthorized,
|
||||
JSON: spec.InvalidUsername(err.Error()),
|
||||
JSON: jsonerror.InvalidUsername(err.Error()),
|
||||
}
|
||||
}
|
||||
if !t.Config.Matrix.IsLocalServerName(domain) {
|
||||
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)
|
||||
_, err = t.GetAccountByPassword(ctx, localpart, r.Password)
|
||||
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
|
||||
// but that would leak the existence of the user.
|
||||
if !res.Exists {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
JSON: spec.Forbidden("The username or password was incorrect or the account does not exist."),
|
||||
}
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusForbidden,
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,11 +18,10 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/tidwall/gjson"
|
||||
|
|
@ -33,29 +32,27 @@ import (
|
|||
type Type interface {
|
||||
// Name returns the name of the auth type e.g `m.login.password`
|
||||
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.
|
||||
// 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
|
||||
// `req` is guaranteed to be the type returned from Request()
|
||||
// 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:
|
||||
// "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,
|
||||
// with the addition of the standard errcode and error fields describing the error."
|
||||
//
|
||||
// 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)
|
||||
Login(ctx context.Context, req interface{}) (login *Login, errRes *util.JSONResponse)
|
||||
// TODO: Extend to support Register() flow
|
||||
// Register(ctx context.Context, sessionID string, req interface{})
|
||||
}
|
||||
|
||||
type LoginCleanupFunc func(context.Context, *util.JSONResponse)
|
||||
|
||||
// LoginIdentifier represents identifier types
|
||||
// https://matrix.org/docs/spec/client_server/r0.6.1#identifier-types
|
||||
type LoginIdentifier struct {
|
||||
Type string `json:"type"`
|
||||
// when type = m.id.user or m.id.application_service
|
||||
// when type = m.id.user
|
||||
User string `json:"user"`
|
||||
// when type = m.id.thirdparty
|
||||
Medium string `json:"medium"`
|
||||
|
|
@ -64,8 +61,11 @@ type LoginIdentifier struct {
|
|||
|
||||
// Login represents the shared fields used in all forms of login/sudo endpoints.
|
||||
type Login struct {
|
||||
LoginIdentifier // Flat fields deprecated in favour of `identifier`.
|
||||
Identifier LoginIdentifier `json:"identifier"`
|
||||
Type string `json:"type"`
|
||||
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 ("")
|
||||
// Thus a pointer is needed to differentiate between the two
|
||||
|
|
@ -103,20 +103,22 @@ type userInteractiveFlow struct {
|
|||
// the user already has a valid access token, but we want to double-check
|
||||
// that it isn't stolen by re-authenticating them.
|
||||
type UserInteractive struct {
|
||||
sync.RWMutex
|
||||
Flows []userInteractiveFlow
|
||||
Completed []string
|
||||
Flows []userInteractiveFlow
|
||||
// Map of login type to implementation
|
||||
Types map[string]Type
|
||||
// Map of session ID to completed login types, will need to be extended in future
|
||||
Sessions map[string][]string
|
||||
}
|
||||
|
||||
func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive {
|
||||
func NewUserInteractive(getAccByPass GetAccountByPassword, cfg *config.ClientAPI) *UserInteractive {
|
||||
typePassword := &LoginTypePassword{
|
||||
GetAccountByPassword: userAccountAPI.QueryAccountByPassword,
|
||||
GetAccountByPassword: getAccByPass,
|
||||
Config: cfg,
|
||||
}
|
||||
// TODO: Add SSO login
|
||||
return &UserInteractive{
|
||||
Completed: []string{},
|
||||
Flows: []userInteractiveFlow{
|
||||
{
|
||||
Stages: []string{typePassword.Name()},
|
||||
|
|
@ -130,8 +132,6 @@ func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI)
|
|||
}
|
||||
|
||||
func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
||||
u.RLock()
|
||||
defer u.RUnlock()
|
||||
for _, f := range u.Flows {
|
||||
if len(f.Stages) == 1 && f.Stages[0] == authType {
|
||||
return true
|
||||
|
|
@ -141,34 +141,26 @@ func (u *UserInteractive) IsSingleStageFlow(authType string) bool {
|
|||
}
|
||||
|
||||
func (u *UserInteractive) AddCompletedStage(sessionID, authType string) {
|
||||
u.Lock()
|
||||
// TODO: Handle multi-stage flows
|
||||
u.Completed = append(u.Completed, authType)
|
||||
delete(u.Sessions, sessionID)
|
||||
u.Unlock()
|
||||
}
|
||||
|
||||
type Challenge struct {
|
||||
Completed []string `json:"completed"`
|
||||
Flows []userInteractiveFlow `json:"flows"`
|
||||
Session string `json:"session"`
|
||||
// TODO: Return any additional `params`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}
|
||||
|
||||
// Challenge returns an HTTP 401 with the supported flows for authenticating
|
||||
func (u *UserInteractive) challenge(sessionID string) *util.JSONResponse {
|
||||
u.RLock()
|
||||
completed := u.Sessions[sessionID]
|
||||
flows := u.Flows
|
||||
u.RUnlock()
|
||||
|
||||
func (u *UserInteractive) Challenge(sessionID string) *util.JSONResponse {
|
||||
return &util.JSONResponse{
|
||||
Code: 401,
|
||||
JSON: Challenge{
|
||||
Completed: completed,
|
||||
Flows: flows,
|
||||
Session: sessionID,
|
||||
Params: make(map[string]interface{}),
|
||||
JSON: struct {
|
||||
Completed []string `json:"completed"`
|
||||
Flows []userInteractiveFlow `json:"flows"`
|
||||
Session string `json:"session"`
|
||||
// TODO: Return any additional `params`
|
||||
Params map[string]interface{} `json:"params"`
|
||||
}{
|
||||
u.Completed,
|
||||
u.Flows,
|
||||
sessionID,
|
||||
make(map[string]interface{}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -178,15 +170,11 @@ func (u *UserInteractive) NewSession() *util.JSONResponse {
|
|||
sessionID, err := GenerateAccessToken()
|
||||
if err != nil {
|
||||
logrus.WithError(err).Error("failed to generate session ID")
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
res := jsonerror.InternalServerError()
|
||||
return &res
|
||||
}
|
||||
u.Lock()
|
||||
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
|
||||
|
|
@ -195,19 +183,15 @@ func (u *UserInteractive) ResponseWithChallenge(sessionID string, response inter
|
|||
mixedObjects := make(map[string]interface{})
|
||||
b, err := json.Marshal(response)
|
||||
if err != nil {
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
ise := jsonerror.InternalServerError()
|
||||
return &ise
|
||||
}
|
||||
_ = json.Unmarshal(b, &mixedObjects)
|
||||
challenge := u.challenge(sessionID)
|
||||
challenge := u.Challenge(sessionID)
|
||||
b, err = json.Marshal(challenge.JSON)
|
||||
if err != nil {
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
ise := jsonerror.InternalServerError()
|
||||
return &ise
|
||||
}
|
||||
_ = 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
|
||||
authType := gjson.GetBytes(bodyBytes, "auth.type").Str
|
||||
|
||||
u.RLock()
|
||||
loginType, ok := u.Types[authType]
|
||||
u.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.BadJSON("Unknown auth.type: " + authType),
|
||||
JSON: jsonerror.BadJSON("Unknown auth.type: " + authType),
|
||||
}
|
||||
}
|
||||
|
||||
// retrieve the session
|
||||
sessionID := gjson.GetBytes(bodyBytes, "auth.session").Str
|
||||
|
||||
u.RLock()
|
||||
_, ok = u.Sessions[sessionID]
|
||||
u.RUnlock()
|
||||
|
||||
if !ok {
|
||||
if _, ok = u.Sessions[sessionID]; !ok {
|
||||
// if the login type is part of a single stage flow then allow them to omit the session ID
|
||||
if !u.IsSingleStageFlow(authType) {
|
||||
return nil, &util.JSONResponse{
|
||||
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))
|
||||
if resErr != nil {
|
||||
return nil, u.ResponseWithChallenge(sessionID, resErr.JSON)
|
||||
r := loginType.Request()
|
||||
if err := json.Unmarshal([]byte(gjson.GetBytes(bodyBytes, "auth").Raw), r); err != nil {
|
||||
return nil, &util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
u.AddCompletedStage(sessionID, authType)
|
||||
cleanup(ctx, nil)
|
||||
// TODO: Check if there's more stages to go and return an error
|
||||
return login, nil
|
||||
login, resErr := loginType.Login(ctx, r)
|
||||
if resErr == nil {
|
||||
u.AddCompletedStage(sessionID, authType)
|
||||
// TODO: Check if there's more stages to go and return an error
|
||||
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/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
var (
|
||||
ctx = context.Background()
|
||||
serverName = spec.ServerName("example.com")
|
||||
serverName = gomatrixserverlib.ServerName("example.com")
|
||||
// space separated localpart+password -> account
|
||||
lookup = make(map[string]*api.Account)
|
||||
device = &api.Device{
|
||||
|
|
@ -25,35 +24,21 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
type fakeAccountDatabase struct{}
|
||||
|
||||
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]
|
||||
func getAccountByPassword(ctx context.Context, localpart, plaintextPassword string) (*api.Account, error) {
|
||||
acc, ok := lookup[localpart+" "+plaintextPassword]
|
||||
if !ok {
|
||||
return fmt.Errorf("unknown user/password")
|
||||
return nil, fmt.Errorf("unknown user/password")
|
||||
}
|
||||
res.Account = acc
|
||||
res.Exists = true
|
||||
return nil
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
func setup() *UserInteractive {
|
||||
cfg := &config.ClientAPI{
|
||||
Matrix: &config.Global{
|
||||
SigningIdentity: fclient.SigningIdentity{
|
||||
ServerName: serverName,
|
||||
},
|
||||
ServerName: serverName,
|
||||
},
|
||||
}
|
||||
return NewUserInteractive(&fakeAccountDatabase{}, cfg)
|
||||
return NewUserInteractive(getAccountByPassword, cfg)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
import (
|
||||
"github.com/matrix-org/dendrite/internal/httputil"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/dendrite/setup/process"
|
||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/fclient"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
||||
"github.com/matrix-org/dendrite/clientapi/api"
|
||||
"github.com/matrix-org/dendrite/clientapi/producers"
|
||||
"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"
|
||||
keyserverAPI "github.com/matrix-org/dendrite/keyserver/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.
|
||||
func AddPublicRoutes(
|
||||
processContext *process.ProcessContext,
|
||||
routers httputil.Routers,
|
||||
cfg *config.Dendrite,
|
||||
natsInstance *jetstream.NATSInstance,
|
||||
federation fclient.FederationClient,
|
||||
rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||
router *mux.Router,
|
||||
synapseAdminRouter *mux.Router,
|
||||
cfg *config.ClientAPI,
|
||||
accountsDB accounts.Database,
|
||||
federation *gomatrixserverlib.FederationClient,
|
||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
eduInputAPI eduServerAPI.EDUServerInputAPI,
|
||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||
transactionsCache *transactions.Cache,
|
||||
fsAPI federationAPI.ClientFederationAPI,
|
||||
userAPI userapi.ClientUserAPI,
|
||||
userDirectoryProvider userapi.QuerySearchProfilesAPI,
|
||||
extRoomsProvider api.ExtraPublicRoomsProvider, enableMetrics bool,
|
||||
fsAPI federationSenderAPI.FederationSenderInternalAPI,
|
||||
userAPI userapi.UserInternalAPI,
|
||||
keyAPI keyserverAPI.KeyInternalAPI,
|
||||
extRoomsProvider api.ExtraPublicRoomsProvider,
|
||||
mscCfg *config.MSCs,
|
||||
) {
|
||||
js, natsClient := natsInstance.Prepare(processContext, &cfg.Global.JetStream)
|
||||
_, producer := kafka.SetupConsumerProducer(&cfg.Matrix.Kafka)
|
||||
|
||||
syncProducer := &producers.SyncAPIProducer{
|
||||
JetStream: js,
|
||||
TopicReceiptEvent: cfg.Global.JetStream.Prefixed(jetstream.OutputReceiptEvent),
|
||||
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,
|
||||
Producer: producer,
|
||||
Topic: cfg.Matrix.Kafka.TopicFor(config.TopicOutputClientData),
|
||||
}
|
||||
|
||||
routing.Setup(
|
||||
routers,
|
||||
cfg, rsAPI, asAPI,
|
||||
userAPI, userDirectoryProvider, federation,
|
||||
syncProducer, transactionsCache, fsAPI,
|
||||
extRoomsProvider, natsClient, enableMetrics,
|
||||
router, synapseAdminRouter, cfg, eduInputAPI, rsAPI, asAPI,
|
||||
accountsDB, userAPI, federation,
|
||||
syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -16,11 +16,11 @@ package httputil
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
|
|
@ -29,23 +29,17 @@ import (
|
|||
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
|
||||
// encoding/json allows invalid utf-8, matrix does not
|
||||
// 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 {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
util.GetLogger(req.Context()).WithError(err).Error("ioutil.ReadAll failed")
|
||||
resp := jsonerror.InternalServerError()
|
||||
return &resp
|
||||
}
|
||||
|
||||
return UnmarshalJSON(body, iface)
|
||||
}
|
||||
|
||||
func UnmarshalJSON(body []byte, iface interface{}) *util.JSONResponse {
|
||||
if !utf8.Valid(body) {
|
||||
return &util.JSONResponse{
|
||||
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.
|
||||
return &util.JSONResponse{
|
||||
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
|
||||
|
|
|
|||
|
|
@ -32,8 +32,8 @@ func ParseTSParam(req *http.Request) (time.Time, error) {
|
|||
// The parameter exists, parse into a Time object
|
||||
ts, err := strconv.ParseInt(tsStr, 10, 64)
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/Shopify/sarama"
|
||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||
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
|
||||
type SyncAPIProducer struct {
|
||||
TopicReceiptEvent string
|
||||
TopicSendToDeviceEvent string
|
||||
TopicTypingEvent string
|
||||
TopicPresenceEvent string
|
||||
JetStream nats.JetStreamContext
|
||||
ServerName spec.ServerName
|
||||
UserAPI userapi.ClientUserAPI
|
||||
Topic string
|
||||
Producer sarama.SyncProducer
|
||||
}
|
||||
|
||||
func (p *SyncAPIProducer) SendReceipt(
|
||||
ctx context.Context,
|
||||
userID, roomID, eventID, receiptType string, timestamp spec.Timestamp,
|
||||
) error {
|
||||
m := &nats.Msg{
|
||||
Subject: p.TopicReceiptEvent,
|
||||
Header: nats.Header{},
|
||||
// SendData sends account data to the sync API server
|
||||
func (p *SyncAPIProducer) SendData(userID string, roomID string, dataType string) error {
|
||||
var m sarama.ProducerMessage
|
||||
|
||||
data := eventutil.AccountData{
|
||||
RoomID: roomID,
|
||||
Type: dataType,
|
||||
}
|
||||
m.Header.Set(jetstream.UserID, userID)
|
||||
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)
|
||||
value, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If the event is targeted locally then we want to expand the wildcard
|
||||
// out into individual device IDs so that we can send them to each respective
|
||||
// device. If the event isn't targeted locally then we can't expand the
|
||||
// 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)
|
||||
}
|
||||
|
||||
m.Topic = string(p.Topic)
|
||||
m.Key = sarama.StringEncoder(userID)
|
||||
m.Value = sarama.ByteEncoder(value)
|
||||
log.WithFields(log.Fields{
|
||||
"user_id": userID,
|
||||
"num_devices": len(devices),
|
||||
"type": eventType,
|
||||
}).Tracef("Producing to topic '%s'", p.TopicSendToDeviceEvent)
|
||||
for i, device := range devices {
|
||||
ote := &types.OutputSendToDeviceEvent{
|
||||
UserID: userID,
|
||||
DeviceID: device,
|
||||
SendToDeviceEvent: gomatrixserverlib.SendToDeviceEvent{
|
||||
Sender: sender,
|
||||
Type: eventType,
|
||||
Content: message,
|
||||
},
|
||||
}
|
||||
"user_id": userID,
|
||||
"room_id": roomID,
|
||||
"data_type": dataType,
|
||||
}).Infof("Producing to topic '%s'", p.Topic)
|
||||
|
||||
eventJSON, err := json.Marshal(ote)
|
||||
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))
|
||||
_, _, err = p.Producer.SendMessage(&m)
|
||||
return err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,28 +17,28 @@ package routing
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"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/internal/eventutil"
|
||||
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
// GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type}
|
||||
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,
|
||||
) util.JSONResponse {
|
||||
if userID != device.UserID {
|
||||
return util.JSONResponse{
|
||||
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{
|
||||
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}
|
||||
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,
|
||||
) util.JSONResponse {
|
||||
if userID != device.UserID {
|
||||
return util.JSONResponse{
|
||||
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 {
|
||||
return util.JSONResponse{
|
||||
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{
|
||||
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 {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("io.ReadAll failed")
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
util.GetLogger(req.Context()).WithError(err).Error("ioutil.ReadAll failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
if !json.Valid(body) {
|
||||
return util.JSONResponse{
|
||||
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)
|
||||
}
|
||||
|
||||
// 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{
|
||||
Code: http.StatusOK,
|
||||
JSON: struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
type readMarkerJSON struct {
|
||||
FullyRead string `json:"m.fully_read"`
|
||||
Read string `json:"m.read"`
|
||||
}
|
||||
|
||||
type fullyReadEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
}
|
||||
|
|
@ -142,57 +150,53 @@ type fullyReadEvent struct {
|
|||
// SaveReadMarker implements POST /rooms/{roomId}/read_markers
|
||||
func SaveReadMarker(
|
||||
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,
|
||||
) 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
|
||||
resErr := checkMemberInRoom(req.Context(), rsAPI, *deviceUserID, roomID)
|
||||
resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||
if resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
|
||||
var r eventutil.ReadMarkerJSON
|
||||
var r readMarkerJSON
|
||||
resErr = httputil.UnmarshalJSONRequest(req, &r)
|
||||
if resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
|
||||
if r.FullyRead != "" {
|
||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||
if err != nil {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
}
|
||||
|
||||
dataReq := api.InputAccountDataRequest{
|
||||
UserID: device.UserID,
|
||||
DataType: "m.fully_read",
|
||||
RoomID: roomID,
|
||||
AccountData: data,
|
||||
}
|
||||
dataRes := api.InputAccountDataResponse{}
|
||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||
return util.ErrorResponse(err)
|
||||
if r.FullyRead == "" {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: jsonerror.BadJSON("Missing m.fully_read mandatory field"),
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the read receipts that may be included in the read marker.
|
||||
data, err := json.Marshal(fullyReadEvent{EventID: r.FullyRead})
|
||||
if err != nil {
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
dataReq := api.InputAccountDataRequest{
|
||||
UserID: device.UserID,
|
||||
DataType: "m.fully_read",
|
||||
RoomID: roomID,
|
||||
AccountData: data,
|
||||
}
|
||||
dataRes := api.InputAccountDataResponse{}
|
||||
if err := userAPI.InputAccountData(req.Context(), &dataReq, &dataRes); err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("userAPI.InputAccountData failed")
|
||||
return util.ErrorResponse(err)
|
||||
}
|
||||
|
||||
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 receipt that may be included in the read marker
|
||||
if r.Read != "" {
|
||||
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read", r.Read)
|
||||
}
|
||||
if r.ReadPrivate != "" {
|
||||
return SetReceipt(req, userAPI, syncProducer, device, roomID, "m.read.private", r.ReadPrivate)
|
||||
return SetReceipt(req, eduAPI, device, roomID, "m.read", r.Read)
|
||||
}
|
||||
|
||||
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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
|
@ -44,14 +44,14 @@ type connectionInfo struct {
|
|||
|
||||
// GetAdminWhois implements GET /admin/whois/{userId}
|
||||
func GetAdminWhois(
|
||||
req *http.Request, userAPI api.ClientUserAPI, device *api.Device,
|
||||
req *http.Request, userAPI api.UserInternalAPI, device *api.Device,
|
||||
userID string,
|
||||
) util.JSONResponse {
|
||||
allowed := device.AccountType == api.AccountTypeAdmin || userID == device.UserID
|
||||
if !allowed {
|
||||
if userID != device.UserID {
|
||||
// TODO: Still allow if user is admin
|
||||
return util.JSONResponse{
|
||||
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)
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("GetAdminWhois failed to query user devices")
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
devices := make(map[string]deviceInfo)
|
||||
|
|
|
|||
|
|
@ -15,23 +15,23 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
"github.com/matrix-org/dendrite/roomserver/api"
|
||||
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/gomatrixserverlib/spec"
|
||||
|
||||
"github.com/matrix-org/util"
|
||||
)
|
||||
|
||||
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
||||
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 {
|
||||
stateTuple := gomatrixserverlib.StateKeyTuple{
|
||||
EventType: spec.MRoomHistoryVisibility,
|
||||
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
||||
StateKey: "",
|
||||
}
|
||||
stateReq := &api.QueryCurrentStateRequest{
|
||||
|
|
@ -44,40 +44,29 @@ func GetAliases(
|
|||
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
||||
}
|
||||
|
||||
visibility := gomatrixserverlib.HistoryVisibilityInvited
|
||||
visibility := "invite"
|
||||
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
||||
var err error
|
||||
var content gomatrixserverlib.HistoryVisibilityContent
|
||||
if err = json.Unmarshal(historyVisEvent.Content(), &content); err != nil {
|
||||
visibility, err = historyVisEvent.HistoryVisibility()
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("historyVisEvent.HistoryVisibility failed")
|
||||
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{
|
||||
RoomID: roomID,
|
||||
UserID: *deviceUserID,
|
||||
UserID: device.UserID,
|
||||
}
|
||||
var queryRes api.QueryMembershipForUserResponse
|
||||
if err := rsAPI.QueryMembershipForUser(req.Context(), &queryReq, &queryRes); err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryMembershipsForRoom failed")
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
}
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
if !queryRes.IsInRoom {
|
||||
return util.JSONResponse{
|
||||
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
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"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/util"
|
||||
)
|
||||
|
|
@ -31,7 +31,8 @@ const recaptchaTemplate = `
|
|||
<title>Authentication</title>
|
||||
<meta name='viewport' content='width=device-width, initial-scale=1,
|
||||
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>
|
||||
function captchaDone() {
|
||||
|
|
@ -50,8 +51,8 @@ function captchaDone() {
|
|||
Please verify that you're not a robot.
|
||||
</p>
|
||||
<input type="hidden" name="session" value="{{.session}}" />
|
||||
<div class="{{.sitekeyClass}}"
|
||||
data-sitekey="{{.sitekey}}"
|
||||
<div class="g-recaptcha"
|
||||
data-sitekey="{{.siteKey}}"
|
||||
data-callback="captchaDone">
|
||||
</div>
|
||||
<noscript>
|
||||
|
|
@ -101,38 +102,21 @@ func serveTemplate(w http.ResponseWriter, templateHTML string, data map[string]s
|
|||
func AuthFallback(
|
||||
w http.ResponseWriter, req *http.Request, authType string,
|
||||
cfg *config.ClientAPI,
|
||||
) {
|
||||
// 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
|
||||
}
|
||||
|
||||
) *util.JSONResponse {
|
||||
sessionID := req.URL.Query().Get("session")
|
||||
|
||||
if sessionID == "" {
|
||||
writeHTTPMessage(w, req,
|
||||
return writeHTTPMessage(w, req,
|
||||
"Session ID not provided",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
serveRecaptcha := func() {
|
||||
data := map[string]string{
|
||||
"myUrl": req.URL.String(),
|
||||
"session": sessionID,
|
||||
"apiJsUrl": cfg.RecaptchaApiJsUrl,
|
||||
"sitekey": cfg.RecaptchaPublicKey,
|
||||
"sitekeyClass": cfg.RecaptchaSitekeyClass,
|
||||
"formField": cfg.RecaptchaFormField,
|
||||
"myUrl": req.URL.String(),
|
||||
"session": sessionID,
|
||||
"siteKey": cfg.RecaptchaPublicKey,
|
||||
}
|
||||
serveTemplate(w, recaptchaTemplate, data)
|
||||
}
|
||||
|
|
@ -144,44 +128,70 @@ func AuthFallback(
|
|||
|
||||
if req.Method == http.MethodGet {
|
||||
// Handle Recaptcha
|
||||
serveRecaptcha()
|
||||
return
|
||||
if authType == authtypes.LoginTypeRecaptcha {
|
||||
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
serveRecaptcha()
|
||||
return nil
|
||||
}
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusNotFound,
|
||||
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
||||
}
|
||||
} else if req.Method == http.MethodPost {
|
||||
// Handle Recaptcha
|
||||
clientIP := req.RemoteAddr
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
serveRecaptcha()
|
||||
return
|
||||
if authType == authtypes.LoginTypeRecaptcha {
|
||||
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientIP := req.RemoteAddr
|
||||
err := req.ParseForm()
|
||||
if err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
||||
res := jsonerror.InternalServerError()
|
||||
return &res
|
||||
}
|
||||
|
||||
response := req.Form.Get("g-recaptcha-response")
|
||||
if err := validateRecaptcha(cfg, response, clientIP); err != nil {
|
||||
util.GetLogger(req.Context()).Error(err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Success. Add recaptcha as a completed login flow
|
||||
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||
|
||||
serveSuccess()
|
||||
return nil
|
||||
}
|
||||
|
||||
response := req.Form.Get(cfg.RecaptchaFormField)
|
||||
err = validateRecaptcha(cfg, response, clientIP)
|
||||
switch err {
|
||||
case ErrMissingResponse:
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
serveRecaptcha() // serve the initial page again, instead of nothing
|
||||
return
|
||||
case ErrInvalidCaptcha:
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
serveRecaptcha()
|
||||
return
|
||||
case nil:
|
||||
default: // something else failed
|
||||
util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha")
|
||||
serveRecaptcha()
|
||||
return
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusNotFound,
|
||||
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
||||
}
|
||||
|
||||
// Success. Add recaptcha as a completed login flow
|
||||
sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||
|
||||
serveSuccess()
|
||||
return
|
||||
}
|
||||
writeHTTPMessage(w, req, "Bad method", http.StatusMethodNotAllowed)
|
||||
return &util.JSONResponse{
|
||||
Code: http.StatusMethodNotAllowed,
|
||||
JSON: jsonerror.NotFound("Bad method"),
|
||||
}
|
||||
}
|
||||
|
||||
// checkRecaptchaEnabled creates an error response if recaptcha is not usable on homeserver.
|
||||
func checkRecaptchaEnabled(
|
||||
cfg *config.ClientAPI,
|
||||
w http.ResponseWriter,
|
||||
req *http.Request,
|
||||
) *util.JSONResponse {
|
||||
if !cfg.RecaptchaEnabled {
|
||||
return writeHTTPMessage(w, req,
|
||||
"Recaptcha login is disabled on this Homeserver",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeHTTPMessage writes the given header and message to the HTTP response writer.
|
||||
|
|
@ -189,10 +199,13 @@ func AuthFallback(
|
|||
func writeHTTPMessage(
|
||||
w http.ResponseWriter, req *http.Request,
|
||||
message string, header int,
|
||||
) {
|
||||
) *util.JSONResponse {
|
||||
w.WriteHeader(header)
|
||||
_, err := w.Write([]byte(message))
|
||||
if err != nil {
|
||||
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 (
|
||||
"net/http"
|
||||
|
||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||
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"
|
||||
)
|
||||
|
||||
// GetCapabilities returns information about the server's supported feature set
|
||||
// and other relevant capabilities to an authenticated user.
|
||||
func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse {
|
||||
versionsMap := map[gomatrixserverlib.RoomVersion]string{}
|
||||
for v, desc := range version.SupportedRoomVersions() {
|
||||
if desc.Stable() {
|
||||
versionsMap[v] = "stable"
|
||||
} else {
|
||||
versionsMap[v] = "unstable"
|
||||
}
|
||||
func GetCapabilities(
|
||||
req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
) util.JSONResponse {
|
||||
roomVersionsQueryReq := roomserverAPI.QueryRoomVersionCapabilitiesRequest{}
|
||||
roomVersionsQueryRes := roomserverAPI.QueryRoomVersionCapabilitiesResponse{}
|
||||
if err := rsAPI.QueryRoomVersionCapabilities(
|
||||
req.Context(),
|
||||
&roomVersionsQueryReq,
|
||||
&roomVersionsQueryRes,
|
||||
); err != nil {
|
||||
util.GetLogger(req.Context()).WithError(err).Error("queryAPI.QueryRoomVersionCapabilities failed")
|
||||
return jsonerror.InternalServerError()
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
|
|
@ -40,10 +44,7 @@ func GetCapabilities(rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse
|
|||
"m.change_password": map[string]bool{
|
||||
"enabled": true,
|
||||
},
|
||||
"m.room_versions": map[string]interface{}{
|
||||
"default": rsAPI.DefaultRoomVersion(),
|
||||
"available": versionsMap,
|
||||
},
|
||||
"m.room_versions": roomVersionsQueryRes,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,7 +15,6 @@
|
|||
package routing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -26,10 +25,12 @@ import (
|
|||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||
roomserverVersion "github.com/matrix-org/dendrite/roomserver/version"
|
||||
"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/jsonerror"
|
||||
"github.com/matrix-org/dendrite/internal/eventutil"
|
||||
"github.com/matrix-org/dendrite/setup/config"
|
||||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||
"github.com/matrix-org/gomatrixserverlib"
|
||||
"github.com/matrix-org/util"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
|
@ -37,19 +38,32 @@ import (
|
|||
|
||||
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
|
||||
type createRoomRequest struct {
|
||||
Invite []string `json:"invite"`
|
||||
Name string `json:"name"`
|
||||
Visibility string `json:"visibility"`
|
||||
Topic string `json:"topic"`
|
||||
Preset string `json:"preset"`
|
||||
CreationContent json.RawMessage `json:"creation_content"`
|
||||
InitialState []gomatrixserverlib.FledglingEvent `json:"initial_state"`
|
||||
RoomAliasName string `json:"room_alias_name"`
|
||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
||||
IsDirect bool `json:"is_direct"`
|
||||
Invite []string `json:"invite"`
|
||||
Name string `json:"name"`
|
||||
Visibility string `json:"visibility"`
|
||||
Topic string `json:"topic"`
|
||||
Preset string `json:"preset"`
|
||||
CreationContent json.RawMessage `json:"creation_content"`
|
||||
InitialState []fledglingEvent `json:"initial_state"`
|
||||
RoomAliasName string `json:"room_alias_name"`
|
||||
GuestCanJoin bool `json:"guest_can_join"`
|
||||
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
|
||||
PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"`
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
|
|
@ -57,23 +71,28 @@ func (r createRoomRequest) Validate() *util.JSONResponse {
|
|||
if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
|
||||
return &util.JSONResponse{
|
||||
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 {
|
||||
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{
|
||||
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 {
|
||||
case spec.PresetPrivateChat, spec.PresetTrustedPrivateChat, spec.PresetPublicChat, "":
|
||||
case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
|
||||
default:
|
||||
return &util.JSONResponse{
|
||||
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 {
|
||||
return &util.JSONResponse{
|
||||
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 {
|
||||
return &util.JSONResponse{
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func CreateRoom(
|
||||
req *http.Request, device *api.Device,
|
||||
cfg *config.ClientAPI,
|
||||
profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI,
|
||||
asAPI appserviceAPI.AppServiceInternalAPI,
|
||||
accountDB accounts.Database, rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||
) util.JSONResponse {
|
||||
var createRequest createRoomRequest
|
||||
resErr := httputil.UnmarshalJSONRequest(req, &createRequest)
|
||||
// TODO (#267): Check room ID doesn't clash with an existing one, and we
|
||||
// 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 {
|
||||
return *resErr
|
||||
}
|
||||
if resErr = createRequest.Validate(); resErr != nil {
|
||||
// TODO: apply rate-limit
|
||||
|
||||
if resErr = r.Validate(); resErr != nil {
|
||||
return *resErr
|
||||
}
|
||||
|
||||
evTime, err := httputil.ParseTSParam(req)
|
||||
if err != nil {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.InvalidParam(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{},
|
||||
JSON: jsonerror.InvalidArgumentValue(err.Error()),
|
||||
}
|
||||
}
|
||||
|
||||
// Clobber keys: creator, room_version
|
||||
|
||||
roomVersion := rsAPI.DefaultRoomVersion()
|
||||
if createRequest.RoomVersion != "" {
|
||||
candidateVersion := gomatrixserverlib.RoomVersion(createRequest.RoomVersion)
|
||||
roomVersion := roomserverVersion.DefaultRoomVersion()
|
||||
if r.RoomVersion != "" {
|
||||
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
||||
_, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion)
|
||||
if roomVersionError != nil {
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusBadRequest,
|
||||
JSON: spec.UnsupportedRoomVersion(roomVersionError.Error()),
|
||||
JSON: jsonerror.UnsupportedRoomVersion(roomVersionError.Error()),
|
||||
}
|
||||
}
|
||||
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{
|
||||
"userID": userID.String(),
|
||||
"roomID": roomID.String(),
|
||||
"userID": userID,
|
||||
"roomID": roomID,
|
||||
"roomVersion": roomVersion,
|
||||
}).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 {
|
||||
util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed")
|
||||
return util.JSONResponse{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: spec.InternalServerError{},
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userDisplayName := profile.DisplayName
|
||||
userAvatarURL := profile.AvatarURL
|
||||
|
||||
keyID := cfg.Matrix.KeyID
|
||||
privateKey := cfg.Matrix.PrivateKey
|
||||
|
||||
req := roomserverAPI.PerformCreateRoomRequest{
|
||||
InvitedUsers: createRequest.Invite,
|
||||
RoomName: createRequest.Name,
|
||||
Visibility: createRequest.Visibility,
|
||||
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,
|
||||
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
|
||||
}
|
||||
|
||||
roomAlias, createRes := rsAPI.PerformCreateRoom(ctx, *userID, *roomID, &req)
|
||||
if createRes != nil {
|
||||
return *createRes
|
||||
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{
|
||||
Code: http.StatusInternalServerError,
|
||||
JSON: jsonerror.InternalServerError(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if r.Visibility == "public" {
|
||||
// expose this room in the published room list
|
||||
var pubRes roomserverAPI.PerformPublishResponse
|
||||
rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{
|
||||
RoomID: roomID,
|
||||
Visibility: "public",
|
||||
}, &pubRes)
|
||||
if pubRes.Error != nil {
|
||||
// treat as non-fatal since the room is already made by this point
|
||||
util.GetLogger(req.Context()).WithError(pubRes.Error).Error("failed to visibility:public")
|
||||
}
|
||||
}
|
||||
|
||||
response := createRoomResponse{
|
||||
RoomID: roomID.String(),
|
||||
RoomID: roomID,
|
||||
RoomAlias: roomAlias,
|
||||
}
|
||||
|
||||
|
|
@ -240,3 +578,30 @@ func createRoom(
|
|||
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