mirror of
https://github.com/matrix-org/dendrite.git
synced 2025-12-29 01:33:10 -06:00
Merge remote-tracking branch 'upstream/master' into piotr-kozimor/hs-sends-emails
This commit is contained in:
commit
840e54c6d2
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
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -3,6 +3,9 @@
|
||||||
# Hidden files
|
# Hidden files
|
||||||
.*
|
.*
|
||||||
|
|
||||||
|
# Allow GitHub config
|
||||||
|
!.github
|
||||||
|
|
||||||
# Downloads
|
# Downloads
|
||||||
/.downloads
|
/.downloads
|
||||||
|
|
||||||
|
|
@ -36,6 +39,7 @@ _testmain.go
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
*.wasm
|
||||||
|
|
||||||
# Generated keys
|
# Generated keys
|
||||||
*.pem
|
*.pem
|
||||||
|
|
@ -53,3 +57,6 @@ dendrite.yaml
|
||||||
|
|
||||||
# Generated code
|
# Generated code
|
||||||
cmd/dendrite-demo-yggdrasil/embed/fs*.go
|
cmd/dendrite-demo-yggdrasil/embed/fs*.go
|
||||||
|
|
||||||
|
# Test dependencies
|
||||||
|
test/wasm/node_modules
|
||||||
|
|
|
||||||
61
CHANGES.md
61
CHANGES.md
|
|
@ -1,5 +1,66 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Dendrite 0.4.1 (2021-07-26)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* Support for room version 7 has been added
|
||||||
|
* Key notary support is now more complete, allowing Dendrite to be used as a notary server for looking up signing keys
|
||||||
|
* State resolution v2 performance has been optimised further by caching the create event, power levels and join rules in memory instead of parsing them repeatedly
|
||||||
|
* The media API now handles cases where the maximum file size is configured to be less than 0 for unlimited size
|
||||||
|
* The `initial_state` in a `/createRoom` request is now respected when creating a room
|
||||||
|
* Code paths for checking if servers are joined to rooms have been optimised significantly
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* A bug resulting in `cannot xref null state block with snapshot` during the new state storage migration has been fixed
|
||||||
|
* Invites are now retired correctly when rejecting an invite from a remote server which is no longer reachable
|
||||||
|
* The DNS cache `cache_lifetime` option is now handled correctly (contributed by [S7evinK](https://github.com/S7evinK))
|
||||||
|
* Invalid events in a room join response are now dropped correctly, rather than failing the entire join
|
||||||
|
* The `prev_state` of an event will no longer be populated incorrectly to the state of the current event
|
||||||
|
* Receiving an invite to an unsupported room version will now correctly return the `M_UNSUPPORTED_ROOM_VERSION` error code instead of `M_BAD_JSON` (contributed by [meenal06](https://github.com/meenal06))
|
||||||
|
|
||||||
|
## Dendrite 0.4.0 (2021-07-12)
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
* All-new state storage in the roomserver, which dramatically reduces disk space utilisation
|
||||||
|
* State snapshots and blocks are now aggressively deduplicated and reused wherever possible, with state blocks being reduced by up to 15x and snapshot references being reduced up to 2x
|
||||||
|
* Dendrite will upgrade to the new state storage automatically on the first run after upgrade, although this may take some time depending on the size of the state storage
|
||||||
|
* Appservice support has been improved significantly, with many bridges now working correctly with Dendrite
|
||||||
|
* Events are now correctly sent to appservices based on room memberships
|
||||||
|
* Aliases and namespaces are now handled correctly, calling the appservice to query for aliases as needed
|
||||||
|
* Appservice user registrations are no longer being subject to incorrect validation checks
|
||||||
|
* Shared secret registration has now been implemented correctly
|
||||||
|
* The roomserver input API implements a new queuing system to reduce backpressure across rooms
|
||||||
|
* Checking if the local server is in a room has been optimised substantially, reducing CPU usage
|
||||||
|
* State resolution v2 has been optimised further by improving the power level checks, reducing CPU usage
|
||||||
|
* The federation API `/send` endpoint now deduplicates missing auth and prev events more aggressively to reduce memory usage
|
||||||
|
* The federation API `/send` endpoint now uses workers to reduce backpressure across rooms
|
||||||
|
* The bcrypt cost for password storage is now configurable with the `user_api.bcrypt_cost` option
|
||||||
|
* The federation API will now use significantly less memory when calling `/get_missing_events`
|
||||||
|
* MSC2946 Spaces endpoints have been updated to stable endpoint naming
|
||||||
|
* The media API can now be configured without a maximum file size
|
||||||
|
* A new `dendrite-upgrade-test` test has been added for verifying database schema upgrades across versions
|
||||||
|
* Added Prometheus metrics for roomserver backpressure, excessive device list updates and federation API event processing summaries
|
||||||
|
* Sentry support has been added for error reporting
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
* Removed the legacy `/v1` register endpoint. Dendrite only implements `/r0` of the CS API, and the legacy `/v1` endpoint had implementation errors which made it possible to bypass shared secret registration (thanks to Jakob Varmose Bentzen for reporting this)
|
||||||
|
* Attempting to register an account that already exists now returns a sensible error code rather than a HTTP 500
|
||||||
|
* Dendrite will no longer attempt to `/make_join` with itself if listed in the request `server_names`
|
||||||
|
* `/sync` will no longer return immediately if there is nothing to sync, which happened particularly with new accounts, causing high CPU usage
|
||||||
|
* Malicious media uploads can no longer exhaust all available memory (contributed by [S7evinK](https://github.com/S7evinK))
|
||||||
|
* Selecting one-time keys from the database has been optimised (contributed by [S7evinK](https://github.com/S7evinK))
|
||||||
|
* The return code when trying to fetch missing account data has been fixed (contributed by [adamgreig](https://github.com/adamgreig))
|
||||||
|
* Dendrite will no longer attempt to use `/make_leave` over federation when rejecting a local invite
|
||||||
|
* A panic has been fixed in `QueryMembershipsForRoom`
|
||||||
|
* A panic on duplicate membership events has been fixed in the federation sender
|
||||||
|
* A panic has been fixed in in `IsInterestedInRoomID` (contributed by [bodqhrohro](https://github.com/bodqhrohro))
|
||||||
|
* A panic in the roomserver has been fixed when handling empty state sets
|
||||||
|
* A panic in the federation API has been fixed when handling cached events
|
||||||
|
|
||||||
## Dendrite 0.3.11 (2021-03-02)
|
## Dendrite 0.3.11 (2021-03-02)
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ If you have further questions, please take a look at [our FAQ](docs/FAQ.md) or j
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
To build Dendrite, you will need Go 1.13 or later.
|
To build Dendrite, you will need Go 1.15 or later.
|
||||||
|
|
||||||
For a usable federating Dendrite deployment, you will also need:
|
For a usable federating Dendrite deployment, you will also need:
|
||||||
- A domain name (or subdomain)
|
- A domain name (or subdomain)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"github.com/matrix-org/dendrite/internal/sqlutil"
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Database stores events intended to be later sent to application services
|
// Database stores events intended to be later sent to application services
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,13 @@ reg POST /register rejects registration of usernames with '£'
|
||||||
reg POST /register rejects registration of usernames with 'é'
|
reg POST /register rejects registration of usernames with 'é'
|
||||||
reg POST /register rejects registration of usernames with '\n'
|
reg POST /register rejects registration of usernames with '\n'
|
||||||
reg POST /register rejects registration of usernames with '''
|
reg POST /register rejects registration of usernames with '''
|
||||||
|
reg POST /register allows registration of usernames with 'q'
|
||||||
|
reg POST /register allows registration of usernames with '3'
|
||||||
|
reg POST /register allows registration of usernames with '.'
|
||||||
|
reg POST /register allows registration of usernames with '_'
|
||||||
|
reg POST /register allows registration of usernames with '='
|
||||||
|
reg POST /register allows registration of usernames with '-'
|
||||||
|
reg POST /register allows registration of usernames with '/'
|
||||||
reg POST /r0/admin/register with shared secret
|
reg POST /r0/admin/register with shared secret
|
||||||
reg POST /r0/admin/register admin with shared secret
|
reg POST /r0/admin/register admin with shared secret
|
||||||
reg POST /r0/admin/register with shared secret downcases capitals
|
reg POST /r0/admin/register with shared secret downcases capitals
|
||||||
|
|
@ -76,7 +83,7 @@ rst GET /rooms/:room_id/state/m.room.topic gets topic
|
||||||
rst GET /rooms/:room_id/state fetches entire room state
|
rst GET /rooms/:room_id/state fetches entire room state
|
||||||
crm POST /createRoom with creation content
|
crm POST /createRoom with creation content
|
||||||
ali PUT /directory/room/:room_alias creates alias
|
ali PUT /directory/room/:room_alias creates alias
|
||||||
nsp GET /rooms/:room_id/aliases lists aliases
|
ali GET /rooms/:room_id/aliases lists aliases
|
||||||
jon POST /rooms/:room_id/join can join a room
|
jon POST /rooms/:room_id/join can join a room
|
||||||
jon POST /join/:room_alias can join a room
|
jon POST /join/:room_alias can join a room
|
||||||
jon POST /join/:room_id can join a room
|
jon POST /join/:room_id can join a room
|
||||||
|
|
@ -95,6 +102,7 @@ typ Typing notifications don't leak (3 subtests)
|
||||||
rst GET /rooms/:room_id/state/m.room.power_levels can fetch levels
|
rst GET /rooms/:room_id/state/m.room.power_levels can fetch levels
|
||||||
rst PUT /rooms/:room_id/state/m.room.power_levels can set levels
|
rst PUT /rooms/:room_id/state/m.room.power_levels can set levels
|
||||||
rst PUT power_levels should not explode if the old power levels were empty
|
rst PUT power_levels should not explode if the old power levels were empty
|
||||||
|
rst Users cannot set notifications powerlevel higher than their own (2 subtests)
|
||||||
rst Both GET and PUT work
|
rst Both GET and PUT work
|
||||||
rct POST /rooms/:room_id/receipt can create receipts
|
rct POST /rooms/:room_id/receipt can create receipts
|
||||||
red POST /rooms/:room_id/read_markers can create read marker
|
red POST /rooms/:room_id/read_markers can create read marker
|
||||||
|
|
@ -175,7 +183,7 @@ ali Users with sufficient power-level can delete other's aliases
|
||||||
ali Can delete canonical alias
|
ali Can delete canonical alias
|
||||||
ali Alias creators can delete alias with no ops
|
ali Alias creators can delete alias with no ops
|
||||||
ali Alias creators can delete canonical alias with no ops
|
ali Alias creators can delete canonical alias with no ops
|
||||||
ali Only room members can list aliases of a room
|
msc Only room members can list aliases of a room
|
||||||
inv Can invite users to invite-only rooms
|
inv Can invite users to invite-only rooms
|
||||||
inv Uninvited users cannot join the room
|
inv Uninvited users cannot join the room
|
||||||
inv Invited user can reject invite
|
inv Invited user can reject invite
|
||||||
|
|
@ -353,6 +361,7 @@ 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 full_state incremental update returns only recent timeline
|
||||||
syn A prev_batch token can be used in the v1 messages API
|
syn A prev_batch token can be used in the v1 messages API
|
||||||
syn A next_batch token can be used in the v1 messages API
|
syn A next_batch token can be used in the v1 messages API
|
||||||
|
syn A prev_batch token from incremental sync can be used in the v1 messages API
|
||||||
syn User sees their own presence in a sync
|
syn User sees their own presence in a sync
|
||||||
syn User is offline if they set_presence=offline in their 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 User sees updates to presence from other users in the incremental sync.
|
||||||
|
|
@ -574,6 +583,7 @@ fqu Outbound federation can query profile data
|
||||||
fqu Inbound federation can query profile data
|
fqu Inbound federation can query profile data
|
||||||
fqu Outbound federation can query room alias directory
|
fqu Outbound federation can query room alias directory
|
||||||
fqu Inbound federation can query room alias directory
|
fqu Inbound federation can query room alias directory
|
||||||
|
fsj Membership event with an invalid displayname in the send_join response should not cause room join to fail
|
||||||
fsj Outbound federation can query v1 /send_join
|
fsj Outbound federation can query v1 /send_join
|
||||||
fsj Outbound federation can query v2 /send_join
|
fsj Outbound federation can query v2 /send_join
|
||||||
fmj Outbound federation passes make_join failures through to the client
|
fmj Outbound federation passes make_join failures through to the client
|
||||||
|
|
@ -596,7 +606,7 @@ fsj Inbound: send_join rejects invalid JSON for room version 6
|
||||||
fed Outbound federation can send events
|
fed Outbound federation can send events
|
||||||
fed Inbound federation can receive events
|
fed Inbound federation can receive events
|
||||||
fed Inbound federation can receive redacted events
|
fed Inbound federation can receive redacted events
|
||||||
fed Ephemeral messages received from servers are correctly expired
|
msc Ephemeral messages received from servers are correctly expired
|
||||||
fed Events whose auth_events are in the wrong room do not mess up the room state
|
fed Events whose auth_events are in the wrong room do not mess up the room state
|
||||||
fed Inbound federation can return events
|
fed Inbound federation can return events
|
||||||
fed Inbound federation redacts events from erased users
|
fed Inbound federation redacts events from erased users
|
||||||
|
|
@ -743,6 +753,10 @@ nsp Set group joinable and join it
|
||||||
nsp Group is not joinable by default
|
nsp Group is not joinable by default
|
||||||
nsp Group is joinable over federation
|
nsp Group is joinable over federation
|
||||||
nsp Room is transitioned on local and remote groups upon room upgrade
|
nsp Room is transitioned on local and remote groups upon room upgrade
|
||||||
|
nsp POST /_synapse/admin/v1/register with shared secret
|
||||||
|
nsp POST /_synapse/admin/v1/register admin with shared secret
|
||||||
|
nsp POST /_synapse/admin/v1/register with shared secret downcases capitals
|
||||||
|
nsp POST /_synapse/admin/v1/register with shared secret disallows symbols
|
||||||
3pd Can bind 3PID via home server
|
3pd Can bind 3PID via home server
|
||||||
3pd Can bind and unbind 3PID via homeserver
|
3pd Can bind and unbind 3PID via homeserver
|
||||||
3pd Can unbind 3PID via homeserver when bound out of band
|
3pd Can unbind 3PID via homeserver when bound out of band
|
||||||
|
|
@ -859,8 +873,14 @@ jso Invalid JSON special values
|
||||||
inv Can invite users to invite-only rooms (2 subtests)
|
inv Can invite users to invite-only rooms (2 subtests)
|
||||||
plv setting 'm.room.name' respects room powerlevel (2 subtests)
|
plv setting 'm.room.name' respects room powerlevel (2 subtests)
|
||||||
psh Messages that notify from another user increment notification_count
|
psh Messages that notify from another user increment notification_count
|
||||||
psh Messages that org.matrix.msc2625.mark_unread from another user increment org.matrix.msc2625.unread_count
|
msc Messages that org.matrix.msc2625.mark_unread from another user increment org.matrix.msc2625.unread_count
|
||||||
dvk Can claim one time key using POST (2 subtests)
|
dvk Can claim one time key using POST (2 subtests)
|
||||||
fdk Can query remote device keys using POST (1 subtests)
|
fdk Can query remote device keys using POST (1 subtests)
|
||||||
fdk Can claim remote one time key using POST (2 subtests)
|
fdk Can claim remote one time key using POST (2 subtests)
|
||||||
fmj Inbound /make_join rejects attempts to join rooms where all users have left
|
fmj Inbound /make_join rejects attempts to join rooms where all users have left
|
||||||
|
msc Local users can peek into world_readable rooms by room ID
|
||||||
|
msc We can't peek into rooms with shared history_visibility
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ test_mappings = {
|
||||||
"nsp": "Non-Spec API",
|
"nsp": "Non-Spec API",
|
||||||
"unk": "Unknown API (no group specified)",
|
"unk": "Unknown API (no group specified)",
|
||||||
"app": "Application Services API",
|
"app": "Application Services API",
|
||||||
|
"msc": "MSCs",
|
||||||
"f": "Federation", # flag to mark test involves federation
|
"f": "Federation", # flag to mark test involves federation
|
||||||
|
|
||||||
"federation_apis": {
|
"federation_apis": {
|
||||||
|
|
@ -223,6 +224,7 @@ def main(results_tap_path, verbose):
|
||||||
},
|
},
|
||||||
"nonspec": {
|
"nonspec": {
|
||||||
"nsp": {},
|
"nsp": {},
|
||||||
|
"msc": {},
|
||||||
"unk": {}
|
"unk": {}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
@ -237,6 +239,8 @@ def main(results_tap_path, verbose):
|
||||||
summary["nonspec"]["unk"][name] = test_result["ok"]
|
summary["nonspec"]["unk"][name] = test_result["ok"]
|
||||||
if group_id == "nsp":
|
if group_id == "nsp":
|
||||||
summary["nonspec"]["nsp"][name] = test_result["ok"]
|
summary["nonspec"]["nsp"][name] = test_result["ok"]
|
||||||
|
elif group_id == "msc":
|
||||||
|
summary["nonspec"]["msc"][name] = test_result["ok"]
|
||||||
elif group_id == "app":
|
elif group_id == "app":
|
||||||
summary["appservice"]["app"][name] = test_result["ok"]
|
summary["appservice"]["app"][name] = test_result["ok"]
|
||||||
elif group_id in test_mappings["federation_apis"]:
|
elif group_id in test_mappings["federation_apis"]:
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
#!/bin/sh -eu
|
#!/bin/sh -eu
|
||||||
|
|
||||||
export GIT_COMMIT=$(git rev-list -1 HEAD) && \
|
export GIT_COMMIT=$(git rev-list -1 HEAD) && \
|
||||||
GOOS=js GOARCH=wasm go build -ldflags "-X main.GitCommit=$GIT_COMMIT" -o main.wasm ./cmd/dendritejs
|
GOOS=js GOARCH=wasm go build -ldflags "-X main.GitCommit=$GIT_COMMIT" -o bin/main.wasm ./cmd/dendritejs-pinecone
|
||||||
|
|
|
||||||
2
build.sh
2
build.sh
|
|
@ -21,4 +21,4 @@ mkdir -p bin
|
||||||
|
|
||||||
CGO_ENABLED=1 go build -trimpath -ldflags "$FLAGS" -v -o "bin/" ./cmd/...
|
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
|
CGO_ENABLED=0 GOOS=js GOARCH=wasm go build -trimpath -ldflags "$FLAGS" -o bin/main.wasm ./cmd/dendritejs-pinecone
|
||||||
|
|
|
||||||
|
|
@ -28,8 +28,8 @@ There are three sample `docker-compose` files:
|
||||||
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
The `docker-compose` files refer to the `/etc/dendrite` volume as where the
|
||||||
runtime config should come from. The mounted folder must contain:
|
runtime config should come from. The mounted folder must contain:
|
||||||
|
|
||||||
- `dendrite.yaml` configuration file (based on the sample `dendrite-config.yaml`
|
- `dendrite.yaml` configuration file (based on the [`dendrite-config.yaml`](https://raw.githubusercontent.com/matrix-org/dendrite/master/dendrite-config.yaml)
|
||||||
in the `docker/config` folder in the [Dendrite repository](https://github.com/matrix-org/dendrite)
|
sample in the `build/docker/config` folder of this repository.)
|
||||||
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
- `matrix_key.pem` server key, as generated using `cmd/generate-keys`
|
||||||
- `server.crt` certificate file
|
- `server.crt` certificate file
|
||||||
- `server.key` private key file for the above certificate
|
- `server.key` private key file for the above certificate
|
||||||
|
|
@ -37,19 +37,20 @@ runtime config should come from. The mounted folder must contain:
|
||||||
To generate keys:
|
To generate keys:
|
||||||
|
|
||||||
```
|
```
|
||||||
go run github.com/matrix-org/dendrite/cmd/generate-keys \
|
docker run --rm --entrypoint="" \
|
||||||
--private-key=matrix_key.pem \
|
-v $(pwd):/mnt \
|
||||||
--tls-cert=server.crt \
|
matrixdotorg/dendrite-monolith:latest \
|
||||||
--tls-key=server.key
|
/usr/bin/generate-keys \
|
||||||
|
-private-key /mnt/matrix_key.pem \
|
||||||
|
-tls-cert /mnt/server.crt \
|
||||||
|
-tls-key /mnt/server.key
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The key files will now exist in your current working directory, and can be mounted into place.
|
||||||
|
|
||||||
## Starting Dendrite as a monolith deployment
|
## Starting Dendrite as a monolith deployment
|
||||||
|
|
||||||
Create your config based on the `dendrite.yaml` configuration file in the `docker/config`
|
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).
|
||||||
folder in the [Dendrite repository](https://github.com/matrix-org/dendrite). Additionally,
|
|
||||||
make the following changes to the configuration:
|
|
||||||
|
|
||||||
- Enable Naffka: `use_naffka: true`
|
|
||||||
|
|
||||||
Once in place, start the PostgreSQL dependency:
|
Once in place, start the PostgreSQL dependency:
|
||||||
|
|
||||||
|
|
@ -65,8 +66,7 @@ docker-compose -f docker-compose.monolith.yml up
|
||||||
|
|
||||||
## Starting Dendrite as a polylith deployment
|
## Starting Dendrite as a polylith deployment
|
||||||
|
|
||||||
Create your config based on the `dendrite.yaml` configuration file in the `docker/config`
|
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).
|
||||||
folder in the [Dendrite repository](https://github.com/matrix-org/dendrite).
|
|
||||||
|
|
||||||
Once in place, start all the dependencies:
|
Once in place, start all the dependencies:
|
||||||
|
|
||||||
|
|
@ -82,10 +82,10 @@ docker-compose -f docker-compose.polylith.yml up
|
||||||
|
|
||||||
## Building the images
|
## Building the images
|
||||||
|
|
||||||
The `docker/images-build.sh` script will build the base image, followed by
|
The `build/docker/images-build.sh` script will build the base image, followed by
|
||||||
all of the component images.
|
all of the component images.
|
||||||
|
|
||||||
The `docker/images-push.sh` script will push them to Docker Hub (subject
|
The `build/docker/images-push.sh` script will push them to Docker Hub (subject
|
||||||
to permissions).
|
to permissions).
|
||||||
|
|
||||||
If you wish to build and push your own images, rename `matrixdotorg/dendrite` to
|
If you wish to build and push your own images, rename `matrixdotorg/dendrite` to
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -37,15 +36,14 @@ import (
|
||||||
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
userapiAPI "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"go.uber.org/atomic"
|
|
||||||
"golang.org/x/net/http2"
|
"golang.org/x/net/http2"
|
||||||
"golang.org/x/net/http2/h2c"
|
"golang.org/x/net/http2/h2c"
|
||||||
|
|
||||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||||
|
"github.com/matrix-org/pinecone/router"
|
||||||
pineconeRouter "github.com/matrix-org/pinecone/router"
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
||||||
"github.com/matrix-org/pinecone/types"
|
"github.com/matrix-org/pinecone/types"
|
||||||
pineconeTypes "github.com/matrix-org/pinecone/types"
|
|
||||||
|
|
||||||
_ "golang.org/x/mobile/bind"
|
_ "golang.org/x/mobile/bind"
|
||||||
)
|
)
|
||||||
|
|
@ -65,7 +63,7 @@ type DendriteMonolith struct {
|
||||||
CacheDirectory string
|
CacheDirectory string
|
||||||
staticPeerURI string
|
staticPeerURI string
|
||||||
staticPeerMutex sync.RWMutex
|
staticPeerMutex sync.RWMutex
|
||||||
staticPeerAttempts atomic.Uint32
|
staticPeerAttempt chan struct{}
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
processContext *process.ProcessContext
|
processContext *process.ProcessContext
|
||||||
|
|
@ -99,7 +97,9 @@ func (m *DendriteMonolith) SetStaticPeer(uri string) {
|
||||||
m.staticPeerMutex.Unlock()
|
m.staticPeerMutex.Unlock()
|
||||||
m.DisconnectType(pineconeRouter.PeerTypeRemote)
|
m.DisconnectType(pineconeRouter.PeerTypeRemote)
|
||||||
if uri != "" {
|
if uri != "" {
|
||||||
go m.staticPeerConnect()
|
go func() {
|
||||||
|
m.staticPeerAttempt <- struct{}{}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -195,6 +195,8 @@ func (m *DendriteMonolith) RegisterDevice(localpart, deviceID string) (string, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) staticPeerConnect() {
|
func (m *DendriteMonolith) staticPeerConnect() {
|
||||||
|
attempt := func() {
|
||||||
|
if m.PineconeRouter.PeerCount(router.PeerTypeRemote) == 0 {
|
||||||
m.staticPeerMutex.RLock()
|
m.staticPeerMutex.RLock()
|
||||||
uri := m.staticPeerURI
|
uri := m.staticPeerURI
|
||||||
m.staticPeerMutex.RUnlock()
|
m.staticPeerMutex.RUnlock()
|
||||||
|
|
@ -202,10 +204,18 @@ func (m *DendriteMonolith) staticPeerConnect() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := conn.ConnectToPeer(m.PineconeRouter, uri); err != nil {
|
if err := conn.ConnectToPeer(m.PineconeRouter, uri); err != nil {
|
||||||
exp := time.Second * time.Duration(math.Exp2(float64(m.staticPeerAttempts.Inc())))
|
logrus.WithError(err).Error("Failed to connect to static peer")
|
||||||
time.AfterFunc(exp, m.staticPeerConnect)
|
}
|
||||||
} else {
|
}
|
||||||
m.staticPeerAttempts.Store(0)
|
}
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-m.processContext.Context().Done():
|
||||||
|
case <-m.staticPeerAttempt:
|
||||||
|
attempt()
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
attempt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -248,13 +258,6 @@ func (m *DendriteMonolith) Start() {
|
||||||
m.PineconeQUIC = pineconeSessions.NewSessions(logger, m.PineconeRouter)
|
m.PineconeQUIC = pineconeSessions.NewSessions(logger, m.PineconeRouter)
|
||||||
m.PineconeMulticast = pineconeMulticast.NewMulticast(logger, m.PineconeRouter)
|
m.PineconeMulticast = pineconeMulticast.NewMulticast(logger, m.PineconeRouter)
|
||||||
|
|
||||||
m.PineconeRouter.SetDisconnectedCallback(func(port pineconeTypes.SwitchPortID, public pineconeTypes.PublicKey, peertype int, err error) {
|
|
||||||
if peertype == pineconeRouter.PeerTypeRemote {
|
|
||||||
m.staticPeerAttempts.Store(0)
|
|
||||||
time.AfterFunc(time.Second, m.staticPeerConnect)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
prefix := hex.EncodeToString(pk)
|
prefix := hex.EncodeToString(pk)
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults()
|
cfg.Defaults()
|
||||||
|
|
@ -296,7 +299,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
base, federation, rsAPI, keyRing, true,
|
base, federation, rsAPI, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, fsAPI)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
m.userAPI = userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
m.userAPI = userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
||||||
keyAPI.SetUserAPI(m.userAPI)
|
keyAPI.SetUserAPI(m.userAPI)
|
||||||
|
|
||||||
|
|
@ -331,6 +334,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
|
|
@ -359,8 +363,12 @@ func (m *DendriteMonolith) Start() {
|
||||||
},
|
},
|
||||||
Handler: h2c.NewHandler(pMux, h2s),
|
Handler: h2c.NewHandler(pMux, h2s),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.processContext = base.ProcessContext
|
m.processContext = base.ProcessContext
|
||||||
|
|
||||||
|
m.staticPeerAttempt = make(chan struct{}, 1)
|
||||||
|
go m.staticPeerConnect()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
m.logger.Info("Listening on ", cfg.Global.ServerName)
|
m.logger.Info("Listening on ", cfg.Global.ServerName)
|
||||||
m.logger.Fatal(m.httpServer.Serve(m.PineconeQUIC))
|
m.logger.Fatal(m.httpServer.Serve(m.PineconeQUIC))
|
||||||
|
|
|
||||||
|
|
@ -45,10 +45,6 @@ func (m *DendriteMonolith) PeerCount() int {
|
||||||
return m.YggdrasilNode.PeerCount()
|
return m.YggdrasilNode.PeerCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *DendriteMonolith) SessionCount() int {
|
|
||||||
return m.YggdrasilNode.SessionCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
func (m *DendriteMonolith) SetMulticastEnabled(enabled bool) {
|
||||||
m.YggdrasilNode.SetMulticastEnabled(enabled)
|
m.YggdrasilNode.SetMulticastEnabled(enabled)
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +74,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ygg, err := yggconn.Setup("dendrite", m.StorageDirectory)
|
ygg, err := yggconn.Setup("dendrite", m.StorageDirectory, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +83,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults()
|
cfg.Defaults()
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
||||||
cfg.Global.PrivateKey = ygg.SigningPrivateKey()
|
cfg.Global.PrivateKey = ygg.PrivateKey()
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.Kafka.UseNaffka = true
|
cfg.Global.Kafka.UseNaffka = true
|
||||||
cfg.Global.Kafka.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-naffka.db", m.StorageDirectory))
|
cfg.Global.Kafka.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-naffka.db", m.StorageDirectory))
|
||||||
|
|
@ -123,7 +119,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
base, federation, rsAPI, keyRing, true,
|
base, federation, rsAPI, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, federation)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
|
@ -134,18 +130,6 @@ func (m *DendriteMonolith) Start() {
|
||||||
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI)
|
||||||
rsAPI.SetAppserviceAPI(asAPI)
|
rsAPI.SetAppserviceAPI(asAPI)
|
||||||
|
|
||||||
ygg.SetSessionFunc(func(address string) {
|
|
||||||
req := &api.PerformServersAliveRequest{
|
|
||||||
Servers: []gomatrixserverlib.ServerName{
|
|
||||||
gomatrixserverlib.ServerName(address),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res := &api.PerformServersAliveResponse{}
|
|
||||||
if err := fsAPI.PerformServersAlive(context.TODO(), req, res); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to send wake-up message to newly connected node")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// The underlying roomserver implementation needs to be able to call the fedsender.
|
// The underlying roomserver implementation needs to be able to call the fedsender.
|
||||||
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
||||||
rsAPI.SetFederationSenderAPI(fsAPI)
|
rsAPI.SetFederationSenderAPI(fsAPI)
|
||||||
|
|
@ -173,6 +157,7 @@ func (m *DendriteMonolith) Start() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter()
|
httpRouter := mux.NewRouter()
|
||||||
|
|
|
||||||
|
|
@ -52,7 +52,7 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
|
||||||
if username == "" {
|
if username == "" {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusUnauthorized,
|
Code: http.StatusUnauthorized,
|
||||||
JSON: jsonerror.BadJSON("'user' must be supplied."),
|
JSON: jsonerror.BadJSON("A username must be supplied."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
|
localpart, err := userutil.ParseUsernameParam(username, &t.Config.Matrix.ServerName)
|
||||||
|
|
@ -68,7 +68,7 @@ func (t *LoginTypePassword) Login(ctx context.Context, req interface{}) (*Login,
|
||||||
// but that would leak the existence of the user.
|
// but that would leak the existence of the user.
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("username or password was incorrect, or the account does not exist"),
|
JSON: jsonerror.Forbidden("The username or password was incorrect or the account does not exist."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &r.Login, nil
|
return &r.Login, nil
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ func (r *Login) Username() string {
|
||||||
if r.Identifier.Type == "m.id.user" {
|
if r.Identifier.Type == "m.id.user" {
|
||||||
return r.Identifier.User
|
return r.Identifier.User
|
||||||
}
|
}
|
||||||
// deprecated but without it Riot iOS won't log in
|
// deprecated but without it Element iOS won't log in
|
||||||
return r.User
|
return r.User
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -220,7 +220,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON("unknown auth.type: " + authType),
|
JSON: jsonerror.BadJSON("Unknown auth.type: " + authType),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,7 +231,7 @@ func (u *UserInteractive) Verify(ctx context.Context, bodyBytes []byte, device *
|
||||||
if !u.IsSingleStageFlow(authType) {
|
if !u.IsSingleStageFlow(authType) {
|
||||||
return nil, &util.JSONResponse{
|
return nil, &util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.Unknown("missing or unknown auth.session"),
|
JSON: jsonerror.Unknown("The auth.session is missing or unknown."),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ import (
|
||||||
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
// AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component.
|
||||||
func AddPublicRoutes(
|
func AddPublicRoutes(
|
||||||
router *mux.Router,
|
router *mux.Router,
|
||||||
|
synapseAdminRouter *mux.Router,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
accountsDB accounts.Database,
|
accountsDB accounts.Database,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
|
|
@ -56,7 +57,7 @@ func AddPublicRoutes(
|
||||||
}
|
}
|
||||||
|
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
router, cfg, eduInputAPI, rsAPI, asAPI,
|
router, synapseAdminRouter, cfg, eduInputAPI, rsAPI, asAPI,
|
||||||
accountsDB, userAPI, federation,
|
accountsDB, userAPI, federation,
|
||||||
syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
|
syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,18 @@ func GuestAccessForbidden(msg string) *MatrixError {
|
||||||
return &MatrixError{"M_GUEST_ACCESS_FORBIDDEN", msg}
|
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}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
type IncompatibleRoomVersionError struct {
|
||||||
RoomVersion string `json:"room_version"`
|
RoomVersion string `json:"room_version"`
|
||||||
Error string `json:"error"`
|
Error string `json:"error"`
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ func GetAccountData(
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusNotFound,
|
Code: http.StatusNotFound,
|
||||||
JSON: jsonerror.Forbidden("data not found"),
|
JSON: jsonerror.NotFound("data not found"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
96
clientapi/routing/aliases.go
Normal file
96
clientapi/routing/aliases.go
Normal file
|
|
@ -0,0 +1,96 @@
|
||||||
|
// 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 routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases
|
||||||
|
func GetAliases(
|
||||||
|
req *http.Request, rsAPI api.RoomserverInternalAPI, device *userapi.Device, roomID string,
|
||||||
|
) util.JSONResponse {
|
||||||
|
stateTuple := gomatrixserverlib.StateKeyTuple{
|
||||||
|
EventType: gomatrixserverlib.MRoomHistoryVisibility,
|
||||||
|
StateKey: "",
|
||||||
|
}
|
||||||
|
stateReq := &api.QueryCurrentStateRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
StateTuples: []gomatrixserverlib.StateKeyTuple{stateTuple},
|
||||||
|
}
|
||||||
|
stateRes := &api.QueryCurrentStateResponse{}
|
||||||
|
if err := rsAPI.QueryCurrentState(req.Context(), stateReq, stateRes); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.QueryCurrentState failed")
|
||||||
|
return util.ErrorResponse(fmt.Errorf("rsAPI.QueryCurrentState: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
visibility := "invite"
|
||||||
|
if historyVisEvent, ok := stateRes.StateEvents[stateTuple]; ok {
|
||||||
|
var err error
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if visibility != gomatrixserverlib.WorldReadable {
|
||||||
|
queryReq := api.QueryMembershipForUserRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
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 jsonerror.InternalServerError()
|
||||||
|
}
|
||||||
|
if !queryRes.IsInRoom {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("You aren't a member of this room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasesReq := api.GetAliasesForRoomIDRequest{
|
||||||
|
RoomID: roomID,
|
||||||
|
}
|
||||||
|
aliasesRes := api.GetAliasesForRoomIDResponse{}
|
||||||
|
if err := rsAPI.GetAliasesForRoomID(req.Context(), &aliasesReq, &aliasesRes); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("rsAPI.GetAliasesForRoomID failed")
|
||||||
|
return util.ErrorResponse(fmt.Errorf("rsAPI.GetAliasesForRoomID: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
response := struct {
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
|
}{
|
||||||
|
Aliases: aliasesRes.Aliases,
|
||||||
|
}
|
||||||
|
if response.Aliases == nil {
|
||||||
|
response.Aliases = []string{} // pleases sytest
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,7 +43,7 @@ type createRoomRequest struct {
|
||||||
Visibility string `json:"visibility"`
|
Visibility string `json:"visibility"`
|
||||||
Topic string `json:"topic"`
|
Topic string `json:"topic"`
|
||||||
Preset string `json:"preset"`
|
Preset string `json:"preset"`
|
||||||
CreationContent map[string]interface{} `json:"creation_content"`
|
CreationContent json.RawMessage `json:"creation_content"`
|
||||||
InitialState []fledglingEvent `json:"initial_state"`
|
InitialState []fledglingEvent `json:"initial_state"`
|
||||||
RoomAliasName string `json:"room_alias_name"`
|
RoomAliasName string `json:"room_alias_name"`
|
||||||
GuestCanJoin bool `json:"guest_can_join"`
|
GuestCanJoin bool `json:"guest_can_join"`
|
||||||
|
|
@ -177,11 +177,6 @@ func createRoom(
|
||||||
|
|
||||||
// Clobber keys: creator, room_version
|
// Clobber keys: creator, room_version
|
||||||
|
|
||||||
if r.CreationContent == nil {
|
|
||||||
r.CreationContent = make(map[string]interface{}, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
r.CreationContent["creator"] = userID
|
|
||||||
roomVersion := roomserverVersion.DefaultRoomVersion()
|
roomVersion := roomserverVersion.DefaultRoomVersion()
|
||||||
if r.RoomVersion != "" {
|
if r.RoomVersion != "" {
|
||||||
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
candidateVersion := gomatrixserverlib.RoomVersion(r.RoomVersion)
|
||||||
|
|
@ -194,7 +189,6 @@ func createRoom(
|
||||||
}
|
}
|
||||||
roomVersion = candidateVersion
|
roomVersion = candidateVersion
|
||||||
}
|
}
|
||||||
r.CreationContent["room_version"] = roomVersion
|
|
||||||
|
|
||||||
// TODO: visibility/presets/raw initial state
|
// TODO: visibility/presets/raw initial state
|
||||||
// TODO: Create room alias association
|
// TODO: Create room alias association
|
||||||
|
|
@ -203,7 +197,7 @@ func createRoom(
|
||||||
logger.WithFields(log.Fields{
|
logger.WithFields(log.Fields{
|
||||||
"userID": userID,
|
"userID": userID,
|
||||||
"roomID": roomID,
|
"roomID": roomID,
|
||||||
"roomVersion": r.CreationContent["room_version"],
|
"roomVersion": roomVersion,
|
||||||
}).Info("Creating new room")
|
}).Info("Creating new room")
|
||||||
|
|
||||||
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB)
|
profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, accountDB)
|
||||||
|
|
@ -212,6 +206,109 @@ func createRoom(
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createContent := map[string]interface{}{}
|
||||||
|
if len(r.CreationContent) > 0 {
|
||||||
|
if err = json.Unmarshal(r.CreationContent, &createContent); err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("json.Unmarshal for creation_content failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("invalid create content"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
createContent["creator"] = userID
|
||||||
|
createContent["room_version"] = roomVersion
|
||||||
|
powerLevelContent := eventutil.InitialPowerLevelsContent(userID)
|
||||||
|
joinRuleContent := gomatrixserverlib.JoinRuleContent{
|
||||||
|
JoinRule: gomatrixserverlib.Invite,
|
||||||
|
}
|
||||||
|
historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{
|
||||||
|
HistoryVisibility: historyVisibilityShared,
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.PowerLevelContentOverride != nil {
|
||||||
|
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
||||||
|
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
||||||
|
if err != nil {
|
||||||
|
util.GetLogger(req.Context()).WithError(err).Error("json.Unmarshal for power_level_content_override failed")
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.Preset {
|
||||||
|
case presetPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
case presetTrustedPrivateChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Invite
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
// TODO If trusted_private_chat, all invitees are given the same power level as the room creator.
|
||||||
|
case presetPublicChat:
|
||||||
|
joinRuleContent.JoinRule = gomatrixserverlib.Public
|
||||||
|
historyVisibilityContent.HistoryVisibility = historyVisibilityShared
|
||||||
|
}
|
||||||
|
|
||||||
|
createEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomCreate,
|
||||||
|
Content: createContent,
|
||||||
|
}
|
||||||
|
powerLevelEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomPowerLevels,
|
||||||
|
Content: powerLevelContent,
|
||||||
|
}
|
||||||
|
joinRuleEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomJoinRules,
|
||||||
|
Content: joinRuleContent,
|
||||||
|
}
|
||||||
|
historyVisibilityEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomHistoryVisibility,
|
||||||
|
Content: historyVisibilityContent,
|
||||||
|
}
|
||||||
|
membershipEvent := fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomMember,
|
||||||
|
StateKey: userID,
|
||||||
|
Content: gomatrixserverlib.MemberContent{
|
||||||
|
Membership: gomatrixserverlib.Join,
|
||||||
|
DisplayName: profile.DisplayName,
|
||||||
|
AvatarURL: profile.AvatarURL,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameEvent *fledglingEvent
|
||||||
|
var topicEvent *fledglingEvent
|
||||||
|
var guestAccessEvent *fledglingEvent
|
||||||
|
var aliasEvent *fledglingEvent
|
||||||
|
|
||||||
|
if r.Name != "" {
|
||||||
|
nameEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomName,
|
||||||
|
Content: eventutil.NameContent{
|
||||||
|
Name: r.Name,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.Topic != "" {
|
||||||
|
topicEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomTopic,
|
||||||
|
Content: eventutil.TopicContent{
|
||||||
|
Topic: r.Topic,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.GuestCanJoin {
|
||||||
|
guestAccessEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomGuestAccess,
|
||||||
|
Content: eventutil.GuestAccessContent{
|
||||||
|
GuestAccess: "can_join",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var roomAlias string
|
var roomAlias string
|
||||||
if r.RoomAliasName != "" {
|
if r.RoomAliasName != "" {
|
||||||
roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, cfg.Matrix.ServerName)
|
roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, cfg.Matrix.ServerName)
|
||||||
|
|
@ -230,44 +327,46 @@ func createRoom(
|
||||||
if aliasResp.RoomID != "" {
|
if aliasResp.RoomID != "" {
|
||||||
return util.MessageResponse(400, "Alias already exists")
|
return util.MessageResponse(400, "Alias already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aliasEvent = &fledglingEvent{
|
||||||
|
Type: gomatrixserverlib.MRoomCanonicalAlias,
|
||||||
|
Content: eventutil.CanonicalAlias{
|
||||||
|
Alias: roomAlias,
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
membershipContent := gomatrixserverlib.MemberContent{
|
var initialStateEvents []fledglingEvent
|
||||||
Membership: gomatrixserverlib.Join,
|
for i := range r.InitialState {
|
||||||
DisplayName: profile.DisplayName,
|
if r.InitialState[i].StateKey != "" {
|
||||||
AvatarURL: profile.AvatarURL,
|
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var joinRules, historyVisibility string
|
switch r.InitialState[i].Type {
|
||||||
switch r.Preset {
|
case gomatrixserverlib.MRoomCreate:
|
||||||
case presetPrivateChat:
|
continue
|
||||||
joinRules = gomatrixserverlib.Invite
|
|
||||||
historyVisibility = historyVisibilityShared
|
case gomatrixserverlib.MRoomPowerLevels:
|
||||||
case presetTrustedPrivateChat:
|
powerLevelEvent = r.InitialState[i]
|
||||||
joinRules = gomatrixserverlib.Invite
|
|
||||||
historyVisibility = historyVisibilityShared
|
case gomatrixserverlib.MRoomJoinRules:
|
||||||
// TODO If trusted_private_chat, all invitees are given the same power level as the room creator.
|
joinRuleEvent = r.InitialState[i]
|
||||||
case presetPublicChat:
|
|
||||||
joinRules = gomatrixserverlib.Public
|
case gomatrixserverlib.MRoomHistoryVisibility:
|
||||||
historyVisibility = historyVisibilityShared
|
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:
|
default:
|
||||||
// Default room rules, r.Preset was previously checked for valid values so
|
initialStateEvents = append(initialStateEvents, r.InitialState[i])
|
||||||
// only a request with no preset should end up here.
|
|
||||||
joinRules = gomatrixserverlib.Invite
|
|
||||||
historyVisibility = historyVisibilityShared
|
|
||||||
}
|
|
||||||
|
|
||||||
var builtEvents []*gomatrixserverlib.HeaderedEvent
|
|
||||||
|
|
||||||
powerLevelContent := eventutil.InitialPowerLevelsContent(userID)
|
|
||||||
if r.PowerLevelContentOverride != nil {
|
|
||||||
// Merge powerLevelContentOverride fields by unmarshalling it atop the defaults
|
|
||||||
err = json.Unmarshal(r.PowerLevelContentOverride, &powerLevelContent)
|
|
||||||
if err != nil {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusBadRequest,
|
|
||||||
JSON: jsonerror.BadJSON("malformed power_level_content_override"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -284,37 +383,33 @@ func createRoom(
|
||||||
// 10- m.room.topic (opt)
|
// 10- m.room.topic (opt)
|
||||||
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
// 11- invite events (opt) - with is_direct flag if applicable TODO
|
||||||
// 12- 3pid invite events (opt) TODO
|
// 12- 3pid invite events (opt) TODO
|
||||||
// 13- m.room.aliases event for HS (if alias specified) TODO
|
|
||||||
// This differs from Synapse slightly. Synapse would vary the ordering of 3-7
|
// 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
|
// 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.
|
// 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?
|
// TODO: Synapse has txn/token ID on each event. Do we need to do this here?
|
||||||
eventsToMake := []fledglingEvent{
|
eventsToMake := []fledglingEvent{
|
||||||
{"m.room.create", "", r.CreationContent},
|
createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent,
|
||||||
{"m.room.member", userID, membershipContent},
|
|
||||||
{"m.room.power_levels", "", powerLevelContent},
|
|
||||||
{"m.room.join_rules", "", gomatrixserverlib.JoinRuleContent{JoinRule: joinRules}},
|
|
||||||
{"m.room.history_visibility", "", eventutil.HistoryVisibilityContent{HistoryVisibility: historyVisibility}},
|
|
||||||
}
|
}
|
||||||
if roomAlias != "" {
|
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.
|
// 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.
|
// This means we might fail creating the alias but say the canonical alias is something that doesn't exist.
|
||||||
// m.room.aliases is handled when we call roomserver.SetRoomAlias
|
eventsToMake = append(eventsToMake, *aliasEvent)
|
||||||
eventsToMake = append(eventsToMake, fledglingEvent{"m.room.canonical_alias", "", eventutil.CanonicalAlias{Alias: roomAlias}})
|
|
||||||
}
|
|
||||||
if r.GuestCanJoin {
|
|
||||||
eventsToMake = append(eventsToMake, fledglingEvent{"m.room.guest_access", "", eventutil.GuestAccessContent{GuestAccess: "can_join"}})
|
|
||||||
}
|
|
||||||
eventsToMake = append(eventsToMake, r.InitialState...)
|
|
||||||
if r.Name != "" {
|
|
||||||
eventsToMake = append(eventsToMake, fledglingEvent{"m.room.name", "", eventutil.NameContent{Name: r.Name}})
|
|
||||||
}
|
|
||||||
if r.Topic != "" {
|
|
||||||
eventsToMake = append(eventsToMake, fledglingEvent{"m.room.topic", "", eventutil.TopicContent{Topic: r.Topic}})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: invite events
|
// TODO: invite events
|
||||||
// TODO: 3pid invite events
|
// TODO: 3pid invite events
|
||||||
|
|
||||||
|
var builtEvents []*gomatrixserverlib.HeaderedEvent
|
||||||
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
authEvents := gomatrixserverlib.NewAuthEvents(nil)
|
||||||
for i, e := range eventsToMake {
|
for i, e := range eventsToMake {
|
||||||
depth := i + 1 // depth starts at 1
|
depth := i + 1 // depth starts at 1
|
||||||
|
|
@ -403,7 +498,7 @@ func createRoom(
|
||||||
fallthrough
|
fallthrough
|
||||||
case gomatrixserverlib.MRoomCanonicalAlias:
|
case gomatrixserverlib.MRoomCanonicalAlias:
|
||||||
fallthrough
|
fallthrough
|
||||||
case "m.room.encryption": // TODO: move this to gmsl
|
case gomatrixserverlib.MRoomEncryption:
|
||||||
fallthrough
|
fallthrough
|
||||||
case gomatrixserverlib.MRoomMember:
|
case gomatrixserverlib.MRoomMember:
|
||||||
fallthrough
|
fallthrough
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func Deactivate(
|
||||||
return *errRes
|
return *errRes
|
||||||
}
|
}
|
||||||
|
|
||||||
localpart, _, err := gomatrixserverlib.SplitID('@', login.User)
|
localpart, _, err := gomatrixserverlib.SplitID('@', login.Username())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
|
|
|
||||||
|
|
@ -113,13 +113,12 @@ func DirectoryRoom(
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLocalAlias implements PUT /directory/room/{roomAlias}
|
// SetLocalAlias implements PUT /directory/room/{roomAlias}
|
||||||
// TODO: Check if the user has the power level to set an alias
|
|
||||||
func SetLocalAlias(
|
func SetLocalAlias(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
device *api.Device,
|
device *api.Device,
|
||||||
alias string,
|
alias string,
|
||||||
cfg *config.ClientAPI,
|
cfg *config.ClientAPI,
|
||||||
aliasAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
_, domain, err := gomatrixserverlib.SplitID('#', alias)
|
_, domain, err := gomatrixserverlib.SplitID('#', alias)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -172,7 +171,7 @@ func SetLocalAlias(
|
||||||
Alias: alias,
|
Alias: alias,
|
||||||
}
|
}
|
||||||
var queryRes roomserverAPI.SetRoomAliasResponse
|
var queryRes roomserverAPI.SetRoomAliasResponse
|
||||||
if err := aliasAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
if err := rsAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.SetRoomAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
@ -195,43 +194,32 @@ func RemoveLocalAlias(
|
||||||
req *http.Request,
|
req *http.Request,
|
||||||
device *api.Device,
|
device *api.Device,
|
||||||
alias string,
|
alias string,
|
||||||
aliasAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
|
|
||||||
creatorQueryReq := roomserverAPI.GetCreatorIDForAliasRequest{
|
|
||||||
Alias: alias,
|
|
||||||
}
|
|
||||||
var creatorQueryRes roomserverAPI.GetCreatorIDForAliasResponse
|
|
||||||
if err := aliasAPI.GetCreatorIDForAlias(req.Context(), &creatorQueryReq, &creatorQueryRes); err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.GetCreatorIDForAlias failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
}
|
|
||||||
|
|
||||||
if creatorQueryRes.UserID == "" {
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusNotFound,
|
|
||||||
JSON: jsonerror.NotFound("Alias does not exist"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if creatorQueryRes.UserID != device.UserID {
|
|
||||||
// TODO: Still allow deletion if user is admin
|
|
||||||
return util.JSONResponse{
|
|
||||||
Code: http.StatusForbidden,
|
|
||||||
JSON: jsonerror.Forbidden("You do not have permission to delete this alias"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
queryReq := roomserverAPI.RemoveRoomAliasRequest{
|
queryReq := roomserverAPI.RemoveRoomAliasRequest{
|
||||||
Alias: alias,
|
Alias: alias,
|
||||||
UserID: device.UserID,
|
UserID: device.UserID,
|
||||||
}
|
}
|
||||||
var queryRes roomserverAPI.RemoveRoomAliasResponse
|
var queryRes roomserverAPI.RemoveRoomAliasResponse
|
||||||
if err := aliasAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
if err := rsAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
util.GetLogger(req.Context()).WithError(err).Error("aliasAPI.RemoveRoomAlias failed")
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !queryRes.Found {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusNotFound,
|
||||||
|
JSON: jsonerror.NotFound("The alias does not exist."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !queryRes.Removed {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("You do not have permission to remove this alias."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
JSON: struct{}{},
|
JSON: struct{}{},
|
||||||
|
|
@ -294,9 +282,9 @@ func SetVisibility(
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTSPEC: Check if the user's power is greater than power required to change m.room.aliases event
|
// NOTSPEC: Check if the user's power is greater than power required to change m.room.canonical_alias event
|
||||||
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].Event)
|
power, _ := gomatrixserverlib.NewPowerLevelContentFromEvent(queryEventsRes.StateEvents[0].Event)
|
||||||
if power.UserLevel(dev.UserID) < power.EventLevel(gomatrixserverlib.MRoomAliases, true) {
|
if power.UserLevel(dev.UserID) < power.EventLevel(gomatrixserverlib.MRoomCanonicalAlias, true) {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusForbidden,
|
Code: http.StatusForbidden,
|
||||||
JSON: jsonerror.Forbidden("userID doesn't have power level to change visibility"),
|
JSON: jsonerror.Forbidden("userID doesn't have power level to change visibility"),
|
||||||
|
|
|
||||||
291
clientapi/routing/key_backup.go
Normal file
291
clientapi/routing/key_backup.go
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
// 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 routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type keyBackupVersion struct {
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
AuthData json.RawMessage `json:"auth_data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBackupVersionCreateResponse struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBackupVersionResponse struct {
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
AuthData json.RawMessage `json:"auth_data"`
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
ETag string `json:"etag"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBackupSessionRequest struct {
|
||||||
|
Rooms map[string]struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
} `json:"rooms"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type keyBackupSessionResponse struct {
|
||||||
|
Count int64 `json:"count"`
|
||||||
|
ETag string `json:"etag"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new key backup. Request must contain a `keyBackupVersion`. Returns a `keyBackupVersionCreateResponse`.
|
||||||
|
// Implements POST /_matrix/client/r0/room_keys/version
|
||||||
|
func CreateKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device) util.JSONResponse {
|
||||||
|
var kb keyBackupVersion
|
||||||
|
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: "",
|
||||||
|
AuthData: kb.AuthData,
|
||||||
|
Algorithm: kb.Algorithm,
|
||||||
|
}, &performKeyBackupResp)
|
||||||
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
Version: performKeyBackupResp.Version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KeyBackupVersion returns the key backup version specified. If `version` is empty, the latest `keyBackupVersionResponse` is returned.
|
||||||
|
// Implements GET /_matrix/client/r0/room_keys/version and GET /_matrix/client/r0/room_keys/version/{version}
|
||||||
|
func KeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
|
var queryResp userapi.QueryKeyBackupResponse
|
||||||
|
userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
}, &queryResp)
|
||||||
|
if queryResp.Error != "" {
|
||||||
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
||||||
|
}
|
||||||
|
if !queryResp.Exists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("version not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: keyBackupVersionResponse{
|
||||||
|
Algorithm: queryResp.Algorithm,
|
||||||
|
AuthData: queryResp.AuthData,
|
||||||
|
Count: queryResp.Count,
|
||||||
|
ETag: queryResp.ETag,
|
||||||
|
Version: queryResp.Version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modify the auth data of a key backup. Version must not be empty. Request must contain a `keyBackupVersion`
|
||||||
|
// Implements PUT /_matrix/client/r0/room_keys/version/{version}
|
||||||
|
func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
|
var kb keyBackupVersion
|
||||||
|
resErr := httputil.UnmarshalJSONRequest(req, &kb)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
AuthData: kb.AuthData,
|
||||||
|
Algorithm: kb.Algorithm,
|
||||||
|
}, &performKeyBackupResp)
|
||||||
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
|
}
|
||||||
|
if !performKeyBackupResp.Exists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unclear what the 200 body should be
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
Version: performKeyBackupResp.Version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a version of key backup. Version must not be empty. If the key backup was previously deleted, will return 200 OK.
|
||||||
|
// Implements DELETE /_matrix/client/r0/room_keys/version/{version}
|
||||||
|
func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string) util.JSONResponse {
|
||||||
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
DeleteBackup: true,
|
||||||
|
}, &performKeyBackupResp)
|
||||||
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
|
}
|
||||||
|
if !performKeyBackupResp.Exists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Unclear what the 200 body should be
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: keyBackupVersionCreateResponse{
|
||||||
|
Version: performKeyBackupResp.Version,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload a bunch of session keys for a given `version`.
|
||||||
|
func UploadBackupKeys(
|
||||||
|
req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest,
|
||||||
|
) util.JSONResponse {
|
||||||
|
var performKeyBackupResp userapi.PerformKeyBackupResponse
|
||||||
|
userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
Keys: *keys,
|
||||||
|
}, &performKeyBackupResp)
|
||||||
|
if performKeyBackupResp.Error != "" {
|
||||||
|
if performKeyBackupResp.BadInput {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue(performKeyBackupResp.Error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.ErrorResponse(fmt.Errorf("PerformKeyBackup: %s", performKeyBackupResp.Error))
|
||||||
|
}
|
||||||
|
if !performKeyBackupResp.Exists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("backup version not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: keyBackupSessionResponse{
|
||||||
|
Count: performKeyBackupResp.KeyCount,
|
||||||
|
ETag: performKeyBackupResp.KeyETag,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get keys from a given backup version. Response returned varies depending on if roomID and sessionID are set.
|
||||||
|
func GetBackupKeys(
|
||||||
|
req *http.Request, userAPI userapi.UserInternalAPI, device *userapi.Device, version, roomID, sessionID string,
|
||||||
|
) util.JSONResponse {
|
||||||
|
var queryResp userapi.QueryKeyBackupResponse
|
||||||
|
userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
|
Version: version,
|
||||||
|
ReturnKeys: true,
|
||||||
|
KeysForRoomID: roomID,
|
||||||
|
KeysForSessionID: sessionID,
|
||||||
|
}, &queryResp)
|
||||||
|
if queryResp.Error != "" {
|
||||||
|
return util.ErrorResponse(fmt.Errorf("QueryKeyBackup: %s", queryResp.Error))
|
||||||
|
}
|
||||||
|
if !queryResp.Exists {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("version not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sessionID != "" {
|
||||||
|
// return the key itself if it was found
|
||||||
|
roomData, ok := queryResp.Keys[roomID]
|
||||||
|
if ok {
|
||||||
|
key, ok := roomData[sessionID]
|
||||||
|
if ok {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if roomID != "" {
|
||||||
|
roomData, ok := queryResp.Keys[roomID]
|
||||||
|
if ok {
|
||||||
|
// wrap response in "sessions"
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: roomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// response is the same as the upload request
|
||||||
|
var resp keyBackupSessionRequest
|
||||||
|
resp.Rooms = make(map[string]struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
})
|
||||||
|
for roomID, roomData := range queryResp.Keys {
|
||||||
|
resp.Rooms[roomID] = struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: roomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: resp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 404,
|
||||||
|
JSON: jsonerror.NotFound("keys not found"),
|
||||||
|
}
|
||||||
|
}
|
||||||
125
clientapi/routing/key_crosssigning.go
Normal file
125
clientapi/routing/key_crosssigning.go
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
// 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 routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/auth"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
||||||
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
|
"github.com/matrix-org/dendrite/keyserver/api"
|
||||||
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
func UploadCrossSigningDeviceKeys(
|
||||||
|
req *http.Request, userInteractiveAuth *auth.UserInteractive,
|
||||||
|
keyserverAPI api.KeyInternalAPI, device *userapi.Device,
|
||||||
|
accountDB accounts.Database, cfg *config.ClientAPI,
|
||||||
|
) util.JSONResponse {
|
||||||
|
uploadReq := &api.PerformUploadDeviceKeysRequest{}
|
||||||
|
uploadRes := &api.PerformUploadDeviceKeysResponse{}
|
||||||
|
|
||||||
|
ctx := req.Context()
|
||||||
|
defer req.Body.Close() // nolint:errcheck
|
||||||
|
bodyBytes, err := ioutil.ReadAll(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("The request body could not be read: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := userInteractiveAuth.Verify(ctx, bodyBytes, device); err != nil {
|
||||||
|
return *err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = json.Unmarshal(bodyBytes, &uploadReq); err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("The request body could not be unmarshalled: " + err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadReq.UserID = device.UserID
|
||||||
|
keyserverAPI.PerformUploadDeviceKeys(req.Context(), uploadReq, uploadRes)
|
||||||
|
|
||||||
|
if err := uploadRes.Error; err != nil {
|
||||||
|
switch {
|
||||||
|
case err.IsInvalidSignature:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidSignature(err.Error()),
|
||||||
|
}
|
||||||
|
case err.IsMissingParam:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.MissingParam(err.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse {
|
||||||
|
uploadReq := &api.PerformUploadDeviceSignaturesRequest{}
|
||||||
|
uploadRes := &api.PerformUploadDeviceSignaturesResponse{}
|
||||||
|
|
||||||
|
if err := httputil.UnmarshalJSONRequest(req, &uploadReq.Signatures); err != nil {
|
||||||
|
return *err
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadReq.UserID = device.UserID
|
||||||
|
keyserverAPI.PerformUploadDeviceSignatures(req.Context(), uploadReq, uploadRes)
|
||||||
|
|
||||||
|
if err := uploadRes.Error; err != nil {
|
||||||
|
switch {
|
||||||
|
case err.IsInvalidSignature:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidSignature(err.Error()),
|
||||||
|
}
|
||||||
|
case err.IsMissingParam:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.MissingParam(err.Error()),
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.Unknown(err.Error()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusOK,
|
||||||
|
JSON: struct{}{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -100,7 +100,7 @@ func (r *queryKeysRequest) GetTimeout() time.Duration {
|
||||||
return time.Duration(r.Timeout) * time.Millisecond
|
return time.Duration(r.Timeout) * time.Millisecond
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse {
|
func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse {
|
||||||
var r queryKeysRequest
|
var r queryKeysRequest
|
||||||
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
resErr := httputil.UnmarshalJSONRequest(req, &r)
|
||||||
if resErr != nil {
|
if resErr != nil {
|
||||||
|
|
@ -108,6 +108,7 @@ func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse {
|
||||||
}
|
}
|
||||||
queryRes := api.QueryKeysResponse{}
|
queryRes := api.QueryKeysResponse{}
|
||||||
keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
keyAPI.QueryKeys(req.Context(), &api.QueryKeysRequest{
|
||||||
|
UserID: device.UserID,
|
||||||
UserToDevices: r.DeviceKeys,
|
UserToDevices: r.DeviceKeys,
|
||||||
Timeout: r.GetTimeout(),
|
Timeout: r.GetTimeout(),
|
||||||
// TODO: Token?
|
// TODO: Token?
|
||||||
|
|
@ -116,6 +117,9 @@ func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse {
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: map[string]interface{}{
|
JSON: map[string]interface{}{
|
||||||
"device_keys": queryRes.DeviceKeys,
|
"device_keys": queryRes.DeviceKeys,
|
||||||
|
"master_keys": queryRes.MasterKeys,
|
||||||
|
"self_signing_keys": queryRes.SelfSigningKeys,
|
||||||
|
"user_signing_keys": queryRes.UserSigningKeys,
|
||||||
"failures": queryRes.Failures,
|
"failures": queryRes.Failures,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,37 @@ func SendBan(
|
||||||
if reqErr != nil {
|
if reqErr != nil {
|
||||||
return *reqErr
|
return *reqErr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
||||||
|
if errRes != nil {
|
||||||
|
return *errRes
|
||||||
|
}
|
||||||
|
|
||||||
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
||||||
|
EventType: gomatrixserverlib.MRoomPowerLevels,
|
||||||
|
StateKey: "",
|
||||||
|
})
|
||||||
|
if plEvent == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, no power_levels event in this room."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pl, err := plEvent.PowerLevels()
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
||||||
|
if !allowedToBan {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("You don't have permission to ban this user, power level too low."),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sendMembership(req.Context(), accountDB, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
return sendMembership(req.Context(), accountDB, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,10 +17,7 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/sha1"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -594,7 +591,6 @@ func handleRegistrationFlow(
|
||||||
accessToken string,
|
accessToken string,
|
||||||
accessTokenErr error,
|
accessTokenErr error,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
// TODO: Shared secret registration (create new user scripts)
|
|
||||||
// TODO: Enable registration config flag
|
// TODO: Enable registration config flag
|
||||||
// TODO: Guest account upgrading
|
// TODO: Guest account upgrading
|
||||||
|
|
||||||
|
|
@ -643,20 +639,6 @@ func handleRegistrationFlow(
|
||||||
// Add Recaptcha to the list of completed registration stages
|
// Add Recaptcha to the list of completed registration stages
|
||||||
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
||||||
|
|
||||||
case authtypes.LoginTypeSharedSecret:
|
|
||||||
// Check shared secret against config
|
|
||||||
valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
util.GetLogger(req.Context()).WithError(err).Error("isValidMacLogin failed")
|
|
||||||
return jsonerror.InternalServerError()
|
|
||||||
} else if !valid {
|
|
||||||
return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add SharedSecret to the list of completed registration stages
|
|
||||||
AddCompletedSessionStage(sessionID, authtypes.LoginTypeSharedSecret)
|
|
||||||
|
|
||||||
case authtypes.LoginTypeDummy:
|
case authtypes.LoginTypeDummy:
|
||||||
// there is nothing to do
|
// there is nothing to do
|
||||||
// Add Dummy to the list of completed registration stages
|
// Add Dummy to the list of completed registration stages
|
||||||
|
|
@ -849,49 +831,6 @@ func completeRegistration(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Used for shared secret registration.
|
|
||||||
// Checks if the username, password and isAdmin flag matches the given mac.
|
|
||||||
func isValidMacLogin(
|
|
||||||
cfg *config.ClientAPI,
|
|
||||||
username, password string,
|
|
||||||
isAdmin bool,
|
|
||||||
givenMac []byte,
|
|
||||||
) (bool, error) {
|
|
||||||
sharedSecret := cfg.RegistrationSharedSecret
|
|
||||||
|
|
||||||
// Check that shared secret registration isn't disabled.
|
|
||||||
if cfg.RegistrationSharedSecret == "" {
|
|
||||||
return false, errors.New("Shared secret registration is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double check that username/password don't contain the HMAC delimiters. We should have
|
|
||||||
// already checked this.
|
|
||||||
if strings.Contains(username, "\x00") {
|
|
||||||
return false, errors.New("Username contains invalid character")
|
|
||||||
}
|
|
||||||
if strings.Contains(password, "\x00") {
|
|
||||||
return false, errors.New("Password contains invalid character")
|
|
||||||
}
|
|
||||||
if sharedSecret == "" {
|
|
||||||
return false, errors.New("Shared secret registration is disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
adminString := "notadmin"
|
|
||||||
if isAdmin {
|
|
||||||
adminString = "admin"
|
|
||||||
}
|
|
||||||
joined := strings.Join([]string{username, password, adminString}, "\x00")
|
|
||||||
|
|
||||||
mac := hmac.New(sha1.New, []byte(sharedSecret))
|
|
||||||
_, err := mac.Write([]byte(joined))
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
expectedMAC := mac.Sum(nil)
|
|
||||||
|
|
||||||
return hmac.Equal(givenMac, expectedMAC), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkFlows checks a single completed flow against another required one. If
|
// checkFlows checks a single completed flow against another required one. If
|
||||||
// one contains at least all of the stages that the other does, checkFlows
|
// one contains at least all of the stages that the other does, checkFlows
|
||||||
// returns true.
|
// returns true.
|
||||||
|
|
@ -995,3 +934,34 @@ func RegisterAvailable(
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func handleSharedSecretRegistration(userAPI userapi.UserInternalAPI, sr *SharedSecretRegistration, req *http.Request) util.JSONResponse {
|
||||||
|
ssrr, err := NewSharedSecretRegistrationRequest(req.Body)
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.BadJSON(fmt.Sprintf("malformed json: %s", err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
valid, err := sr.IsValidMacLogin(ssrr.Nonce, ssrr.User, ssrr.Password, ssrr.Admin, ssrr.MacBytes)
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 403,
|
||||||
|
JSON: jsonerror.Forbidden("bad mac"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// downcase capitals
|
||||||
|
ssrr.User = strings.ToLower(ssrr.User)
|
||||||
|
|
||||||
|
if resErr := validateUsername(ssrr.User); resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
if resErr := validatePassword(ssrr.Password); resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
deviceID := "shared_secret_registration"
|
||||||
|
return completeRegistration(req.Context(), userAPI, ssrr.User, ssrr.Password, "", req.RemoteAddr, req.UserAgent(), false, &ssrr.User, &deviceID)
|
||||||
|
}
|
||||||
|
|
|
||||||
99
clientapi/routing/register_secret.go
Normal file
99
clientapi/routing/register_secret.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha1"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
|
cache "github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SharedSecretRegistrationRequest struct {
|
||||||
|
User string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
MacBytes []byte
|
||||||
|
MacStr string `json:"mac"`
|
||||||
|
Admin bool `json:"admin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSharedSecretRegistrationRequest(reader io.ReadCloser) (*SharedSecretRegistrationRequest, error) {
|
||||||
|
defer internal.CloseAndLogIfError(context.Background(), reader, "NewSharedSecretRegistrationRequest: failed to close request body")
|
||||||
|
var ssrr SharedSecretRegistrationRequest
|
||||||
|
err := json.NewDecoder(reader).Decode(&ssrr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ssrr.MacBytes, err = hex.DecodeString(ssrr.MacStr)
|
||||||
|
return &ssrr, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type SharedSecretRegistration struct {
|
||||||
|
sharedSecret string
|
||||||
|
nonces *cache.Cache
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSharedSecretRegistration(sharedSecret string) *SharedSecretRegistration {
|
||||||
|
return &SharedSecretRegistration{
|
||||||
|
sharedSecret: sharedSecret,
|
||||||
|
// nonces live for 5mins, purge every 10mins
|
||||||
|
nonces: cache.New(5*time.Minute, 10*time.Minute),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SharedSecretRegistration) GenerateNonce() string {
|
||||||
|
nonce := util.RandomString(16)
|
||||||
|
r.nonces.Set(nonce, true, cache.DefaultExpiration)
|
||||||
|
return nonce
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SharedSecretRegistration) validNonce(nonce string) bool {
|
||||||
|
_, exists := r.nonces.Get(nonce)
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *SharedSecretRegistration) IsValidMacLogin(
|
||||||
|
nonce, username, password string,
|
||||||
|
isAdmin bool,
|
||||||
|
givenMac []byte,
|
||||||
|
) (bool, error) {
|
||||||
|
// Check that shared secret registration isn't disabled.
|
||||||
|
if r.sharedSecret == "" {
|
||||||
|
return false, errors.New("Shared secret registration is disabled")
|
||||||
|
}
|
||||||
|
if !r.validNonce(nonce) {
|
||||||
|
return false, fmt.Errorf("Incorrect or expired nonce: %s", nonce)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that username/password don't contain the HMAC delimiters.
|
||||||
|
if strings.Contains(username, "\x00") {
|
||||||
|
return false, errors.New("Username contains invalid character")
|
||||||
|
}
|
||||||
|
if strings.Contains(password, "\x00") {
|
||||||
|
return false, errors.New("Password contains invalid character")
|
||||||
|
}
|
||||||
|
|
||||||
|
adminString := "notadmin"
|
||||||
|
if isAdmin {
|
||||||
|
adminString = "admin"
|
||||||
|
}
|
||||||
|
joined := strings.Join([]string{nonce, username, password, adminString}, "\x00")
|
||||||
|
|
||||||
|
mac := hmac.New(sha1.New, []byte(r.sharedSecret))
|
||||||
|
_, err := mac.Write([]byte(joined))
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
expectedMAC := mac.Sum(nil)
|
||||||
|
|
||||||
|
return hmac.Equal(givenMac, expectedMAC), nil
|
||||||
|
}
|
||||||
43
clientapi/routing/register_secret_test.go
Normal file
43
clientapi/routing/register_secret_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
package routing
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io/ioutil"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSharedSecretRegister(t *testing.T) {
|
||||||
|
// these values have come from a local synapse instance to ensure compatibility
|
||||||
|
jsonStr := []byte(`{"admin":false,"mac":"f1ba8d37123866fd659b40de4bad9b0f8965c565","nonce":"759f047f312b99ff428b21d581256f8592b8976e58bc1b543972dc6147e529a79657605b52d7becd160ff5137f3de11975684319187e06901955f79e5a6c5a79","password":"wonderland","username":"alice"}`)
|
||||||
|
sharedSecret := "dendritetest"
|
||||||
|
|
||||||
|
req, err := NewSharedSecretRegistrationRequest(ioutil.NopCloser(bytes.NewBuffer(jsonStr)))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read request: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := NewSharedSecretRegistration(sharedSecret)
|
||||||
|
|
||||||
|
// force the nonce to be known
|
||||||
|
r.nonces.Set(req.Nonce, true, cache.DefaultExpiration)
|
||||||
|
|
||||||
|
valid, err := r.IsValidMacLogin(req.Nonce, req.User, req.Password, req.Admin, req.MacBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to check for valid mac: %s", err)
|
||||||
|
}
|
||||||
|
if !valid {
|
||||||
|
t.Errorf("mac login failed, wanted success")
|
||||||
|
}
|
||||||
|
|
||||||
|
// modify the mac so it fails
|
||||||
|
req.MacBytes[0] = 0xff
|
||||||
|
valid, err = r.IsValidMacLogin(req.Nonce, req.User, req.Password, req.Admin, req.MacBytes)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to check for valid mac: %s", err)
|
||||||
|
}
|
||||||
|
if valid {
|
||||||
|
t.Errorf("mac login succeeded, wanted failure")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -37,6 +37,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
// Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client
|
||||||
|
|
@ -46,7 +47,7 @@ import (
|
||||||
// applied:
|
// applied:
|
||||||
// nolint: gocyclo
|
// nolint: gocyclo
|
||||||
func Setup(
|
func Setup(
|
||||||
publicAPIMux *mux.Router, cfg *config.ClientAPI,
|
publicAPIMux, synapseAdminRouter *mux.Router, cfg *config.ClientAPI,
|
||||||
eduAPI eduServerAPI.EDUServerInputAPI,
|
eduAPI eduServerAPI.EDUServerInputAPI,
|
||||||
rsAPI roomserverAPI.RoomserverInternalAPI,
|
rsAPI roomserverAPI.RoomserverInternalAPI,
|
||||||
asAPI appserviceAPI.AppServiceQueryAPI,
|
asAPI appserviceAPI.AppServiceQueryAPI,
|
||||||
|
|
@ -63,7 +64,9 @@ func Setup(
|
||||||
rateLimits := newRateLimits(&cfg.RateLimiting)
|
rateLimits := newRateLimits(&cfg.RateLimiting)
|
||||||
userInteractiveAuth := auth.NewUserInteractive(accountDB.GetAccountByPassword, cfg)
|
userInteractiveAuth := auth.NewUserInteractive(accountDB.GetAccountByPassword, cfg)
|
||||||
|
|
||||||
unstableFeatures := make(map[string]bool)
|
unstableFeatures := map[string]bool{
|
||||||
|
//"org.matrix.e2e_cross_signing": true,
|
||||||
|
}
|
||||||
for _, msc := range cfg.MSCs.MSCs {
|
for _, msc := range cfg.MSCs.MSCs {
|
||||||
unstableFeatures["org.matrix."+msc] = true
|
unstableFeatures["org.matrix."+msc] = true
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +91,32 @@ func Setup(
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
if cfg.RegistrationSharedSecret != "" {
|
||||||
|
logrus.Info("Enabling shared secret registration at /_synapse/admin/v1/register")
|
||||||
|
sr := NewSharedSecretRegistration(cfg.RegistrationSharedSecret)
|
||||||
|
synapseAdminRouter.Handle("/admin/v1/register",
|
||||||
|
httputil.MakeExternalAPI("shared_secret_registration", func(req *http.Request) util.JSONResponse {
|
||||||
|
if req.Method == http.MethodGet {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 200,
|
||||||
|
JSON: struct {
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
}{
|
||||||
|
Nonce: sr.GenerateNonce(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if req.Method == http.MethodPost {
|
||||||
|
return handleSharedSecretRegistration(userAPI, sr, req)
|
||||||
|
}
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusMethodNotAllowed,
|
||||||
|
JSON: jsonerror.NotFound("unknown method"),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
|
||||||
|
}
|
||||||
|
|
||||||
r0mux := publicAPIMux.PathPrefix("/r0").Subrouter()
|
r0mux := publicAPIMux.PathPrefix("/r0").Subrouter()
|
||||||
unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter()
|
unstableMux := publicAPIMux.PathPrefix("/unstable").Subrouter()
|
||||||
|
|
||||||
|
|
@ -248,6 +277,14 @@ func Setup(
|
||||||
return OnIncomingStateRequest(req.Context(), device, rsAPI, vars["roomID"])
|
return OnIncomingStateRequest(req.Context(), device, rsAPI, vars["roomID"])
|
||||||
})).Methods(http.MethodGet, http.MethodOptions)
|
})).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
r0mux.Handle("/rooms/{roomID}/aliases", httputil.MakeAuthAPI("aliases", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return GetAliases(req, rsAPI, device, vars["roomID"])
|
||||||
|
})).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
r0mux.Handle("/rooms/{roomID}/state/{type:[^/]+/?}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
r0mux.Handle("/rooms/{roomID}/state/{type:[^/]+/?}", httputil.MakeAuthAPI("room_state", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -861,6 +898,192 @@ func Setup(
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodGet, http.MethodOptions)
|
).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
// Key Backup Versions (Metadata)
|
||||||
|
|
||||||
|
getBackupKeysVersion := httputil.MakeAuthAPI("get_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return KeyBackupVersion(req, userAPI, device, vars["version"])
|
||||||
|
})
|
||||||
|
|
||||||
|
getLatestBackupKeysVersion := httputil.MakeAuthAPI("get_latest_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return KeyBackupVersion(req, userAPI, device, "")
|
||||||
|
})
|
||||||
|
|
||||||
|
putBackupKeysVersion := httputil.MakeAuthAPI("put_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return ModifyKeyBackupVersionAuthData(req, userAPI, device, vars["version"])
|
||||||
|
})
|
||||||
|
|
||||||
|
deleteBackupKeysVersion := httputil.MakeAuthAPI("delete_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return DeleteKeyBackupVersion(req, userAPI, device, vars["version"])
|
||||||
|
})
|
||||||
|
|
||||||
|
postNewBackupKeysVersion := httputil.MakeAuthAPI("post_new_backup_keys_version", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return CreateKeyBackupVersion(req, userAPI, device)
|
||||||
|
})
|
||||||
|
|
||||||
|
r0mux.Handle("/room_keys/version/{version}", getBackupKeysVersion).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
r0mux.Handle("/room_keys/version", getLatestBackupKeysVersion).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
r0mux.Handle("/room_keys/version/{version}", putBackupKeysVersion).Methods(http.MethodPut)
|
||||||
|
r0mux.Handle("/room_keys/version/{version}", deleteBackupKeysVersion).Methods(http.MethodDelete)
|
||||||
|
r0mux.Handle("/room_keys/version", postNewBackupKeysVersion).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
unstableMux.Handle("/room_keys/version/{version}", getBackupKeysVersion).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
unstableMux.Handle("/room_keys/version", getLatestBackupKeysVersion).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
unstableMux.Handle("/room_keys/version/{version}", putBackupKeysVersion).Methods(http.MethodPut)
|
||||||
|
unstableMux.Handle("/room_keys/version/{version}", deleteBackupKeysVersion).Methods(http.MethodDelete)
|
||||||
|
unstableMux.Handle("/room_keys/version", postNewBackupKeysVersion).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
// Inserting E2E Backup Keys
|
||||||
|
|
||||||
|
// Bulk room and session
|
||||||
|
putBackupKeys := httputil.MakeAuthAPI("put_backup_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
version := req.URL.Query().Get("version")
|
||||||
|
if version == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var reqBody keyBackupSessionRequest
|
||||||
|
resErr := clientutil.UnmarshalJSONRequest(req, &reqBody)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
return UploadBackupKeys(req, userAPI, device, version, &reqBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Single room bulk session
|
||||||
|
putBackupKeysRoom := httputil.MakeAuthAPI("put_backup_keys_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
version := req.URL.Query().Get("version")
|
||||||
|
if version == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roomID := vars["roomID"]
|
||||||
|
var reqBody keyBackupSessionRequest
|
||||||
|
reqBody.Rooms = make(map[string]struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
})
|
||||||
|
reqBody.Rooms[roomID] = struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: map[string]userapi.KeyBackupSession{},
|
||||||
|
}
|
||||||
|
body := reqBody.Rooms[roomID]
|
||||||
|
resErr := clientutil.UnmarshalJSONRequest(req, &body)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
reqBody.Rooms[roomID] = body
|
||||||
|
return UploadBackupKeys(req, userAPI, device, version, &reqBody)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Single room, single session
|
||||||
|
putBackupKeysRoomSession := httputil.MakeAuthAPI("put_backup_keys_room_session", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
version := req.URL.Query().Get("version")
|
||||||
|
if version == "" {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: 400,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue("version must be specified"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var reqBody userapi.KeyBackupSession
|
||||||
|
resErr := clientutil.UnmarshalJSONRequest(req, &reqBody)
|
||||||
|
if resErr != nil {
|
||||||
|
return *resErr
|
||||||
|
}
|
||||||
|
roomID := vars["roomID"]
|
||||||
|
sessionID := vars["sessionID"]
|
||||||
|
var keyReq keyBackupSessionRequest
|
||||||
|
keyReq.Rooms = make(map[string]struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
})
|
||||||
|
keyReq.Rooms[roomID] = struct {
|
||||||
|
Sessions map[string]userapi.KeyBackupSession `json:"sessions"`
|
||||||
|
}{
|
||||||
|
Sessions: make(map[string]userapi.KeyBackupSession),
|
||||||
|
}
|
||||||
|
keyReq.Rooms[roomID].Sessions[sessionID] = reqBody
|
||||||
|
return UploadBackupKeys(req, userAPI, device, version, &keyReq)
|
||||||
|
})
|
||||||
|
|
||||||
|
r0mux.Handle("/room_keys/keys", putBackupKeys).Methods(http.MethodPut)
|
||||||
|
r0mux.Handle("/room_keys/keys/{roomID}", putBackupKeysRoom).Methods(http.MethodPut)
|
||||||
|
r0mux.Handle("/room_keys/keys/{roomID}/{sessionID}", putBackupKeysRoomSession).Methods(http.MethodPut)
|
||||||
|
|
||||||
|
unstableMux.Handle("/room_keys/keys", putBackupKeys).Methods(http.MethodPut)
|
||||||
|
unstableMux.Handle("/room_keys/keys/{roomID}", putBackupKeysRoom).Methods(http.MethodPut)
|
||||||
|
unstableMux.Handle("/room_keys/keys/{roomID}/{sessionID}", putBackupKeysRoomSession).Methods(http.MethodPut)
|
||||||
|
|
||||||
|
// Querying E2E Backup Keys
|
||||||
|
|
||||||
|
getBackupKeys := httputil.MakeAuthAPI("get_backup_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return GetBackupKeys(req, userAPI, device, req.URL.Query().Get("version"), "", "")
|
||||||
|
})
|
||||||
|
|
||||||
|
getBackupKeysRoom := httputil.MakeAuthAPI("get_backup_keys_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return GetBackupKeys(req, userAPI, device, req.URL.Query().Get("version"), vars["roomID"], "")
|
||||||
|
})
|
||||||
|
|
||||||
|
getBackupKeysRoomSession := httputil.MakeAuthAPI("get_backup_keys_room_session", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
vars, err := httputil.URLDecodeMapValues(mux.Vars(req))
|
||||||
|
if err != nil {
|
||||||
|
return util.ErrorResponse(err)
|
||||||
|
}
|
||||||
|
return GetBackupKeys(req, userAPI, device, req.URL.Query().Get("version"), vars["roomID"], vars["sessionID"])
|
||||||
|
})
|
||||||
|
|
||||||
|
r0mux.Handle("/room_keys/keys", getBackupKeys).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
r0mux.Handle("/room_keys/keys/{roomID}", getBackupKeysRoom).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
r0mux.Handle("/room_keys/keys/{roomID}/{sessionID}", getBackupKeysRoomSession).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
unstableMux.Handle("/room_keys/keys", getBackupKeys).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
unstableMux.Handle("/room_keys/keys/{roomID}", getBackupKeysRoom).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
unstableMux.Handle("/room_keys/keys/{roomID}/{sessionID}", getBackupKeysRoomSession).Methods(http.MethodGet, http.MethodOptions)
|
||||||
|
|
||||||
|
// Deleting E2E Backup Keys
|
||||||
|
|
||||||
|
// Cross-signing device keys
|
||||||
|
|
||||||
|
postDeviceSigningKeys := httputil.MakeAuthAPI("post_device_signing_keys", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return UploadCrossSigningDeviceKeys(req, userInteractiveAuth, keyAPI, device, accountDB, cfg)
|
||||||
|
})
|
||||||
|
|
||||||
|
postDeviceSigningSignatures := httputil.MakeAuthAPI("post_device_signing_signatures", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
return UploadCrossSigningDeviceSignatures(req, keyAPI, device)
|
||||||
|
})
|
||||||
|
|
||||||
|
r0mux.Handle("/keys/device_signing/upload", postDeviceSigningKeys).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
r0mux.Handle("/keys/signatures/upload", postDeviceSigningSignatures).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
|
unstableMux.Handle("/keys/device_signing/upload", postDeviceSigningKeys).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
unstableMux.Handle("/keys/signatures/upload", postDeviceSigningSignatures).Methods(http.MethodPost, http.MethodOptions)
|
||||||
|
|
||||||
// Supplying a device ID is deprecated.
|
// Supplying a device ID is deprecated.
|
||||||
r0mux.Handle("/keys/upload/{deviceID}",
|
r0mux.Handle("/keys/upload/{deviceID}",
|
||||||
httputil.MakeAuthAPI("keys_upload", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("keys_upload", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
|
|
@ -874,7 +1097,7 @@ func Setup(
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
r0mux.Handle("/keys/query",
|
r0mux.Handle("/keys/query",
|
||||||
httputil.MakeAuthAPI("keys_query", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
httputil.MakeAuthAPI("keys_query", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse {
|
||||||
return QueryKeys(req, keyAPI)
|
return QueryKeys(req, keyAPI, device)
|
||||||
}),
|
}),
|
||||||
).Methods(http.MethodPost, http.MethodOptions)
|
).Methods(http.MethodPost, http.MethodOptions)
|
||||||
r0mux.Handle("/keys/claim",
|
r0mux.Handle("/keys/claim",
|
||||||
|
|
|
||||||
|
|
@ -18,13 +18,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
"github.com/matrix-org/dendrite/userapi/storage/accounts"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
const usage = `Usage: %s
|
const usage = `Usage: %s
|
||||||
|
|
@ -33,7 +38,15 @@ Creates a new user account on the homeserver.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
./create-account --config dendrite.yaml --username alice --password foobarbaz
|
# provide password by parameter
|
||||||
|
%s --config dendrite.yaml -username alice -password foobarbaz
|
||||||
|
# use password from file
|
||||||
|
%s --config dendrite.yaml -username alice -passwordfile my.pass
|
||||||
|
# ask user to provide password
|
||||||
|
%s --config dendrite.yaml -username alice -ask-pass
|
||||||
|
# read password from stdin
|
||||||
|
%s --config dendrite.yaml -username alice -passwordstdin < my.pass
|
||||||
|
cat my.pass | %s --config dendrite.yaml -username alice -passwordstdin
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
|
|
||||||
|
|
@ -42,11 +55,15 @@ Arguments:
|
||||||
var (
|
var (
|
||||||
username = flag.String("username", "", "The username of the account to register (specify the localpart only, e.g. 'alice' for '@alice:domain.com')")
|
username = flag.String("username", "", "The username of the account to register (specify the localpart only, e.g. 'alice' for '@alice:domain.com')")
|
||||||
password = flag.String("password", "", "The password to associate with the account (optional, account will be password-less if not specified)")
|
password = flag.String("password", "", "The password to associate with the account (optional, account will be password-less if not specified)")
|
||||||
|
pwdFile = flag.String("passwordfile", "", "The file to use for the password (e.g. for automated account creation)")
|
||||||
|
pwdStdin = flag.Bool("passwordstdin", false, "Reads the password from stdin")
|
||||||
|
askPass = flag.Bool("ask-pass", false, "Ask for the password to use")
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
name := os.Args[0]
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, usage, os.Args[0])
|
_, _ = fmt.Fprintf(os.Stderr, usage, name, name, name, name, name, name)
|
||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
}
|
}
|
||||||
cfg := setup.ParseFlags(true)
|
cfg := setup.ParseFlags(true)
|
||||||
|
|
@ -56,6 +73,8 @@ func main() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pass := getPassword(password, pwdFile, pwdStdin, askPass, os.Stdin)
|
||||||
|
|
||||||
accountDB, err := accounts.NewDatabase(&config.DatabaseOptions{
|
accountDB, err := accounts.NewDatabase(&config.DatabaseOptions{
|
||||||
ConnectionString: cfg.UserAPI.AccountDatabase.ConnectionString,
|
ConnectionString: cfg.UserAPI.AccountDatabase.ConnectionString,
|
||||||
}, cfg.Global.ServerName, bcrypt.DefaultCost, cfg.UserAPI.OpenIDTokenLifetimeMS)
|
}, cfg.Global.ServerName, bcrypt.DefaultCost, cfg.UserAPI.OpenIDTokenLifetimeMS)
|
||||||
|
|
@ -63,10 +82,61 @@ func main() {
|
||||||
logrus.Fatalln("Failed to connect to the database:", err.Error())
|
logrus.Fatalln("Failed to connect to the database:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = accountDB.CreateAccount(context.Background(), *username, *password, "")
|
_, err = accountDB.CreateAccount(context.Background(), *username, pass, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Fatalln("Failed to create the account:", err.Error())
|
logrus.Fatalln("Failed to create the account:", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
logrus.Infoln("Created account", *username)
|
logrus.Infoln("Created account", *username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getPassword(password, pwdFile *string, pwdStdin, askPass *bool, r io.Reader) string {
|
||||||
|
// no password option set, use empty password
|
||||||
|
if password == nil && pwdFile == nil && pwdStdin == nil && askPass == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// password defined as parameter
|
||||||
|
if password != nil && *password != "" {
|
||||||
|
return *password
|
||||||
|
}
|
||||||
|
|
||||||
|
// read password from file
|
||||||
|
if pwdFile != nil && *pwdFile != "" {
|
||||||
|
pw, err := ioutil.ReadFile(*pwdFile)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalln("Unable to read password from file:", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(pw))
|
||||||
|
}
|
||||||
|
|
||||||
|
// read password from stdin
|
||||||
|
if pwdStdin != nil && *pwdStdin {
|
||||||
|
data, err := ioutil.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalln("Unable to read password from stdin:", err)
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ask the user to provide the password
|
||||||
|
if *askPass {
|
||||||
|
fmt.Print("Enter Password: ")
|
||||||
|
bytePassword, err := term.ReadPassword(syscall.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalln("Unable to read password:", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Print("Confirm Password: ")
|
||||||
|
bytePassword2, err := term.ReadPassword(syscall.Stdin)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalln("Unable to read password:", err)
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
if strings.TrimSpace(string(bytePassword)) != strings.TrimSpace(string(bytePassword2)) {
|
||||||
|
logrus.Fatalln("Entered passwords don't match")
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(bytePassword))
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
||||||
62
cmd/create-account/main_test.go
Normal file
62
cmd/create-account/main_test.go
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_getPassword(t *testing.T) {
|
||||||
|
type args struct {
|
||||||
|
password *string
|
||||||
|
pwdFile *string
|
||||||
|
pwdStdin *bool
|
||||||
|
askPass *bool
|
||||||
|
reader io.Reader
|
||||||
|
}
|
||||||
|
|
||||||
|
pass := "mySecretPass"
|
||||||
|
passwordFile := "testdata/my.pass"
|
||||||
|
passwordStdin := true
|
||||||
|
reader := &bytes.Buffer{}
|
||||||
|
_, err := reader.WriteString(pass)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unable to write to buffer: %+v", err)
|
||||||
|
}
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
args args
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no password defined",
|
||||||
|
args: args{},
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "password defined",
|
||||||
|
args: args{password: &pass},
|
||||||
|
want: pass,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "pwdFile defined",
|
||||||
|
args: args{pwdFile: &passwordFile},
|
||||||
|
want: pass,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "read pass from stdin defined",
|
||||||
|
args: args{
|
||||||
|
pwdStdin: &passwordStdin,
|
||||||
|
reader: reader,
|
||||||
|
},
|
||||||
|
want: pass,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if got := getPassword(tt.args.password, tt.args.pwdFile, tt.args.pwdStdin, tt.args.askPass, tt.args.reader); got != tt.want {
|
||||||
|
t.Errorf("getPassword() = '%v', want '%v'", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
1
cmd/create-account/testdata/my.pass
vendored
Normal file
1
cmd/create-account/testdata/my.pass
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
mySecretPass
|
||||||
|
|
@ -1,147 +0,0 @@
|
||||||
// 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.
|
|
||||||
|
|
||||||
// Generate a list of matrix room events for load testing.
|
|
||||||
// Writes the events to stdout by default.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"golang.org/x/crypto/ed25519"
|
|
||||||
)
|
|
||||||
|
|
||||||
const usage = `Usage: %s
|
|
||||||
|
|
||||||
Generate a list of matrix room events for load testing.
|
|
||||||
Writes the events to stdout separated by new lines
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
var (
|
|
||||||
serverName = flag.String("server-name", "localhost", "The name of the matrix server to generate events for")
|
|
||||||
keyID = flag.String("key-id", "ed25519:auto", "The ID of the key used to sign the events")
|
|
||||||
privateKeyString = flag.String("private-key", defaultKey, "Base64 encoded private key to sign events with")
|
|
||||||
roomID = flag.String("room-id", "!roomid:$SERVER_NAME", "The room ID to generate events in")
|
|
||||||
userID = flag.String("user-id", "@userid:$SERVER_NAME", "The user ID to use as the event sender")
|
|
||||||
messageCount = flag.Int("message-count", 10, "The number of m.room.messsage events to generate")
|
|
||||||
format = flag.String("Format", "InputRoomEvent", "The output format to use for the messages: InputRoomEvent or Event")
|
|
||||||
ver = flag.String("version", string(gomatrixserverlib.RoomVersionV1), "Room version to generate events as")
|
|
||||||
)
|
|
||||||
|
|
||||||
// By default we use a private key of 0.
|
|
||||||
const defaultKey = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
|
|
||||||
|
|
||||||
var privateKey ed25519.PrivateKey
|
|
||||||
var emptyString = ""
|
|
||||||
var now time.Time
|
|
||||||
var b gomatrixserverlib.EventBuilder
|
|
||||||
var eventID int
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, usage, os.Args[0])
|
|
||||||
flag.PrintDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
*userID = strings.Replace(*userID, "$SERVER_NAME", *serverName, 1)
|
|
||||||
*roomID = strings.Replace(*roomID, "$SERVER_NAME", *serverName, 1)
|
|
||||||
|
|
||||||
// Decode the ed25519 private key.
|
|
||||||
privateKeyBytes, err := base64.RawStdEncoding.DecodeString(*privateKeyString)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
privateKey = ed25519.PrivateKey(privateKeyBytes)
|
|
||||||
|
|
||||||
// Build a m.room.create event.
|
|
||||||
b.Sender = *userID
|
|
||||||
b.RoomID = *roomID
|
|
||||||
b.Type = "m.room.create"
|
|
||||||
b.StateKey = &emptyString
|
|
||||||
b.SetContent(map[string]string{"creator": *userID}) // nolint: errcheck
|
|
||||||
create := buildAndOutput()
|
|
||||||
|
|
||||||
// Build a m.room.member event.
|
|
||||||
b.Type = "m.room.member"
|
|
||||||
b.StateKey = userID
|
|
||||||
b.SetContent(map[string]string{"membership": gomatrixserverlib.Join}) // nolint: errcheck
|
|
||||||
b.AuthEvents = []gomatrixserverlib.EventReference{create}
|
|
||||||
member := buildAndOutput()
|
|
||||||
|
|
||||||
// Build a number of m.room.message events.
|
|
||||||
b.Type = "m.room.message"
|
|
||||||
b.StateKey = nil
|
|
||||||
b.SetContent(map[string]string{"body": "Test Message"}) // nolint: errcheck
|
|
||||||
b.AuthEvents = []gomatrixserverlib.EventReference{create, member}
|
|
||||||
for i := 0; i < *messageCount; i++ {
|
|
||||||
buildAndOutput()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build an event and write the event to the output.
|
|
||||||
func buildAndOutput() gomatrixserverlib.EventReference {
|
|
||||||
eventID++
|
|
||||||
now = time.Unix(0, 0)
|
|
||||||
name := gomatrixserverlib.ServerName(*serverName)
|
|
||||||
key := gomatrixserverlib.KeyID(*keyID)
|
|
||||||
|
|
||||||
event, err := b.Build(
|
|
||||||
now, name, key, privateKey,
|
|
||||||
gomatrixserverlib.RoomVersion(*ver),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
writeEvent(event)
|
|
||||||
reference := event.EventReference()
|
|
||||||
b.PrevEvents = []gomatrixserverlib.EventReference{reference}
|
|
||||||
b.Depth++
|
|
||||||
return reference
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write an event to the output.
|
|
||||||
func writeEvent(event *gomatrixserverlib.Event) {
|
|
||||||
encoder := json.NewEncoder(os.Stdout)
|
|
||||||
if *format == "InputRoomEvent" {
|
|
||||||
var ire api.InputRoomEvent
|
|
||||||
ire.Kind = api.KindNew
|
|
||||||
ire.Event = event.Headered(gomatrixserverlib.RoomVersion(*ver))
|
|
||||||
authEventIDs := []string{}
|
|
||||||
for _, ref := range b.AuthEvents.([]gomatrixserverlib.EventReference) {
|
|
||||||
authEventIDs = append(authEventIDs, ref.EventID)
|
|
||||||
}
|
|
||||||
ire.AuthEventIDs = authEventIDs
|
|
||||||
if err := encoder.Encode(ire); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
} else if *format == "Event" {
|
|
||||||
if err := encoder.Encode(event); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
panic(fmt.Errorf("Format %q is not valid, must be %q or %q", *format, "InputRoomEvent", "Event"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -44,6 +44,8 @@ import (
|
||||||
"github.com/matrix-org/dendrite/eduserver/cache"
|
"github.com/matrix-org/dendrite/eduserver/cache"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createKeyDB(
|
func createKeyDB(
|
||||||
|
|
@ -145,7 +147,7 @@ func main() {
|
||||||
|
|
||||||
accountDB := base.Base.CreateAccountsDB()
|
accountDB := base.Base.CreateAccountsDB()
|
||||||
federation := createFederationClient(base)
|
federation := createFederationClient(base)
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Base.Cfg.KeyServer, federation)
|
keyAPI := keyserver.NewInternalAPI(&base.Base, &base.Base.Cfg.KeyServer, federation)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
|
@ -197,6 +199,7 @@ func main() {
|
||||||
base.Base.PublicFederationAPIMux,
|
base.Base.PublicFederationAPIMux,
|
||||||
base.Base.PublicKeyAPIMux,
|
base.Base.PublicKeyAPIMux,
|
||||||
base.Base.PublicMediaAPIMux,
|
base.Base.PublicMediaAPIMux,
|
||||||
|
base.Base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
if err := mscs.Enable(&base.Base, &monolith); err != nil {
|
if err := mscs.Enable(&base.Base, &monolith); err != nil {
|
||||||
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
package conn
|
package conn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"nhooyr.io/websocket"
|
||||||
|
|
||||||
pineconeRouter "github.com/matrix-org/pinecone/router"
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
||||||
|
|
@ -17,11 +18,12 @@ import (
|
||||||
func ConnectToPeer(pRouter *pineconeRouter.Router, peer string) error {
|
func ConnectToPeer(pRouter *pineconeRouter.Router, peer string) error {
|
||||||
var parent net.Conn
|
var parent net.Conn
|
||||||
if strings.HasPrefix(peer, "ws://") || strings.HasPrefix(peer, "wss://") {
|
if strings.HasPrefix(peer, "ws://") || strings.HasPrefix(peer, "wss://") {
|
||||||
c, _, err := websocket.DefaultDialer.Dial(peer, nil)
|
ctx := context.Background()
|
||||||
|
c, _, err := websocket.Dial(ctx, peer, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("websocket.DefaultDialer.Dial: %w", err)
|
return fmt.Errorf("websocket.DefaultDialer.Dial: %w", err)
|
||||||
}
|
}
|
||||||
parent = WrapWebSocketConn(c)
|
parent = websocket.NetConn(ctx, c, websocket.MessageBinary)
|
||||||
} else {
|
} else {
|
||||||
var err error
|
var err error
|
||||||
parent, err = net.Dial("tcp", peer)
|
parent, err = net.Dial("tcp", peer)
|
||||||
|
|
@ -46,7 +48,13 @@ func (y *RoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTransport(s *pineconeSessions.Sessions) *http.Transport {
|
func createTransport(s *pineconeSessions.Sessions) *http.Transport {
|
||||||
tr := &http.Transport{}
|
tr := &http.Transport{
|
||||||
|
DisableKeepAlives: false,
|
||||||
|
Dial: s.Dial,
|
||||||
|
DialContext: s.DialContext,
|
||||||
|
DialTLS: s.DialTLS,
|
||||||
|
DialTLSContext: s.DialTLSContext,
|
||||||
|
}
|
||||||
tr.RegisterProtocol(
|
tr.RegisterProtocol(
|
||||||
"matrix", &RoundTripper{
|
"matrix", &RoundTripper{
|
||||||
inner: &http.Transport{
|
inner: &http.Transport{
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// +build riotweb
|
// +build elementweb
|
||||||
|
|
||||||
package embed
|
package embed
|
||||||
|
|
||||||
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// From within the Riot Web directory:
|
// From within the Element Web directory:
|
||||||
// go run github.com/mjibson/esc -o /path/to/dendrite/internal/embed/fs_riotweb.go -private -pkg embed .
|
// go run github.com/mjibson/esc -o /path/to/dendrite/internal/embed/fs_elementweb.go -private -pkg embed .
|
||||||
|
|
||||||
var cssFile = regexp.MustCompile("\\.css$")
|
var cssFile = regexp.MustCompile("\\.css$")
|
||||||
var jsFile = regexp.MustCompile("\\.js$")
|
var jsFile = regexp.MustCompile("\\.js$")
|
||||||
|
|
@ -68,7 +68,7 @@ func Embed(rootMux *mux.Router, listenPort int, serverName string) {
|
||||||
}
|
}
|
||||||
js, _ := sjson.SetBytes(buf, "default_server_config.m\\.homeserver.base_url", url)
|
js, _ := sjson.SetBytes(buf, "default_server_config.m\\.homeserver.base_url", url)
|
||||||
js, _ = sjson.SetBytes(js, "default_server_config.m\\.homeserver.server_name", serverName)
|
js, _ = sjson.SetBytes(js, "default_server_config.m\\.homeserver.server_name", serverName)
|
||||||
js, _ = sjson.SetBytes(js, "brand", fmt.Sprintf("Riot %s", serverName))
|
js, _ = sjson.SetBytes(js, "brand", fmt.Sprintf("Element %s", serverName))
|
||||||
js, _ = sjson.SetBytes(js, "disable_guests", true)
|
js, _ = sjson.SetBytes(js, "disable_guests", true)
|
||||||
js, _ = sjson.SetBytes(js, "disable_3pid_login", true)
|
js, _ = sjson.SetBytes(js, "disable_3pid_login", true)
|
||||||
js, _ = sjson.DeleteBytes(js, "welcomeUserId")
|
js, _ = sjson.DeleteBytes(js, "welcomeUserId")
|
||||||
|
|
@ -76,7 +76,7 @@ func Embed(rootMux *mux.Router, listenPort int, serverName string) {
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Println("*-------------------------------*")
|
fmt.Println("*-------------------------------*")
|
||||||
fmt.Println("| This build includes Riot Web! |")
|
fmt.Println("| This build includes Element Web! |")
|
||||||
fmt.Println("*-------------------------------*")
|
fmt.Println("*-------------------------------*")
|
||||||
fmt.Println("Point your browser to:", url)
|
fmt.Println("Point your browser to:", url)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !riotweb
|
// +build !elementweb
|
||||||
|
|
||||||
package embed
|
package embed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"math"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
|
@ -48,14 +47,15 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"go.uber.org/atomic"
|
|
||||||
|
|
||||||
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
pineconeMulticast "github.com/matrix-org/pinecone/multicast"
|
||||||
|
"github.com/matrix-org/pinecone/router"
|
||||||
pineconeRouter "github.com/matrix-org/pinecone/router"
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
||||||
pineconeTypes "github.com/matrix-org/pinecone/types"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -123,27 +123,23 @@ func main() {
|
||||||
pMulticast := pineconeMulticast.NewMulticast(logger, pRouter)
|
pMulticast := pineconeMulticast.NewMulticast(logger, pRouter)
|
||||||
pMulticast.Start()
|
pMulticast.Start()
|
||||||
|
|
||||||
var staticPeerAttempts atomic.Uint32
|
connectToStaticPeer := func() {
|
||||||
var connectToStaticPeer func()
|
attempt := func() {
|
||||||
connectToStaticPeer = func() {
|
if pRouter.PeerCount(router.PeerTypeRemote) == 0 {
|
||||||
uri := *instancePeer
|
uri := *instancePeer
|
||||||
if uri == "" {
|
if uri == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := conn.ConnectToPeer(pRouter, uri); err != nil {
|
if err := conn.ConnectToPeer(pRouter, uri); err != nil {
|
||||||
exp := time.Second * time.Duration(math.Exp2(float64(staticPeerAttempts.Inc())))
|
logrus.WithError(err).Error("Failed to connect to static peer")
|
||||||
time.AfterFunc(exp, connectToStaticPeer)
|
|
||||||
} else {
|
|
||||||
staticPeerAttempts.Store(0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pRouter.SetDisconnectedCallback(func(port pineconeTypes.SwitchPortID, public pineconeTypes.PublicKey, peertype int, err error) {
|
|
||||||
if peertype == pineconeRouter.PeerTypeRemote && err != nil {
|
|
||||||
staticPeerAttempts.Store(0)
|
|
||||||
time.AfterFunc(time.Second, connectToStaticPeer)
|
|
||||||
}
|
}
|
||||||
})
|
for {
|
||||||
go connectToStaticPeer()
|
attempt()
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults()
|
cfg.Defaults()
|
||||||
|
|
@ -183,7 +179,7 @@ func main() {
|
||||||
base, federation, rsAPI, keyRing, true,
|
base, federation, rsAPI, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, fsAPI)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
|
@ -216,9 +212,14 @@ func main() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
|
|
||||||
wsUpgrader := websocket.Upgrader{}
|
wsUpgrader := websocket.Upgrader{
|
||||||
|
CheckOrigin: func(_ *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
}
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux)
|
||||||
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
httpRouter.PathPrefix(httputil.PublicClientPathPrefix).Handler(base.PublicClientAPIMux)
|
||||||
|
|
@ -257,6 +258,7 @@ func main() {
|
||||||
Handler: pMux,
|
Handler: pMux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
go connectToStaticPeer()
|
||||||
go func() {
|
go func() {
|
||||||
pubkey := pRouter.PublicKey()
|
pubkey := pRouter.PublicKey()
|
||||||
logrus.Info("Listening on ", hex.EncodeToString(pubkey[:]))
|
logrus.Info("Listening on ", hex.EncodeToString(pubkey[:]))
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Yggdrasil Demo
|
# Yggdrasil Demo
|
||||||
|
|
||||||
This is the Dendrite Yggdrasil demo! It's easy to get started - all you need is Go 1.14 or later.
|
This is the Dendrite Yggdrasil demo! It's easy to get started - all you need is Go 1.15 or later.
|
||||||
|
|
||||||
To run the homeserver, start at the root of the Dendrite repository and run:
|
To run the homeserver, start at the root of the Dendrite repository and run:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
// Copyright 2019 Google LLC
|
|
||||||
//
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file or at
|
|
||||||
// https://developers.google.com/open-source/licenses/bsd
|
|
||||||
//
|
|
||||||
// Original code from https://github.com/FiloSottile/age/blob/bbab440e198a4d67ba78591176c7853e62d29e04/internal/age/ssh.go
|
|
||||||
|
|
||||||
package convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/ed25519"
|
|
||||||
"crypto/sha512"
|
|
||||||
"math/big"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/curve25519"
|
|
||||||
)
|
|
||||||
|
|
||||||
var curve25519P, _ = new(big.Int).SetString("57896044618658097711785492504343953926634992332820282019728792003956564819949", 10)
|
|
||||||
|
|
||||||
func Ed25519PrivateKeyToCurve25519(pk ed25519.PrivateKey) []byte {
|
|
||||||
h := sha512.New()
|
|
||||||
_, _ = h.Write(pk.Seed())
|
|
||||||
out := h.Sum(nil)
|
|
||||||
return out[:curve25519.ScalarSize]
|
|
||||||
}
|
|
||||||
|
|
||||||
func Ed25519PublicKeyToCurve25519(pk ed25519.PublicKey) []byte {
|
|
||||||
// ed25519.PublicKey is a little endian representation of the y-coordinate,
|
|
||||||
// with the most significant bit set based on the sign of the x-coordinate.
|
|
||||||
bigEndianY := make([]byte, ed25519.PublicKeySize)
|
|
||||||
for i, b := range pk {
|
|
||||||
bigEndianY[ed25519.PublicKeySize-i-1] = b
|
|
||||||
}
|
|
||||||
bigEndianY[0] &= 0b0111_1111
|
|
||||||
|
|
||||||
// The Montgomery u-coordinate is derived through the bilinear map
|
|
||||||
// u = (1 + y) / (1 - y)
|
|
||||||
// See https://blog.filippo.io/using-ed25519-keys-for-encryption.
|
|
||||||
y := new(big.Int).SetBytes(bigEndianY)
|
|
||||||
denom := big.NewInt(1)
|
|
||||||
denom.ModInverse(denom.Sub(denom, y), curve25519P) // 1 / (1 - y)
|
|
||||||
u := y.Mul(y.Add(y, big.NewInt(1)), denom)
|
|
||||||
u.Mod(u, curve25519P)
|
|
||||||
|
|
||||||
out := make([]byte, curve25519.PointSize)
|
|
||||||
uBytes := u.Bytes()
|
|
||||||
for i, b := range uBytes {
|
|
||||||
out[len(uBytes)-i-1] = b
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
// 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 convert
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/ed25519"
|
|
||||||
"encoding/hex"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/curve25519"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestKeyConversion(t *testing.T) {
|
|
||||||
edPub, edPriv, err := ed25519.GenerateKey(nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Log("Signing public:", hex.EncodeToString(edPub))
|
|
||||||
t.Log("Signing private:", hex.EncodeToString(edPriv))
|
|
||||||
|
|
||||||
cuPriv := Ed25519PrivateKeyToCurve25519(edPriv)
|
|
||||||
t.Log("Encryption private:", hex.EncodeToString(cuPriv))
|
|
||||||
|
|
||||||
cuPub := Ed25519PublicKeyToCurve25519(edPub)
|
|
||||||
t.Log("Converted encryption public:", hex.EncodeToString(cuPub))
|
|
||||||
|
|
||||||
var realPub, realPriv [32]byte
|
|
||||||
copy(realPriv[:32], cuPriv[:32])
|
|
||||||
curve25519.ScalarBaseMult(&realPub, &realPriv)
|
|
||||||
t.Log("Scalar-multed encryption public:", hex.EncodeToString(realPub[:]))
|
|
||||||
|
|
||||||
if !bytes.Equal(realPriv[:], cuPriv[:]) {
|
|
||||||
t.Fatal("Private keys should be equal (this means the test is broken)")
|
|
||||||
}
|
|
||||||
if !bytes.Equal(realPub[:], cuPub[:]) {
|
|
||||||
t.Fatal("Public keys should be equal")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// +build riotweb
|
// +build elementweb
|
||||||
|
|
||||||
package embed
|
package embed
|
||||||
|
|
||||||
|
|
@ -12,8 +12,8 @@ import (
|
||||||
"github.com/tidwall/sjson"
|
"github.com/tidwall/sjson"
|
||||||
)
|
)
|
||||||
|
|
||||||
// From within the Riot Web directory:
|
// From within the Element Web directory:
|
||||||
// go run github.com/mjibson/esc -o /path/to/dendrite/internal/embed/fs_riotweb.go -private -pkg embed .
|
// go run github.com/mjibson/esc -o /path/to/dendrite/internal/embed/fs_elementweb.go -private -pkg embed .
|
||||||
|
|
||||||
var cssFile = regexp.MustCompile("\\.css$")
|
var cssFile = regexp.MustCompile("\\.css$")
|
||||||
var jsFile = regexp.MustCompile("\\.js$")
|
var jsFile = regexp.MustCompile("\\.js$")
|
||||||
|
|
@ -68,16 +68,16 @@ func Embed(rootMux *mux.Router, listenPort int, serverName string) {
|
||||||
}
|
}
|
||||||
js, _ := sjson.SetBytes(buf, "default_server_config.m\\.homeserver.base_url", url)
|
js, _ := sjson.SetBytes(buf, "default_server_config.m\\.homeserver.base_url", url)
|
||||||
js, _ = sjson.SetBytes(js, "default_server_config.m\\.homeserver.server_name", serverName)
|
js, _ = sjson.SetBytes(js, "default_server_config.m\\.homeserver.server_name", serverName)
|
||||||
js, _ = sjson.SetBytes(js, "brand", fmt.Sprintf("Riot %s", serverName))
|
js, _ = sjson.SetBytes(js, "brand", fmt.Sprintf("Element %s", serverName))
|
||||||
js, _ = sjson.SetBytes(js, "disable_guests", true)
|
js, _ = sjson.SetBytes(js, "disable_guests", true)
|
||||||
js, _ = sjson.SetBytes(js, "disable_3pid_login", true)
|
js, _ = sjson.SetBytes(js, "disable_3pid_login", true)
|
||||||
js, _ = sjson.DeleteBytes(js, "welcomeUserId")
|
js, _ = sjson.DeleteBytes(js, "welcomeUserId")
|
||||||
_, _ = w.Write(js)
|
_, _ = w.Write(js)
|
||||||
})
|
})
|
||||||
|
|
||||||
fmt.Println("*-------------------------------*")
|
fmt.Println("*----------------------------------*")
|
||||||
fmt.Println("| This build includes Riot Web! |")
|
fmt.Println("| This build includes Element Web! |")
|
||||||
fmt.Println("*-------------------------------*")
|
fmt.Println("*----------------------------------*")
|
||||||
fmt.Println("Point your browser to:", url)
|
fmt.Println("Point your browser to:", url)
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
// +build !riotweb
|
// +build !elementweb
|
||||||
|
|
||||||
package embed
|
package embed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,8 @@ import (
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -56,21 +58,23 @@ func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
internal.SetupPprof()
|
internal.SetupPprof()
|
||||||
|
|
||||||
ygg, err := yggconn.Setup(*instanceName, ".")
|
ygg, err := yggconn.Setup(*instanceName, ".", *instancePeer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
ygg.SetMulticastEnabled(true)
|
ygg.SetMulticastEnabled(true)
|
||||||
if instancePeer != nil && *instancePeer != "" {
|
if instancePeer != nil && *instancePeer != "" {
|
||||||
if err = ygg.SetStaticPeer(*instancePeer); err != nil {
|
if err = ygg.SetStaticPeer(*instancePeer); err != nil {
|
||||||
logrus.WithError(err).Error("Failed to set static peer")
|
logrus.WithError(err).Error("Failed to set static peer")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
cfg := &config.Dendrite{}
|
cfg := &config.Dendrite{}
|
||||||
cfg.Defaults()
|
cfg.Defaults()
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(ygg.DerivedServerName())
|
||||||
cfg.Global.PrivateKey = ygg.SigningPrivateKey()
|
cfg.Global.PrivateKey = ygg.PrivateKey()
|
||||||
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
cfg.Global.Kafka.UseNaffka = true
|
cfg.Global.Kafka.UseNaffka = true
|
||||||
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-account.db", *instanceName))
|
cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-account.db", *instanceName))
|
||||||
|
|
@ -98,7 +102,7 @@ func main() {
|
||||||
serverKeyAPI := &signing.YggdrasilKeys{}
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
keyRing := serverKeyAPI.KeyRing()
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, federation)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
|
@ -117,18 +121,6 @@ func main() {
|
||||||
base, federation, rsAPI, keyRing, true,
|
base, federation, rsAPI, keyRing, true,
|
||||||
)
|
)
|
||||||
|
|
||||||
ygg.SetSessionFunc(func(address string) {
|
|
||||||
req := &api.PerformServersAliveRequest{
|
|
||||||
Servers: []gomatrixserverlib.ServerName{
|
|
||||||
gomatrixserverlib.ServerName(address),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
res := &api.PerformServersAliveResponse{}
|
|
||||||
if err := fsAPI.PerformServersAlive(context.TODO(), req, res); err != nil {
|
|
||||||
logrus.WithError(err).Error("Failed to send wake-up message to newly connected node")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
rsComponent.SetFederationSenderAPI(fsAPI)
|
rsComponent.SetFederationSenderAPI(fsAPI)
|
||||||
|
|
||||||
monolith := setup.Monolith{
|
monolith := setup.Monolith{
|
||||||
|
|
@ -154,6 +146,7 @@ func main() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
if err := mscs.Enable(base, &monolith); err != nil {
|
if err := mscs.Enable(base, &monolith); err != nil {
|
||||||
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
logrus.WithError(err).Fatalf("Failed to enable MSCs")
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,6 @@ func (n *Node) CreateFederationClient(
|
||||||
ResponseHeaderTimeout: 10 * time.Second,
|
ResponseHeaderTimeout: 10 * time.Second,
|
||||||
IdleConnTimeout: 30 * time.Second,
|
IdleConnTimeout: 30 * time.Second,
|
||||||
DialContext: n.DialerContext,
|
DialContext: n.DialerContext,
|
||||||
TLSClientConfig: n.tlsConfig,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ package yggconn
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/ed25519"
|
"crypto/ed25519"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
@ -26,60 +25,48 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
"github.com/lucas-clemente/quic-go"
|
||||||
"github.com/matrix-org/dendrite/cmd/dendrite-demo-yggdrasil/convert"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"go.uber.org/atomic"
|
"github.com/neilalexander/utp"
|
||||||
|
|
||||||
|
ironwoodtypes "github.com/Arceliar/ironwood/types"
|
||||||
yggdrasilconfig "github.com/yggdrasil-network/yggdrasil-go/src/config"
|
yggdrasilconfig "github.com/yggdrasil-network/yggdrasil-go/src/config"
|
||||||
|
yggdrasilcore "github.com/yggdrasil-network/yggdrasil-go/src/core"
|
||||||
|
yggdrasildefaults "github.com/yggdrasil-network/yggdrasil-go/src/defaults"
|
||||||
yggdrasilmulticast "github.com/yggdrasil-network/yggdrasil-go/src/multicast"
|
yggdrasilmulticast "github.com/yggdrasil-network/yggdrasil-go/src/multicast"
|
||||||
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
|
|
||||||
|
|
||||||
gologme "github.com/gologme/log"
|
gologme "github.com/gologme/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Node struct {
|
type Node struct {
|
||||||
core *yggdrasil.Core
|
core *yggdrasilcore.Core
|
||||||
config *yggdrasilconfig.NodeConfig
|
config *yggdrasilconfig.NodeConfig
|
||||||
state *yggdrasilconfig.NodeState
|
|
||||||
multicast *yggdrasilmulticast.Multicast
|
multicast *yggdrasilmulticast.Multicast
|
||||||
log *gologme.Logger
|
log *gologme.Logger
|
||||||
listener quic.Listener
|
listener quic.Listener
|
||||||
tlsConfig *tls.Config
|
utpSocket *utp.Socket
|
||||||
quicConfig *quic.Config
|
incoming chan net.Conn
|
||||||
sessions sync.Map // string -> *session
|
|
||||||
sessionCount atomic.Uint32
|
|
||||||
sessionFunc func(address string)
|
|
||||||
coords sync.Map // string -> yggdrasil.Coords
|
|
||||||
incoming chan QUICStream
|
|
||||||
NewSession func(remote gomatrixserverlib.ServerName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) Dialer(_, address string) (net.Conn, error) {
|
func (n *Node) DialerContext(ctx context.Context, _, address string) (net.Conn, error) {
|
||||||
tokens := strings.Split(address, ":")
|
tokens := strings.Split(address, ":")
|
||||||
raw, err := hex.DecodeString(tokens[0])
|
raw, err := hex.DecodeString(tokens[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("hex.DecodeString: %w", err)
|
return nil, fmt.Errorf("hex.DecodeString: %w", err)
|
||||||
}
|
}
|
||||||
converted := convert.Ed25519PublicKeyToCurve25519(ed25519.PublicKey(raw))
|
pk := make(ironwoodtypes.Addr, ed25519.PublicKeySize)
|
||||||
convhex := hex.EncodeToString(converted)
|
copy(pk, raw[:])
|
||||||
return n.Dial("curve25519", convhex)
|
return n.utpSocket.DialAddrContext(ctx, pk)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) DialerContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func Setup(instanceName, storageDirectory, peerURI string) (*Node, error) {
|
||||||
return n.Dialer(network, address)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Setup(instanceName, storageDirectory string) (*Node, error) {
|
|
||||||
n := &Node{
|
n := &Node{
|
||||||
core: &yggdrasil.Core{},
|
core: &yggdrasilcore.Core{},
|
||||||
config: yggdrasilconfig.GenerateConfig(),
|
config: yggdrasildefaults.GenerateConfig(),
|
||||||
multicast: &yggdrasilmulticast.Multicast{},
|
multicast: &yggdrasilmulticast.Multicast{},
|
||||||
log: gologme.New(os.Stdout, "YGG ", log.Flags()),
|
log: gologme.New(os.Stdout, "YGG ", log.Flags()),
|
||||||
incoming: make(chan QUICStream),
|
incoming: make(chan net.Conn),
|
||||||
}
|
}
|
||||||
|
|
||||||
yggfile := fmt.Sprintf("%s/%s-yggdrasil.conf", storageDirectory, instanceName)
|
yggfile := fmt.Sprintf("%s/%s-yggdrasil.conf", storageDirectory, instanceName)
|
||||||
|
|
@ -93,24 +80,11 @@ func Setup(instanceName, storageDirectory string) (*Node, error) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
n.core.SetCoordChangeCallback(func(old, new yggdrasil.Coords) {
|
|
||||||
fmt.Println("COORDINATE CHANGE!")
|
|
||||||
fmt.Println("Old:", old)
|
|
||||||
fmt.Println("New:", new)
|
|
||||||
n.sessions.Range(func(k, v interface{}) bool {
|
|
||||||
if s, ok := v.(*session); ok {
|
|
||||||
fmt.Println("Killing session", k)
|
|
||||||
s.kill()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
n.config.Peers = []string{}
|
n.config.Peers = []string{}
|
||||||
|
if peerURI != "" {
|
||||||
|
n.config.Peers = append(n.config.Peers, peerURI)
|
||||||
|
}
|
||||||
n.config.AdminListen = "none"
|
n.config.AdminListen = "none"
|
||||||
n.config.MulticastInterfaces = []string{}
|
|
||||||
n.config.EncryptionPrivateKey = hex.EncodeToString(n.EncryptionPrivateKey())
|
|
||||||
n.config.EncryptionPublicKey = hex.EncodeToString(n.EncryptionPublicKey())
|
|
||||||
|
|
||||||
j, err := json.MarshalIndent(n.config, "", " ")
|
j, err := json.MarshalIndent(n.config, "", " ")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -123,34 +97,22 @@ func Setup(instanceName, storageDirectory string) (*Node, error) {
|
||||||
n.log.EnableLevel("error")
|
n.log.EnableLevel("error")
|
||||||
n.log.EnableLevel("warn")
|
n.log.EnableLevel("warn")
|
||||||
n.log.EnableLevel("info")
|
n.log.EnableLevel("info")
|
||||||
n.state, err = n.core.Start(n.config, n.log)
|
if err = n.core.Start(n.config, n.log); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
n.utpSocket, err = utp.NewSocketFromPacketConnNoClose(n.core)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err = n.multicast.Init(n.core, n.state, n.log, nil); err != nil {
|
if err = n.multicast.Init(n.core, n.config, n.log, nil); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err = n.multicast.Start(); err != nil {
|
if err = n.multicast.Start(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
n.tlsConfig = n.generateTLSConfig()
|
n.log.Println("Public key:", n.core.PublicKey())
|
||||||
n.quicConfig = &quic.Config{
|
go n.listenFromYgg()
|
||||||
MaxIncomingStreams: 0,
|
|
||||||
MaxIncomingUniStreams: 0,
|
|
||||||
KeepAlive: true,
|
|
||||||
MaxIdleTimeout: time.Minute * 30,
|
|
||||||
HandshakeTimeout: time.Second * 15,
|
|
||||||
}
|
|
||||||
copy(n.quicConfig.StatelessResetKey, n.EncryptionPublicKey())
|
|
||||||
|
|
||||||
n.log.Println("Public curve25519:", n.core.EncryptionPublicKey())
|
|
||||||
n.log.Println("Public ed25519:", n.core.SigningPublicKey())
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
time.Sleep(time.Second)
|
|
||||||
n.listenFromYgg()
|
|
||||||
}()
|
|
||||||
|
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
|
|
@ -163,64 +125,33 @@ func (n *Node) Stop() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) DerivedServerName() string {
|
func (n *Node) DerivedServerName() string {
|
||||||
return hex.EncodeToString(n.SigningPublicKey())
|
return hex.EncodeToString(n.PublicKey())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) DerivedSessionName() string {
|
func (n *Node) PrivateKey() ed25519.PrivateKey {
|
||||||
return hex.EncodeToString(n.EncryptionPublicKey())
|
sk := make(ed25519.PrivateKey, ed25519.PrivateKeySize)
|
||||||
|
sb, err := hex.DecodeString(n.config.PrivateKey)
|
||||||
|
if err == nil {
|
||||||
|
copy(sk, sb[:])
|
||||||
|
} else {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sk
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) EncryptionPublicKey() []byte {
|
func (n *Node) PublicKey() ed25519.PublicKey {
|
||||||
edkey := n.SigningPublicKey()
|
return n.core.PublicKey()
|
||||||
return convert.Ed25519PublicKeyToCurve25519(edkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) EncryptionPrivateKey() []byte {
|
|
||||||
edkey := n.SigningPrivateKey()
|
|
||||||
return convert.Ed25519PrivateKeyToCurve25519(edkey)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) SigningPublicKey() ed25519.PublicKey {
|
|
||||||
pubBytes, _ := hex.DecodeString(n.config.SigningPublicKey)
|
|
||||||
return ed25519.PublicKey(pubBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) SigningPrivateKey() ed25519.PrivateKey {
|
|
||||||
privBytes, _ := hex.DecodeString(n.config.SigningPrivateKey)
|
|
||||||
return ed25519.PrivateKey(privBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) SetSessionFunc(f func(address string)) {
|
|
||||||
n.sessionFunc = f
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) PeerCount() int {
|
func (n *Node) PeerCount() int {
|
||||||
return len(n.core.GetPeers()) - 1
|
return len(n.core.GetPeers())
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) SessionCount() int {
|
|
||||||
return int(n.sessionCount.Load())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) KnownNodes() []gomatrixserverlib.ServerName {
|
func (n *Node) KnownNodes() []gomatrixserverlib.ServerName {
|
||||||
nodemap := map[string]struct{}{
|
nodemap := map[string]struct{}{}
|
||||||
//"b5ae50589e50991dd9dd7d59c5c5f7a4521e8da5b603b7f57076272abc58b374": {},
|
for _, peer := range n.core.GetPeers() {
|
||||||
|
nodemap[hex.EncodeToString(peer.Key)] = struct{}{}
|
||||||
}
|
}
|
||||||
for _, peer := range n.core.GetSwitchPeers() {
|
|
||||||
nodemap[hex.EncodeToString(peer.SigPublicKey[:])] = struct{}{}
|
|
||||||
}
|
|
||||||
n.sessions.Range(func(_, v interface{}) bool {
|
|
||||||
session, ok := v.(quic.Session)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(session.ConnectionState().PeerCertificates) != 1 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
subjectName := session.ConnectionState().PeerCertificates[0].Subject.CommonName
|
|
||||||
nodemap[subjectName] = struct{}{}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
var nodes []gomatrixserverlib.ServerName
|
var nodes []gomatrixserverlib.ServerName
|
||||||
for node := range nodemap {
|
for node := range nodemap {
|
||||||
nodes = append(nodes, gomatrixserverlib.ServerName(node))
|
nodes = append(nodes, gomatrixserverlib.ServerName(node))
|
||||||
|
|
@ -229,53 +160,22 @@ func (n *Node) KnownNodes() []gomatrixserverlib.ServerName {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) SetMulticastEnabled(enabled bool) {
|
func (n *Node) SetMulticastEnabled(enabled bool) {
|
||||||
if enabled {
|
// TODO: There's no dynamic reconfiguration in Yggdrasil v0.4
|
||||||
n.config.MulticastInterfaces = []string{".*"}
|
// so we need a solution for this.
|
||||||
} else {
|
|
||||||
n.config.MulticastInterfaces = []string{}
|
|
||||||
}
|
|
||||||
n.multicast.UpdateConfig(n.config)
|
|
||||||
if !enabled {
|
|
||||||
n.DisconnectMulticastPeers()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) DisconnectMulticastPeers() {
|
func (n *Node) DisconnectMulticastPeers() {
|
||||||
for _, sp := range n.core.GetSwitchPeers() {
|
// TODO: There's no dynamic reconfiguration in Yggdrasil v0.4
|
||||||
if !strings.HasPrefix(sp.Endpoint, "fe80") {
|
// so we need a solution for this.
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := n.core.DisconnectPeer(sp.Port); err != nil {
|
|
||||||
n.log.Printf("Failed to disconnect port %d: %s", sp.Port, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) DisconnectNonMulticastPeers() {
|
func (n *Node) DisconnectNonMulticastPeers() {
|
||||||
for _, sp := range n.core.GetSwitchPeers() {
|
// TODO: There's no dynamic reconfiguration in Yggdrasil v0.4
|
||||||
if strings.HasPrefix(sp.Endpoint, "fe80") {
|
// so we need a solution for this.
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := n.core.DisconnectPeer(sp.Port); err != nil {
|
|
||||||
n.log.Printf("Failed to disconnect port %d: %s", sp.Port, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *Node) SetStaticPeer(uri string) error {
|
func (n *Node) SetStaticPeer(uri string) error {
|
||||||
n.config.Peers = []string{}
|
// TODO: There's no dynamic reconfiguration in Yggdrasil v0.4
|
||||||
n.core.UpdateConfig(n.config)
|
// so we need a solution for this.
|
||||||
n.DisconnectNonMulticastPeers()
|
|
||||||
if uri != "" {
|
|
||||||
n.log.Infoln("Adding static peer", uri)
|
|
||||||
if err := n.core.AddPeer(uri, ""); err != nil {
|
|
||||||
n.log.Warnln("Adding static peer failed:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := n.core.CallPeer(uri, ""); err != nil {
|
|
||||||
n.log.Warnln("Calling static peer failed:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,94 +16,17 @@ package yggconn
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"crypto/x509/pkix"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"math/big"
|
|
||||||
"net"
|
"net"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
"github.com/yggdrasil-network/yggdrasil-go/src/crypto"
|
|
||||||
"github.com/yggdrasil-network/yggdrasil-go/src/yggdrasil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type session struct {
|
|
||||||
node *Node
|
|
||||||
session quic.Session
|
|
||||||
address string
|
|
||||||
context context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) newSession(sess quic.Session, address string) *session {
|
|
||||||
ctx, cancel := context.WithCancel(context.TODO())
|
|
||||||
return &session{
|
|
||||||
node: n,
|
|
||||||
session: sess,
|
|
||||||
address: address,
|
|
||||||
context: ctx,
|
|
||||||
cancel: cancel,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) kill() {
|
|
||||||
s.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) listenFromYgg() {
|
func (n *Node) listenFromYgg() {
|
||||||
var err error
|
|
||||||
n.listener, err = quic.Listen(
|
|
||||||
n.core, // yggdrasil.PacketConn
|
|
||||||
n.tlsConfig, // TLS config
|
|
||||||
n.quicConfig, // QUIC config
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for {
|
for {
|
||||||
n.log.Infoln("Waiting to accept QUIC sessions")
|
conn, err := n.utpSocket.Accept()
|
||||||
session, err := n.listener.Accept(context.TODO())
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Println("n.listener.Accept:", err)
|
n.log.Println("n.utpSocket.Accept:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(session.ConnectionState().PeerCertificates) != 1 {
|
n.incoming <- conn
|
||||||
_ = session.CloseWithError(0, "expected a peer certificate")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
address := session.ConnectionState().PeerCertificates[0].DNSNames[0]
|
|
||||||
n.log.Infoln("Accepted connection from", address)
|
|
||||||
go n.newSession(session, address).listenFromQUIC()
|
|
||||||
go n.sessionFunc(address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *session) listenFromQUIC() {
|
|
||||||
if existing, ok := s.node.sessions.Load(s.address); ok {
|
|
||||||
if existingSession, ok := existing.(*session); ok {
|
|
||||||
fmt.Println("Killing existing session to replace", s.address)
|
|
||||||
existingSession.kill()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.node.sessionCount.Inc()
|
|
||||||
s.node.sessions.Store(s.address, s)
|
|
||||||
defer s.node.sessions.Delete(s.address)
|
|
||||||
defer s.node.sessionCount.Dec()
|
|
||||||
for {
|
|
||||||
st, err := s.session.AcceptStream(s.context)
|
|
||||||
if err != nil {
|
|
||||||
s.node.log.Println("session.AcceptStream:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
s.node.incoming <- QUICStream{st, s.session}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,155 +52,5 @@ func (n *Node) Dial(network, address string) (net.Conn, error) {
|
||||||
|
|
||||||
// Implements http.Transport.DialContext
|
// Implements http.Transport.DialContext
|
||||||
func (n *Node) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
func (n *Node) DialContext(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
s, ok1 := n.sessions.Load(address)
|
return n.utpSocket.DialContext(ctx, network, address)
|
||||||
session, ok2 := s.(*session)
|
|
||||||
if !ok1 || !ok2 {
|
|
||||||
// First of all, check if we think we know the coords of this
|
|
||||||
// node. If we do then we'll try to dial to it directly. This
|
|
||||||
// will either succeed or fail.
|
|
||||||
if v, ok := n.coords.Load(address); ok {
|
|
||||||
coords, ok := v.(yggdrasil.Coords)
|
|
||||||
if !ok {
|
|
||||||
n.coords.Delete(address)
|
|
||||||
return nil, errors.New("should have found yggdrasil.Coords but didn't")
|
|
||||||
}
|
|
||||||
n.log.Infof("Coords %s for %q cached, trying to dial", coords.String(), address)
|
|
||||||
var err error
|
|
||||||
// We think we know the coords. Try to dial the node.
|
|
||||||
if session, err = n.tryDial(address, coords); err != nil {
|
|
||||||
// We thought we knew the coords but it didn't result
|
|
||||||
// in a successful dial. Nuke them from the cache.
|
|
||||||
n.coords.Delete(address)
|
|
||||||
n.log.Infof("Cached coords %s for %q failed", coords.String(), address)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We either don't know the coords for the node, or we failed
|
|
||||||
// to dial it before, in which case try to resolve the coords.
|
|
||||||
if _, ok := n.coords.Load(address); !ok {
|
|
||||||
var coords yggdrasil.Coords
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// First look and see if the node is something that we already
|
|
||||||
// know about from our direct switch peers.
|
|
||||||
for _, peer := range n.core.GetSwitchPeers() {
|
|
||||||
if peer.PublicKey.String() == address {
|
|
||||||
coords = peer.Coords
|
|
||||||
n.log.Infof("%q is a direct peer, coords are %s", address, coords.String())
|
|
||||||
n.coords.Store(address, coords)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it isn' a node that we know directly then try to search
|
|
||||||
// the network.
|
|
||||||
if coords == nil {
|
|
||||||
n.log.Infof("Searching for coords for %q", address)
|
|
||||||
dest, derr := hex.DecodeString(address)
|
|
||||||
if derr != nil {
|
|
||||||
return nil, derr
|
|
||||||
}
|
|
||||||
if len(dest) != crypto.BoxPubKeyLen {
|
|
||||||
return nil, errors.New("invalid key length supplied")
|
|
||||||
}
|
|
||||||
var pubKey crypto.BoxPubKey
|
|
||||||
copy(pubKey[:], dest)
|
|
||||||
nodeID := crypto.GetNodeID(&pubKey)
|
|
||||||
nodeMask := &crypto.NodeID{}
|
|
||||||
for i := range nodeMask {
|
|
||||||
nodeMask[i] = 0xFF
|
|
||||||
}
|
|
||||||
|
|
||||||
fmt.Println("Resolving coords")
|
|
||||||
coords, err = n.core.Resolve(nodeID, nodeMask)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("n.core.Resolve: %w", err)
|
|
||||||
}
|
|
||||||
fmt.Println("Found coords:", coords)
|
|
||||||
n.coords.Store(address, coords)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We now know the coords in theory. Let's try dialling the
|
|
||||||
// node again.
|
|
||||||
if session, err = n.tryDial(address, coords); err != nil {
|
|
||||||
return nil, fmt.Errorf("n.tryDial: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if session == nil {
|
|
||||||
return nil, fmt.Errorf("should have found session but didn't")
|
|
||||||
}
|
|
||||||
|
|
||||||
st, err := session.session.OpenStream()
|
|
||||||
if err != nil {
|
|
||||||
n.log.Println("session.OpenStream:", err)
|
|
||||||
_ = session.session.CloseWithError(0, "expected to be able to open session")
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return QUICStream{st, session.session}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) tryDial(address string, coords yggdrasil.Coords) (*session, error) {
|
|
||||||
quicSession, err := quic.Dial(
|
|
||||||
n.core, // yggdrasil.PacketConn
|
|
||||||
coords, // dial address
|
|
||||||
address, // dial SNI
|
|
||||||
n.tlsConfig, // TLS config
|
|
||||||
n.quicConfig, // QUIC config
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(quicSession.ConnectionState().PeerCertificates) != 1 {
|
|
||||||
_ = quicSession.CloseWithError(0, "expected a peer certificate")
|
|
||||||
return nil, errors.New("didn't receive a peer certificate")
|
|
||||||
}
|
|
||||||
if len(quicSession.ConnectionState().PeerCertificates[0].DNSNames) != 1 {
|
|
||||||
_ = quicSession.CloseWithError(0, "expected a DNS name")
|
|
||||||
return nil, errors.New("didn't receive a DNS name")
|
|
||||||
}
|
|
||||||
if gotAddress := quicSession.ConnectionState().PeerCertificates[0].DNSNames[0]; address != gotAddress {
|
|
||||||
_ = quicSession.CloseWithError(0, "you aren't the host I was hoping for")
|
|
||||||
return nil, fmt.Errorf("expected %q but dialled %q", address, gotAddress)
|
|
||||||
}
|
|
||||||
session := n.newSession(quicSession, address)
|
|
||||||
go session.listenFromQUIC()
|
|
||||||
go n.sessionFunc(address)
|
|
||||||
return session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (n *Node) generateTLSConfig() *tls.Config {
|
|
||||||
key, err := rsa.GenerateKey(rand.Reader, 1024)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
template := x509.Certificate{
|
|
||||||
Subject: pkix.Name{
|
|
||||||
CommonName: n.DerivedServerName(),
|
|
||||||
},
|
|
||||||
SerialNumber: big.NewInt(1),
|
|
||||||
NotAfter: time.Now().Add(time.Hour * 24 * 365),
|
|
||||||
DNSNames: []string{n.DerivedSessionName()},
|
|
||||||
}
|
|
||||||
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &key.PublicKey, key)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
|
||||||
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
|
|
||||||
|
|
||||||
tlsCert, err := tls.X509KeyPair(certPEM, keyPEM)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return &tls.Config{
|
|
||||||
Certificates: []tls.Certificate{tlsCert},
|
|
||||||
NextProtos: []string{"quic-matrix-ygg"},
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
ClientAuth: tls.RequireAnyClientCert,
|
|
||||||
GetClientCertificate: func(info *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
|
||||||
return &tlsCert, nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
package yggconn
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
|
|
||||||
"github.com/lucas-clemente/quic-go"
|
|
||||||
)
|
|
||||||
|
|
||||||
type QUICStream struct {
|
|
||||||
quic.Stream
|
|
||||||
session quic.Session
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s QUICStream) LocalAddr() net.Addr {
|
|
||||||
return s.session.LocalAddr()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s QUICStream) RemoteAddr() net.Addr {
|
|
||||||
return s.session.RemoteAddr()
|
|
||||||
}
|
|
||||||
|
|
@ -30,7 +30,10 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/mscs"
|
"github.com/matrix-org/dendrite/setup/mscs"
|
||||||
"github.com/matrix-org/dendrite/signingkeyserver"
|
"github.com/matrix-org/dendrite/signingkeyserver"
|
||||||
"github.com/matrix-org/dendrite/userapi"
|
"github.com/matrix-org/dendrite/userapi"
|
||||||
|
uapi "github.com/matrix-org/dendrite/userapi/api"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -109,9 +112,19 @@ func main() {
|
||||||
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
// This is different to rsAPI which can be the http client which doesn't need this dependency
|
||||||
rsImpl.SetFederationSenderAPI(fsAPI)
|
rsImpl.SetFederationSenderAPI(fsAPI)
|
||||||
|
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, fsAPI)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
if traceInternal {
|
||||||
|
userAPI = &uapi.UserInternalAPITrace{
|
||||||
|
Impl: userAPI,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// needs to be after the SetUserAPI call above
|
||||||
|
if base.UseHTTPAPIs {
|
||||||
|
keyserver.AddInternalRoutes(base.InternalAPIMux, keyAPI)
|
||||||
|
keyAPI = base.KeyServerHTTPClient()
|
||||||
|
}
|
||||||
|
|
||||||
eduInputAPI := eduserver.NewInternalAPI(
|
eduInputAPI := eduserver.NewInternalAPI(
|
||||||
base, cache.New(), userAPI,
|
base, cache.New(), userAPI,
|
||||||
|
|
@ -149,6 +162,7 @@ func main() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(base.Cfg.MSCs.MSCs) > 0 {
|
if len(base.Cfg.MSCs.MSCs) > 0 {
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup"
|
"github.com/matrix-org/dendrite/setup"
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type entrypoint func(base *setup.BaseDendrite, cfg *config.Dendrite)
|
type entrypoint func(base *setup.BaseDendrite, cfg *config.Dendrite)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func ClientAPI(base *setup.BaseDendrite, cfg *config.Dendrite) {
|
||||||
keyAPI := base.KeyServerHTTPClient()
|
keyAPI := base.KeyServerHTTPClient()
|
||||||
|
|
||||||
clientapi.AddPublicRoutes(
|
clientapi.AddPublicRoutes(
|
||||||
base.PublicClientAPIMux, &base.Cfg.ClientAPI, accountDB, federation,
|
base.PublicClientAPIMux, base.SynapseAdminMux, &base.Cfg.ClientAPI, accountDB, federation,
|
||||||
rsAPI, eduInputAPI, asQuery, transactions.New(), fsAPI, userAPI, keyAPI, nil,
|
rsAPI, eduInputAPI, asQuery, transactions.New(), fsAPI, userAPI, keyAPI, nil,
|
||||||
&cfg.MSCs,
|
&cfg.MSCs,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ func FederationAPI(base *setup.BaseDendrite, cfg *config.Dendrite) {
|
||||||
base.PublicFederationAPIMux, base.PublicKeyAPIMux,
|
base.PublicFederationAPIMux, base.PublicKeyAPIMux,
|
||||||
&base.Cfg.FederationAPI, userAPI, federation, keyRing,
|
&base.Cfg.FederationAPI, userAPI, federation, keyRing,
|
||||||
rsAPI, fsAPI, base.EDUServerClient(), keyAPI,
|
rsAPI, fsAPI, base.EDUServerClient(), keyAPI,
|
||||||
&base.Cfg.MSCs,
|
&base.Cfg.MSCs, nil,
|
||||||
)
|
)
|
||||||
|
|
||||||
base.SetupAndServeHTTP(
|
base.SetupAndServeHTTP(
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ import (
|
||||||
|
|
||||||
func KeyServer(base *setup.BaseDendrite, cfg *config.Dendrite) {
|
func KeyServer(base *setup.BaseDendrite, cfg *config.Dendrite) {
|
||||||
fsAPI := base.FederationSenderHTTPClient()
|
fsAPI := base.FederationSenderHTTPClient()
|
||||||
intAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, fsAPI)
|
intAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI)
|
||||||
intAPI.SetUserAPI(base.UserAPIClient())
|
intAPI.SetUserAPI(base.UserAPIClient())
|
||||||
|
|
||||||
keyserver.AddInternalRoutes(base.InternalAPIMux, intAPI)
|
keyserver.AddInternalRoutes(base.InternalAPIMux, intAPI)
|
||||||
|
|
|
||||||
503
cmd/dendrite-upgrade-tests/main.go
Normal file
503
cmd/dendrite-upgrade-tests/main.go
Normal file
|
|
@ -0,0 +1,503 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/codeclysm/extract"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/api/types/mount"
|
||||||
|
"github.com/docker/docker/api/types/volume"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
flagTempDir = flag.String("tmp", "tmp", "Path to temporary directory to dump tarballs to")
|
||||||
|
flagFrom = flag.String("from", "HEAD-1", "The version to start from e.g '0.3.1'. If 'HEAD-N' then starts N versions behind HEAD.")
|
||||||
|
flagTo = flag.String("to", "HEAD", "The version to end on e.g '0.3.3'.")
|
||||||
|
flagBuildConcurrency = flag.Int("build-concurrency", runtime.NumCPU(), "The amount of build concurrency when building images")
|
||||||
|
flagHead = flag.String("head", "", "Location to a dendrite repository to treat as HEAD instead of Github")
|
||||||
|
flagDockerHost = flag.String("docker-host", "localhost", "The hostname of the docker client. 'localhost' if running locally, 'host.docker.internal' if running in Docker.")
|
||||||
|
alphaNumerics = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||||
|
)
|
||||||
|
|
||||||
|
const HEAD = "HEAD"
|
||||||
|
|
||||||
|
// Embed the Dockerfile to use when building dendrite versions.
|
||||||
|
// We cannot use the dockerfile associated with the repo with each version sadly due to changes in
|
||||||
|
// Docker versions. Specifically, earlier Dendrite versions are incompatible with newer Docker clients
|
||||||
|
// due to the error:
|
||||||
|
// When using COPY with more than one source file, the destination must be a directory and end with a /
|
||||||
|
// We need to run a postgres anyway, so use the dockerfile associated with Complement instead.
|
||||||
|
const Dockerfile = `FROM golang:1.13-stretch as build
|
||||||
|
RUN apt-get update && apt-get install -y postgresql
|
||||||
|
WORKDIR /build
|
||||||
|
|
||||||
|
# Copy the build context to the repo as this is the right dendrite code. This is different to the
|
||||||
|
# Complement Dockerfile which wgets a branch.
|
||||||
|
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
|
||||||
|
|
||||||
|
# Replace the connection string with a single postgres DB, using user/db = 'postgres' and no password
|
||||||
|
RUN sed -i "s%connection_string:.*$%connection_string: postgresql://postgres@localhost/postgres?sslmode=disable%g" dendrite.yaml
|
||||||
|
# No password when connecting over localhost
|
||||||
|
RUN sed -i "s%127.0.0.1/32 md5%127.0.0.1/32 trust%g" /etc/postgresql/9.6/main/pg_hba.conf
|
||||||
|
# Bump up max conns for moar concurrency
|
||||||
|
RUN sed -i 's/max_connections = 100/max_connections = 2000/g' /etc/postgresql/9.6/main/postgresql.conf
|
||||||
|
RUN sed -i 's/max_open_conns:.*$/max_open_conns: 100/g' dendrite.yaml
|
||||||
|
|
||||||
|
# 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 9.6 main start \n\
|
||||||
|
\n\
|
||||||
|
until pg_isready \n\
|
||||||
|
do \n\
|
||||||
|
echo "Waiting for postgres"; \n\
|
||||||
|
sleep 1; \n\
|
||||||
|
done \n\
|
||||||
|
\n\
|
||||||
|
sed -i "s/server_name: localhost/server_name: ${SERVER_NAME}/g" dendrite.yaml \n\
|
||||||
|
./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\
|
||||||
|
' > run_dendrite.sh && chmod +x run_dendrite.sh
|
||||||
|
|
||||||
|
ENV SERVER_NAME=localhost
|
||||||
|
EXPOSE 8008 8448
|
||||||
|
CMD /build/run_dendrite.sh `
|
||||||
|
|
||||||
|
const dendriteUpgradeTestLabel = "dendrite_upgrade_test"
|
||||||
|
|
||||||
|
// downloadArchive downloads an arbitrary github archive of the form:
|
||||||
|
// https://github.com/matrix-org/dendrite/archive/v0.3.11.tar.gz
|
||||||
|
// and re-tarballs it without the top-level directory which contains branch information. It inserts
|
||||||
|
// the contents of `dockerfile` as a root file `Dockerfile` in the re-tarballed directory such that
|
||||||
|
// you can directly feed the retarballed archive to `ImageBuild` to have it run said dockerfile.
|
||||||
|
// Returns the tarball buffer on success.
|
||||||
|
func downloadArchive(cli *http.Client, tmpDir, archiveURL string, dockerfile []byte) (*bytes.Buffer, error) {
|
||||||
|
resp, err := cli.Get(archiveURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// nolint:errcheck
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("got HTTP %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
_ = os.RemoveAll(tmpDir)
|
||||||
|
if err = os.Mkdir(tmpDir, os.ModePerm); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to make temporary directory: %s", err)
|
||||||
|
}
|
||||||
|
// nolint:errcheck
|
||||||
|
defer os.RemoveAll(tmpDir)
|
||||||
|
// dump the tarball temporarily, stripping the top-level directory
|
||||||
|
err = extract.Archive(context.Background(), resp.Body, tmpDir, func(inPath string) string {
|
||||||
|
// remove top level
|
||||||
|
segments := strings.Split(inPath, "/")
|
||||||
|
return strings.Join(segments[1:], "/")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// add top level Dockerfile
|
||||||
|
err = ioutil.WriteFile(path.Join(tmpDir, "Dockerfile"), dockerfile, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to inject /Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
// now re-tarball it :/
|
||||||
|
var tarball bytes.Buffer
|
||||||
|
err = compress(tmpDir, &tarball)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &tarball, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildDendrite builds Dendrite on the branchOrTagName given. Returns the image ID or an error
|
||||||
|
func buildDendrite(httpClient *http.Client, dockerClient *client.Client, tmpDir, branchOrTagName string) (string, error) {
|
||||||
|
var tarball *bytes.Buffer
|
||||||
|
var err error
|
||||||
|
// If a custom HEAD location is given, use that, else pull from github. Mostly useful for CI
|
||||||
|
// where we want to use the working directory.
|
||||||
|
if branchOrTagName == HEAD && *flagHead != "" {
|
||||||
|
log.Printf("%s: Using %s as HEAD", branchOrTagName, *flagHead)
|
||||||
|
// add top level Dockerfile
|
||||||
|
err = ioutil.WriteFile(path.Join(*flagHead, "Dockerfile"), []byte(Dockerfile), os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Custom HEAD: failed to inject /Dockerfile: %w", err)
|
||||||
|
}
|
||||||
|
// now tarball it
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
err = compress(*flagHead, &buffer)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to tarball custom HEAD %s : %s", *flagHead, err)
|
||||||
|
}
|
||||||
|
tarball = &buffer
|
||||||
|
} else {
|
||||||
|
log.Printf("%s: Downloading version %s to %s\n", branchOrTagName, branchOrTagName, tmpDir)
|
||||||
|
// pull an archive, this contains a top-level directory which screws with the build context
|
||||||
|
// which we need to fix up post download
|
||||||
|
u := fmt.Sprintf("https://github.com/matrix-org/dendrite/archive/%s.tar.gz", branchOrTagName)
|
||||||
|
tarball, err = downloadArchive(httpClient, tmpDir, u, []byte(Dockerfile))
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to download archive %s: %w", u, err)
|
||||||
|
}
|
||||||
|
log.Printf("%s: %s => %d bytes\n", branchOrTagName, u, tarball.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s: Building version %s\n", branchOrTagName, branchOrTagName)
|
||||||
|
res, err := dockerClient.ImageBuild(context.Background(), tarball, types.ImageBuildOptions{
|
||||||
|
Tags: []string{"dendrite-upgrade"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to start building image: %s", err)
|
||||||
|
}
|
||||||
|
// nolint:errcheck
|
||||||
|
defer res.Body.Close()
|
||||||
|
decoder := json.NewDecoder(res.Body)
|
||||||
|
// {"aux":{"ID":"sha256:247082c717963bc2639fc2daed08838d67811ea12356cd4fda43e1ffef94f2eb"}}
|
||||||
|
var imageID string
|
||||||
|
for decoder.More() {
|
||||||
|
var dl struct {
|
||||||
|
Stream string `json:"stream"`
|
||||||
|
Aux map[string]interface{} `json:"aux"`
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(&dl); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to decode build image output line: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("%s: %s", branchOrTagName, dl.Stream)
|
||||||
|
if dl.Aux != nil {
|
||||||
|
imgID, ok := dl.Aux["ID"]
|
||||||
|
if ok {
|
||||||
|
imageID = imgID.(string)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return imageID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAndSortVersionsFromGithub(httpClient *http.Client) (semVers []*semver.Version, err error) {
|
||||||
|
u := "https://api.github.com/repos/matrix-org/dendrite/tags"
|
||||||
|
res, err := httpClient.Get(u)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return nil, fmt.Errorf("%s returned HTTP %d", u, res.StatusCode)
|
||||||
|
}
|
||||||
|
resp := []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
}{}
|
||||||
|
if err = json.NewDecoder(res.Body).Decode(&resp); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, r := range resp {
|
||||||
|
v, err := semver.NewVersion(r.Name)
|
||||||
|
if err != nil {
|
||||||
|
continue // not a semver, that's ok and isn't an error, we allow tags that aren't semvers
|
||||||
|
}
|
||||||
|
semVers = append(semVers, v)
|
||||||
|
}
|
||||||
|
sort.Sort(semver.Collection(semVers))
|
||||||
|
return semVers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateVersions(cli *http.Client, from, to string) []string {
|
||||||
|
semvers, err := getAndSortVersionsFromGithub(cli)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to collect semvers from github: %s", err)
|
||||||
|
}
|
||||||
|
// snip the lower bound depending on --from
|
||||||
|
if from != "" {
|
||||||
|
if strings.HasPrefix(from, "HEAD-") {
|
||||||
|
var headN int
|
||||||
|
headN, err = strconv.Atoi(strings.TrimPrefix(from, "HEAD-"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid --from, try 'HEAD-1'")
|
||||||
|
}
|
||||||
|
if headN >= len(semvers) {
|
||||||
|
log.Fatalf("only have %d versions, but asked to go to HEAD-%d", len(semvers), headN)
|
||||||
|
}
|
||||||
|
if headN > 0 {
|
||||||
|
semvers = semvers[len(semvers)-headN:]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fromVer, err := semver.NewVersion(from)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid --from: %s", err)
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for i = 0; i < len(semvers); i++ {
|
||||||
|
if semvers[i].LessThan(fromVer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
semvers = semvers[i:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if to != "" && to != HEAD {
|
||||||
|
toVer, err := semver.NewVersion(to)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("invalid --to: %s", err)
|
||||||
|
}
|
||||||
|
var i int
|
||||||
|
for i = len(semvers) - 1; i >= 0; i-- {
|
||||||
|
if semvers[i].GreaterThan(toVer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
semvers = semvers[:i+1]
|
||||||
|
}
|
||||||
|
var versions []string
|
||||||
|
for _, sv := range semvers {
|
||||||
|
versions = append(versions, sv.Original())
|
||||||
|
}
|
||||||
|
if to == HEAD {
|
||||||
|
versions = append(versions, HEAD)
|
||||||
|
}
|
||||||
|
return versions
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDendriteImages(httpClient *http.Client, dockerClient *client.Client, baseTempDir string, concurrency int, branchOrTagNames []string) map[string]string {
|
||||||
|
// concurrently build all versions, this can be done in any order. The mutex protects the map
|
||||||
|
branchToImageID := make(map[string]string)
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(concurrency)
|
||||||
|
ch := make(chan string, len(branchOrTagNames))
|
||||||
|
for _, branchName := range branchOrTagNames {
|
||||||
|
ch <- branchName
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
for branchName := range ch {
|
||||||
|
tmpDir := baseTempDir + alphaNumerics.ReplaceAllString(branchName, "")
|
||||||
|
imgID, err := buildDendrite(httpClient, dockerClient, tmpDir, branchName)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("%s: failed to build dendrite image: %s", branchName, err)
|
||||||
|
}
|
||||||
|
mu.Lock()
|
||||||
|
branchToImageID[branchName] = imgID
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
wg.Wait()
|
||||||
|
return branchToImageID
|
||||||
|
}
|
||||||
|
|
||||||
|
func runImage(dockerClient *client.Client, volumeName, version, imageID string) (csAPIURL, containerID string, err error) {
|
||||||
|
log.Printf("%s: running image %s\n", version, imageID)
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
body, err := dockerClient.ContainerCreate(ctx, &container.Config{
|
||||||
|
Image: imageID,
|
||||||
|
Env: []string{"SERVER_NAME=hs1"},
|
||||||
|
Labels: map[string]string{
|
||||||
|
dendriteUpgradeTestLabel: "yes",
|
||||||
|
},
|
||||||
|
}, &container.HostConfig{
|
||||||
|
PublishAllPorts: true,
|
||||||
|
Mounts: []mount.Mount{
|
||||||
|
{
|
||||||
|
Type: mount.TypeVolume,
|
||||||
|
Source: volumeName,
|
||||||
|
Target: "/var/lib/postgresql/9.6/main",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil, nil, "dendrite_upgrade_test_"+version)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to ContainerCreate: %s", err)
|
||||||
|
}
|
||||||
|
containerID = body.ID
|
||||||
|
|
||||||
|
err = dockerClient.ContainerStart(ctx, containerID, types.ContainerStartOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", fmt.Errorf("failed to ContainerStart: %s", err)
|
||||||
|
}
|
||||||
|
inspect, err := dockerClient.ContainerInspect(ctx, containerID)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
csapiPortInfo, ok := inspect.NetworkSettings.Ports[nat.Port("8008/tcp")]
|
||||||
|
if !ok {
|
||||||
|
return "", "", fmt.Errorf("port 8008 not exposed - exposed ports: %v", inspect.NetworkSettings.Ports)
|
||||||
|
}
|
||||||
|
baseURL := fmt.Sprintf("http://%s:%s", *flagDockerHost, csapiPortInfo[0].HostPort)
|
||||||
|
versionsURL := fmt.Sprintf("%s/_matrix/client/versions", baseURL)
|
||||||
|
// hit /versions to check it is up
|
||||||
|
var lastErr error
|
||||||
|
for i := 0; i < 500; i++ {
|
||||||
|
res, err := http.Get(versionsURL)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = fmt.Errorf("GET %s => error: %s", versionsURL, err)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
lastErr = fmt.Errorf("GET %s => HTTP %s", versionsURL, res.Status)
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
lastErr = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if lastErr != nil {
|
||||||
|
logs, err := dockerClient.ContainerLogs(context.Background(), containerID, types.ContainerLogsOptions{
|
||||||
|
ShowStdout: true,
|
||||||
|
ShowStderr: true,
|
||||||
|
})
|
||||||
|
// ignore errors when cannot get logs, it's just for debugging anyways
|
||||||
|
if err == nil {
|
||||||
|
logbody, err := ioutil.ReadAll(logs)
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Container logs:\n\n%s\n\n", string(logbody))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return baseURL, containerID, lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func destroyContainer(dockerClient *client.Client, containerID string) {
|
||||||
|
err := dockerClient.ContainerRemove(context.TODO(), containerID, types.ContainerRemoveOptions{
|
||||||
|
Force: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to remove container %s : %s", containerID, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAndRunTests(dockerClient *client.Client, volumeName, v string, branchToImageID map[string]string) error {
|
||||||
|
csAPIURL, containerID, err := runImage(dockerClient, volumeName, v, branchToImageID[v])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run container for branch %v: %v", v, err)
|
||||||
|
}
|
||||||
|
defer destroyContainer(dockerClient, containerID)
|
||||||
|
log.Printf("URL %s -> %s \n", csAPIURL, containerID)
|
||||||
|
if err = runTests(csAPIURL, v); err != nil {
|
||||||
|
return fmt.Errorf("failed to run tests on version %s: %s", v, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyTests(dockerClient *client.Client, volumeName string, versions []string, branchToImageID map[string]string) error {
|
||||||
|
lastVer := versions[len(versions)-1]
|
||||||
|
csAPIURL, containerID, err := runImage(dockerClient, volumeName, lastVer, branchToImageID[lastVer])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to run container for branch %v: %v", lastVer, err)
|
||||||
|
}
|
||||||
|
defer destroyContainer(dockerClient, containerID)
|
||||||
|
return verifyTestsRan(csAPIURL, versions)
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanup old containers/volumes from a previous run
|
||||||
|
func cleanup(dockerClient *client.Client) {
|
||||||
|
// ignore all errors, we are just cleaning up and don't want to fail just because we fail to cleanup
|
||||||
|
containers, _ := dockerClient.ContainerList(context.Background(), types.ContainerListOptions{
|
||||||
|
Filters: label(dendriteUpgradeTestLabel),
|
||||||
|
})
|
||||||
|
for _, c := range containers {
|
||||||
|
s := time.Second
|
||||||
|
_ = dockerClient.ContainerStop(context.Background(), c.ID, &s)
|
||||||
|
_ = dockerClient.ContainerRemove(context.Background(), c.ID, types.ContainerRemoveOptions{
|
||||||
|
Force: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
_ = dockerClient.VolumeRemove(context.Background(), "dendrite_upgrade_test", true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func label(in string) filters.Args {
|
||||||
|
f := filters.NewArgs()
|
||||||
|
f.Add("label", in)
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
httpClient := &http.Client{
|
||||||
|
Timeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
dockerClient, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to make docker client: %s", err)
|
||||||
|
}
|
||||||
|
if *flagFrom == "" {
|
||||||
|
flag.Usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
cleanup(dockerClient)
|
||||||
|
versions := calculateVersions(httpClient, *flagFrom, *flagTo)
|
||||||
|
log.Printf("Testing dendrite versions: %v\n", versions)
|
||||||
|
|
||||||
|
branchToImageID := buildDendriteImages(httpClient, dockerClient, *flagTempDir, *flagBuildConcurrency, versions)
|
||||||
|
|
||||||
|
// make a shared postgres volume
|
||||||
|
volume, err := dockerClient.VolumeCreate(context.Background(), volume.VolumeCreateBody{
|
||||||
|
Name: "dendrite_upgrade_test",
|
||||||
|
Labels: map[string]string{
|
||||||
|
dendriteUpgradeTestLabel: "yes",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to make docker volume: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
failed := false
|
||||||
|
defer func() {
|
||||||
|
perr := recover()
|
||||||
|
log.Println("removing postgres volume")
|
||||||
|
verr := dockerClient.VolumeRemove(context.Background(), volume.Name, true)
|
||||||
|
if perr == nil {
|
||||||
|
perr = verr
|
||||||
|
}
|
||||||
|
if perr != nil {
|
||||||
|
panic(perr)
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// run through images sequentially
|
||||||
|
for _, v := range versions {
|
||||||
|
if err = loadAndRunTests(dockerClient, volume.Name, v, branchToImageID); err != nil {
|
||||||
|
log.Printf("failed to run tests for %v: %s\n", v, err)
|
||||||
|
failed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := verifyTests(dockerClient, volume.Name, versions, branchToImageID); err != nil {
|
||||||
|
log.Printf("failed to verify test results: %s", err)
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
63
cmd/dendrite-upgrade-tests/tar.go
Normal file
63
cmd/dendrite-upgrade-tests/tar.go
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// From https://gist.github.com/mimoo/25fc9716e0f1353791f5908f94d6e726
|
||||||
|
// Modified to strip off top-level when compressing
|
||||||
|
func compress(src string, buf io.Writer) error {
|
||||||
|
// tar > gzip > buf
|
||||||
|
zr := gzip.NewWriter(buf)
|
||||||
|
tw := tar.NewWriter(zr)
|
||||||
|
|
||||||
|
// walk through every file in the folder
|
||||||
|
err := filepath.Walk(src, func(file string, fi os.FileInfo, e error) error {
|
||||||
|
// generate tar header
|
||||||
|
header, err := tar.FileInfoHeader(fi, file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// must provide real name
|
||||||
|
// (see https://golang.org/src/archive/tar/common.go?#L626)
|
||||||
|
header.Name = strings.TrimPrefix(filepath.ToSlash(file), src+"/")
|
||||||
|
// write header
|
||||||
|
if err := tw.WriteHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// if not a dir, write file content
|
||||||
|
if !fi.IsDir() {
|
||||||
|
data, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err = io.Copy(tw, data); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = data.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// produce tar
|
||||||
|
if err := tw.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// produce gzip
|
||||||
|
if err := zr.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//
|
||||||
|
return nil
|
||||||
|
}
|
||||||
192
cmd/dendrite-upgrade-tests/tests.go
Normal file
192
cmd/dendrite-upgrade-tests/tests.go
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrix"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userPassword = "this_is_a_long_password"
|
||||||
|
|
||||||
|
type user struct {
|
||||||
|
userID string
|
||||||
|
localpart string
|
||||||
|
client *gomatrix.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// runTests performs the following operations:
|
||||||
|
// - register alice and bob with branch name muxed into the localpart
|
||||||
|
// - create a DM room for the 2 users and exchange messages
|
||||||
|
// - create/join a public #global room and exchange messages
|
||||||
|
func runTests(baseURL, branchName string) error {
|
||||||
|
// register 2 users
|
||||||
|
users := []user{
|
||||||
|
{
|
||||||
|
localpart: "alice" + branchName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
localpart: "bob" + branchName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, u := range users {
|
||||||
|
client, err := gomatrix.NewClient(baseURL, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
resp, err := client.RegisterDummy(&gomatrix.ReqRegister{
|
||||||
|
Username: strings.ToLower(u.localpart),
|
||||||
|
Password: userPassword,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to register %s: %s", u.localpart, err)
|
||||||
|
}
|
||||||
|
client, err = gomatrix.NewClient(baseURL, resp.UserID, resp.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
users[i].client = client
|
||||||
|
users[i].userID = resp.UserID
|
||||||
|
}
|
||||||
|
|
||||||
|
// create DM room, join it and exchange messages
|
||||||
|
createRoomResp, err := users[0].client.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||||
|
Preset: "trusted_private_chat",
|
||||||
|
Invite: []string{users[1].userID},
|
||||||
|
IsDirect: true,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create DM room: %s", err)
|
||||||
|
}
|
||||||
|
dmRoomID := createRoomResp.RoomID
|
||||||
|
if _, err = users[1].client.JoinRoom(dmRoomID, "", nil); err != nil {
|
||||||
|
return fmt.Errorf("failed to join DM room: %s", err)
|
||||||
|
}
|
||||||
|
msgs := []struct {
|
||||||
|
client *gomatrix.Client
|
||||||
|
text string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
client: users[0].client, text: "1: " + branchName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client: users[1].client, text: "2: " + branchName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client: users[0].client, text: "3: " + branchName,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client: users[1].client, text: "4: " + branchName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, msg := range msgs {
|
||||||
|
_, err = msg.client.SendText(dmRoomID, msg.text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send text in dm room: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to create/join the shared public room
|
||||||
|
publicRoomID := ""
|
||||||
|
createRoomResp, err = users[0].client.CreateRoom(&gomatrix.ReqCreateRoom{
|
||||||
|
RoomAliasName: "global",
|
||||||
|
Preset: "public_chat",
|
||||||
|
})
|
||||||
|
if err != nil { // this is okay and expected if the room already exists and the aliases clash
|
||||||
|
// try to join it
|
||||||
|
_, domain, err2 := gomatrixserverlib.SplitID('@', users[0].userID)
|
||||||
|
if err2 != nil {
|
||||||
|
return fmt.Errorf("failed to split user ID: %s, %s", users[0].userID, err2)
|
||||||
|
}
|
||||||
|
joinRoomResp, err2 := users[0].client.JoinRoom(fmt.Sprintf("#global:%s", domain), "", nil)
|
||||||
|
if err2 != nil {
|
||||||
|
return fmt.Errorf("alice failed to join public room: %s", err2)
|
||||||
|
}
|
||||||
|
publicRoomID = joinRoomResp.RoomID
|
||||||
|
} else {
|
||||||
|
publicRoomID = createRoomResp.RoomID
|
||||||
|
}
|
||||||
|
if _, err = users[1].client.JoinRoom(publicRoomID, "", nil); err != nil {
|
||||||
|
return fmt.Errorf("bob failed to join public room: %s", err)
|
||||||
|
}
|
||||||
|
// send messages
|
||||||
|
for _, msg := range msgs {
|
||||||
|
_, err = msg.client.SendText(publicRoomID, "public "+msg.text)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to send text in public room: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("OK! rooms(public=%s, dm=%s) users(%s, %s)\n", publicRoomID, dmRoomID, users[0].userID, users[1].userID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyTestsRan checks that the HS has the right rooms/messages
|
||||||
|
func verifyTestsRan(baseURL string, branchNames []string) error {
|
||||||
|
log.Println("Verifying tests....")
|
||||||
|
// check we can login as all users
|
||||||
|
var resp *gomatrix.RespLogin
|
||||||
|
for _, branchName := range branchNames {
|
||||||
|
client, err := gomatrix.NewClient(baseURL, "", "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
userLocalparts := []string{
|
||||||
|
"alice" + branchName,
|
||||||
|
"bob" + branchName,
|
||||||
|
}
|
||||||
|
for _, userLocalpart := range userLocalparts {
|
||||||
|
resp, err = client.Login(&gomatrix.ReqLogin{
|
||||||
|
Type: "m.login.password",
|
||||||
|
User: strings.ToLower(userLocalpart),
|
||||||
|
Password: userPassword,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to login as %s: %s", userLocalpart, err)
|
||||||
|
}
|
||||||
|
if resp.AccessToken == "" {
|
||||||
|
return fmt.Errorf("failed to login, bad response: %+v", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Println(" accounts exist: OK")
|
||||||
|
client, err := gomatrix.NewClient(baseURL, resp.UserID, resp.AccessToken)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, domain, err := gomatrixserverlib.SplitID('@', client.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
u := client.BuildURL("directory", "room", fmt.Sprintf("#global:%s", domain))
|
||||||
|
r := struct {
|
||||||
|
RoomID string `json:"room_id"`
|
||||||
|
}{}
|
||||||
|
err = client.MakeRequest("GET", u, nil, &r)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to /directory: %s", err)
|
||||||
|
}
|
||||||
|
if r.RoomID == "" {
|
||||||
|
return fmt.Errorf("/directory lookup returned no room ID")
|
||||||
|
}
|
||||||
|
log.Println(" public room exists: OK")
|
||||||
|
|
||||||
|
history, err := client.Messages(r.RoomID, client.Store.LoadNextBatch(client.UserID), "", 'b', 100)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get /messages: %s", err)
|
||||||
|
}
|
||||||
|
// we expect 4 messages per version
|
||||||
|
msgCount := 0
|
||||||
|
for _, ev := range history.Chunk {
|
||||||
|
if ev.Type == "m.room.message" {
|
||||||
|
msgCount += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
wantMsgCount := len(branchNames) * 4
|
||||||
|
if msgCount != wantMsgCount {
|
||||||
|
return fmt.Errorf("got %d messages in global room, want %d", msgCount, wantMsgCount)
|
||||||
|
}
|
||||||
|
log.Println(" messages exist: OK")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
100
cmd/dendritejs-pinecone/jsServer.go
Normal file
100
cmd/dendritejs-pinecone/jsServer.go
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"syscall/js"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSServer exposes an HTTP-like server interface which allows JS to 'send' requests to it.
|
||||||
|
type JSServer struct {
|
||||||
|
// The router which will service requests
|
||||||
|
Mux http.Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnRequestFromJS is the function that JS will invoke when there is a new request.
|
||||||
|
// The JS function signature is:
|
||||||
|
// function(reqString: string): Promise<{result: string, error: string}>
|
||||||
|
// Usage is like:
|
||||||
|
// const res = await global._go_js_server.fetch(reqString);
|
||||||
|
// if (res.error) {
|
||||||
|
// // handle error: this is a 'network' error, not a non-2xx error.
|
||||||
|
// }
|
||||||
|
// const rawHttpResponse = res.result;
|
||||||
|
func (h *JSServer) OnRequestFromJS(this js.Value, args []js.Value) interface{} {
|
||||||
|
// we HAVE to spawn a new goroutine and return immediately or else Go will deadlock
|
||||||
|
// if this request blocks at all e.g for /sync calls
|
||||||
|
httpStr := args[0].String()
|
||||||
|
promise := js.Global().Get("Promise").New(js.FuncOf(func(pthis js.Value, pargs []js.Value) interface{} {
|
||||||
|
// The initial callback code for new Promise() is also called on the critical path, which is why
|
||||||
|
// we need to put this in an immediately invoked goroutine.
|
||||||
|
go func() {
|
||||||
|
resolve := pargs[0]
|
||||||
|
resStr, err := h.handle(httpStr)
|
||||||
|
errStr := ""
|
||||||
|
if err != nil {
|
||||||
|
errStr = err.Error()
|
||||||
|
}
|
||||||
|
resolve.Invoke(map[string]interface{}{
|
||||||
|
"result": resStr,
|
||||||
|
"error": errStr,
|
||||||
|
})
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
return promise
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle invokes the http.ServeMux for this request and returns the raw HTTP response.
|
||||||
|
func (h *JSServer) handle(httpStr string) (resStr string, err error) {
|
||||||
|
req, err := http.ReadRequest(bufio.NewReader(strings.NewReader(httpStr)))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
|
||||||
|
h.Mux.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
res := w.Result()
|
||||||
|
var resBuffer strings.Builder
|
||||||
|
err = res.Write(&resBuffer)
|
||||||
|
return resBuffer.String(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListenAndServe registers a variable in JS-land with the given namespace. This variable is
|
||||||
|
// a function which JS-land can call to 'send' HTTP requests. The function is attached to
|
||||||
|
// a global object called "_go_js_server". See OnRequestFromJS for more info.
|
||||||
|
func (h *JSServer) ListenAndServe(namespace string) {
|
||||||
|
globalName := "_go_js_server"
|
||||||
|
// register a hook in JS-land for it to invoke stuff
|
||||||
|
server := js.Global().Get(globalName)
|
||||||
|
if !server.Truthy() {
|
||||||
|
server = js.Global().Get("Object").New()
|
||||||
|
js.Global().Set(globalName, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
server.Set(namespace, js.FuncOf(h.OnRequestFromJS))
|
||||||
|
|
||||||
|
fmt.Printf("Listening for requests from JS on function %s.%s\n", globalName, namespace)
|
||||||
|
// Block forever to mimic http.ListenAndServe
|
||||||
|
select {}
|
||||||
|
}
|
||||||
261
cmd/dendritejs-pinecone/main.go
Normal file
261
cmd/dendritejs-pinecone/main.go
Normal file
|
|
@ -0,0 +1,261 @@
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ed25519"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"syscall/js"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/matrix-org/dendrite/appservice"
|
||||||
|
"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/eduserver"
|
||||||
|
"github.com/matrix-org/dendrite/eduserver/cache"
|
||||||
|
"github.com/matrix-org/dendrite/federationsender"
|
||||||
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
"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/userapi"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
|
||||||
|
_ "github.com/matrix-org/go-sqlite3-js"
|
||||||
|
|
||||||
|
pineconeRouter "github.com/matrix-org/pinecone/router"
|
||||||
|
pineconeSessions "github.com/matrix-org/pinecone/sessions"
|
||||||
|
)
|
||||||
|
|
||||||
|
var GitCommit string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
fmt.Printf("[%s] dendrite.js starting...\n", GitCommit)
|
||||||
|
}
|
||||||
|
|
||||||
|
const publicPeer = "wss://pinecone.matrix.org/public"
|
||||||
|
const keyNameEd25519 = "_go_ed25519_key"
|
||||||
|
|
||||||
|
func readKeyFromLocalStorage() (key ed25519.PrivateKey, err error) {
|
||||||
|
localforage := js.Global().Get("localforage")
|
||||||
|
if !localforage.Truthy() {
|
||||||
|
err = fmt.Errorf("readKeyFromLocalStorage: no localforage")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// https://localforage.github.io/localForage/
|
||||||
|
item, ok := await(localforage.Call("getItem", keyNameEd25519))
|
||||||
|
if !ok || !item.Truthy() {
|
||||||
|
err = fmt.Errorf("readKeyFromLocalStorage: no key in localforage")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Found key in localforage")
|
||||||
|
// extract []byte and make an ed25519 key
|
||||||
|
seed := make([]byte, 32, 32)
|
||||||
|
js.CopyBytesToGo(seed, item)
|
||||||
|
|
||||||
|
return ed25519.NewKeyFromSeed(seed), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeKeyToLocalStorage(key ed25519.PrivateKey) error {
|
||||||
|
localforage := js.Global().Get("localforage")
|
||||||
|
if !localforage.Truthy() {
|
||||||
|
return fmt.Errorf("writeKeyToLocalStorage: no localforage")
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a Uint8Array from the key's seed
|
||||||
|
seed := key.Seed()
|
||||||
|
jsSeed := js.Global().Get("Uint8Array").New(len(seed))
|
||||||
|
js.CopyBytesToJS(jsSeed, seed)
|
||||||
|
// write it
|
||||||
|
localforage.Call("setItem", keyNameEd25519, jsSeed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// taken from https://go-review.googlesource.com/c/go/+/150917
|
||||||
|
|
||||||
|
// await waits until the promise v has been resolved or rejected and returns the promise's result value.
|
||||||
|
// The boolean value ok is true if the promise has been resolved, false if it has been rejected.
|
||||||
|
// If v is not a promise, v itself is returned as the value and ok is true.
|
||||||
|
func await(v js.Value) (result js.Value, ok bool) {
|
||||||
|
if v.Type() != js.TypeObject || v.Get("then").Type() != js.TypeFunction {
|
||||||
|
return v, true
|
||||||
|
}
|
||||||
|
done := make(chan struct{})
|
||||||
|
onResolve := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
result = args[0]
|
||||||
|
ok = true
|
||||||
|
close(done)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer onResolve.Release()
|
||||||
|
onReject := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||||
|
result = args[0]
|
||||||
|
ok = false
|
||||||
|
close(done)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
defer onReject.Release()
|
||||||
|
v.Call("then", onResolve, onReject)
|
||||||
|
<-done
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateKey() ed25519.PrivateKey {
|
||||||
|
// attempt to look for a seed in JS-land and if it exists use it.
|
||||||
|
priv, err := readKeyFromLocalStorage()
|
||||||
|
if err == nil {
|
||||||
|
fmt.Println("Read key from localStorage")
|
||||||
|
return priv
|
||||||
|
}
|
||||||
|
// generate a new key
|
||||||
|
fmt.Println(err, " : Generating new ed25519 key")
|
||||||
|
_, priv, err = ed25519.GenerateKey(nil)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Fatalf("Failed to generate ed25519 key: %s", err)
|
||||||
|
}
|
||||||
|
if err := writeKeyToLocalStorage(priv); err != nil {
|
||||||
|
fmt.Println("failed to write key to localStorage: ", err)
|
||||||
|
// non-fatal, we'll just have amnesia for a while
|
||||||
|
}
|
||||||
|
return priv
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
startup()
|
||||||
|
|
||||||
|
// We want to block forever to let the fetch and libp2p handler serve the APIs
|
||||||
|
select {}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startup() {
|
||||||
|
sk := generateKey()
|
||||||
|
pk := sk.Public().(ed25519.PublicKey)
|
||||||
|
|
||||||
|
logger := log.New(os.Stdout, "", 0)
|
||||||
|
pRouter := pineconeRouter.NewRouter(logger, "dendrite", sk, pk, nil)
|
||||||
|
pSessions := pineconeSessions.NewSessions(logger, pRouter)
|
||||||
|
|
||||||
|
cfg := &config.Dendrite{}
|
||||||
|
cfg.Defaults()
|
||||||
|
cfg.UserAPI.AccountDatabase.ConnectionString = "file:/idb/dendritejs_account.db"
|
||||||
|
cfg.AppServiceAPI.Database.ConnectionString = "file:/idb/dendritejs_appservice.db"
|
||||||
|
cfg.UserAPI.DeviceDatabase.ConnectionString = "file:/idb/dendritejs_device.db"
|
||||||
|
cfg.FederationSender.Database.ConnectionString = "file:/idb/dendritejs_fedsender.db"
|
||||||
|
cfg.MediaAPI.Database.ConnectionString = "file:/idb/dendritejs_mediaapi.db"
|
||||||
|
cfg.RoomServer.Database.ConnectionString = "file:/idb/dendritejs_roomserver.db"
|
||||||
|
cfg.SigningKeyServer.Database.ConnectionString = "file:/idb/dendritejs_signingkeyserver.db"
|
||||||
|
cfg.SyncAPI.Database.ConnectionString = "file:/idb/dendritejs_syncapi.db"
|
||||||
|
cfg.KeyServer.Database.ConnectionString = "file:/idb/dendritejs_e2ekey.db"
|
||||||
|
cfg.Global.Kafka.UseNaffka = true
|
||||||
|
cfg.Global.Kafka.Database.ConnectionString = "file:/idb/dendritejs_naffka.db"
|
||||||
|
cfg.Global.TrustedIDServers = []string{}
|
||||||
|
cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID)
|
||||||
|
cfg.Global.PrivateKey = sk
|
||||||
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk))
|
||||||
|
|
||||||
|
if err := cfg.Derive(); err != nil {
|
||||||
|
logrus.Fatalf("Failed to derive values from config: %s", err)
|
||||||
|
}
|
||||||
|
base := setup.NewBaseDendrite(cfg, "Monolith", false)
|
||||||
|
defer base.Close() // nolint: errcheck
|
||||||
|
|
||||||
|
accountDB := base.CreateAccountsDB()
|
||||||
|
federation := conn.CreateFederationClient(base, pSessions)
|
||||||
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
||||||
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
serverKeyAPI := &signing.YggdrasilKeys{}
|
||||||
|
keyRing := serverKeyAPI.KeyRing()
|
||||||
|
|
||||||
|
rsAPI := roomserver.NewInternalAPI(base, keyRing)
|
||||||
|
eduInputAPI := eduserver.NewInternalAPI(base, cache.New(), userAPI)
|
||||||
|
asQuery := appservice.NewInternalAPI(
|
||||||
|
base, userAPI, rsAPI,
|
||||||
|
)
|
||||||
|
rsAPI.SetAppserviceAPI(asQuery)
|
||||||
|
fedSenderAPI := federationsender.NewInternalAPI(base, federation, rsAPI, keyRing, true)
|
||||||
|
rsAPI.SetFederationSenderAPI(fedSenderAPI)
|
||||||
|
|
||||||
|
monolith := setup.Monolith{
|
||||||
|
Config: base.Cfg,
|
||||||
|
AccountDB: accountDB,
|
||||||
|
Client: conn.CreateClient(base, pSessions),
|
||||||
|
FedClient: federation,
|
||||||
|
KeyRing: keyRing,
|
||||||
|
|
||||||
|
AppserviceAPI: asQuery,
|
||||||
|
EDUInternalAPI: eduInputAPI,
|
||||||
|
FederationSenderAPI: fedSenderAPI,
|
||||||
|
RoomserverAPI: rsAPI,
|
||||||
|
UserAPI: userAPI,
|
||||||
|
KeyAPI: keyAPI,
|
||||||
|
//ServerKeyAPI: serverKeyAPI,
|
||||||
|
ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, 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)
|
||||||
|
|
||||||
|
p2pRouter := pSessions.HTTP().Mux()
|
||||||
|
p2pRouter.Handle(httputil.PublicFederationPathPrefix, base.PublicFederationAPIMux)
|
||||||
|
p2pRouter.Handle(httputil.PublicMediaPathPrefix, base.PublicMediaAPIMux)
|
||||||
|
|
||||||
|
// Expose the matrix APIs via fetch - for local traffic
|
||||||
|
go func() {
|
||||||
|
logrus.Info("Listening for service-worker fetch traffic")
|
||||||
|
s := JSServer{
|
||||||
|
Mux: httpRouter,
|
||||||
|
}
|
||||||
|
s.ListenAndServe("fetch")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Connect to the static peer
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if pRouter.PeerCount(pineconeRouter.PeerTypeRemote) == 0 {
|
||||||
|
if err := conn.ConnectToPeer(pRouter, publicPeer); err != nil {
|
||||||
|
logrus.WithError(err).Error("Failed to connect to static peer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case <-base.ProcessContext.Context().Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
23
cmd/dendritejs-pinecone/main_noop.go
Normal file
23
cmd/dendritejs-pinecone/main_noop.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
// 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 main
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("dendritejs: no-op when not compiling for WebAssembly")
|
||||||
|
}
|
||||||
25
cmd/dendritejs-pinecone/main_test.go
Normal file
25
cmd/dendritejs-pinecone/main_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// 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.
|
||||||
|
|
||||||
|
// +build wasm
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStartup(t *testing.T) {
|
||||||
|
startup()
|
||||||
|
}
|
||||||
|
|
@ -192,7 +192,7 @@ func main() {
|
||||||
|
|
||||||
accountDB := base.CreateAccountsDB()
|
accountDB := base.CreateAccountsDB()
|
||||||
federation := createFederationClient(cfg, node)
|
federation := createFederationClient(cfg, node)
|
||||||
keyAPI := keyserver.NewInternalAPI(&base.Cfg.KeyServer, federation)
|
keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation)
|
||||||
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
userAPI := userapi.NewInternalAPI(accountDB, &cfg.UserAPI, nil, keyAPI)
|
||||||
keyAPI.SetUserAPI(userAPI)
|
keyAPI.SetUserAPI(userAPI)
|
||||||
|
|
||||||
|
|
@ -236,6 +236,7 @@ func main() {
|
||||||
base.PublicFederationAPIMux,
|
base.PublicFederationAPIMux,
|
||||||
base.PublicKeyAPIMux,
|
base.PublicKeyAPIMux,
|
||||||
base.PublicMediaAPIMux,
|
base.PublicMediaAPIMux,
|
||||||
|
base.SynapseAdminMux,
|
||||||
)
|
)
|
||||||
|
|
||||||
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath()
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
// 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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
sarama "github.com/Shopify/sarama"
|
|
||||||
)
|
|
||||||
|
|
||||||
const usage = `Usage: %s
|
|
||||||
|
|
||||||
Reads a list of newline separated messages from stdin and writes them to a single partition in kafka.
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
|
|
||||||
`
|
|
||||||
|
|
||||||
var (
|
|
||||||
brokerList = flag.String("brokers", os.Getenv("KAFKA_PEERS"), "The comma separated list of brokers in the Kafka cluster. You can also set the KAFKA_PEERS environment variable")
|
|
||||||
topic = flag.String("topic", "", "REQUIRED: the topic to produce to")
|
|
||||||
partition = flag.Int("partition", 0, "The partition to produce to. All the messages will be written to this partition.")
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
flag.Usage = func() {
|
|
||||||
fmt.Fprintf(os.Stderr, usage, os.Args[0])
|
|
||||||
flag.PrintDefaults()
|
|
||||||
}
|
|
||||||
|
|
||||||
flag.Parse()
|
|
||||||
|
|
||||||
if *brokerList == "" {
|
|
||||||
fmt.Fprintln(os.Stderr, "no -brokers specified. Alternatively, set the KAFKA_PEERS environment variable")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if *topic == "" {
|
|
||||||
fmt.Fprintln(os.Stderr, "no -topic specified")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
config := sarama.NewConfig()
|
|
||||||
config.Producer.RequiredAcks = sarama.WaitForAll
|
|
||||||
config.Producer.Return.Successes = true
|
|
||||||
config.Producer.Partitioner = sarama.NewManualPartitioner
|
|
||||||
|
|
||||||
producer, err := sarama.NewSyncProducer(strings.Split(*brokerList, ","), config)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "Failed to open Kafka producer:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := producer.Close(); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "Failed to close Kafka producer cleanly:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
scanner := bufio.NewScanner(os.Stdin)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Bytes()
|
|
||||||
message := &sarama.ProducerMessage{
|
|
||||||
Topic: *topic,
|
|
||||||
Partition: int32(*partition),
|
|
||||||
Value: sarama.ByteEncoder(line),
|
|
||||||
}
|
|
||||||
if _, _, err := producer.SendMessage(message); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "Failed to send message:", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
if err := scanner.Err(); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "reading standard input:", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -1,69 +0,0 @@
|
||||||
# Media API Tests
|
|
||||||
|
|
||||||
## Implemented
|
|
||||||
|
|
||||||
* functional
|
|
||||||
* upload
|
|
||||||
* normal case
|
|
||||||
* download
|
|
||||||
* local file
|
|
||||||
* existing
|
|
||||||
* non-existing
|
|
||||||
* remote file
|
|
||||||
* existing
|
|
||||||
* thumbnail
|
|
||||||
* original file formats
|
|
||||||
* JPEG
|
|
||||||
* local file
|
|
||||||
* existing
|
|
||||||
* remote file
|
|
||||||
* existing
|
|
||||||
* cache
|
|
||||||
* cold
|
|
||||||
* hot
|
|
||||||
* pre-generation according to configuration
|
|
||||||
* scale
|
|
||||||
* crop
|
|
||||||
* dynamic generation
|
|
||||||
* cold cache
|
|
||||||
* larger than original
|
|
||||||
* scale
|
|
||||||
|
|
||||||
## TODO
|
|
||||||
|
|
||||||
* functional
|
|
||||||
* upload
|
|
||||||
* file too large
|
|
||||||
* 0-byte file?
|
|
||||||
* invalid filename
|
|
||||||
* invalid content-type
|
|
||||||
* download
|
|
||||||
* invalid origin
|
|
||||||
* invalid media id
|
|
||||||
* thumbnail
|
|
||||||
* original file formats
|
|
||||||
* GIF
|
|
||||||
* PNG
|
|
||||||
* BMP
|
|
||||||
* SVG
|
|
||||||
* PDF
|
|
||||||
* TIFF
|
|
||||||
* WEBP
|
|
||||||
* local file
|
|
||||||
* non-existing
|
|
||||||
* remote file
|
|
||||||
* non-existing
|
|
||||||
* pre-generation according to configuration
|
|
||||||
* manual verification + hash check for regressions?
|
|
||||||
* dynamic generation
|
|
||||||
* hot cache
|
|
||||||
* limit on dimensions?
|
|
||||||
* 0x0
|
|
||||||
* crop
|
|
||||||
* load
|
|
||||||
* 100 parallel requests
|
|
||||||
* same file
|
|
||||||
* different local files
|
|
||||||
* different remote files
|
|
||||||
* pre-generated thumbnails
|
|
||||||
* non-pre-generated thumbnails
|
|
||||||
|
|
@ -1,269 +0,0 @@
|
||||||
// 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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/test"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
"gopkg.in/yaml.v2"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// How long to wait for the server to write the expected output messages.
|
|
||||||
// This needs to be high enough to account for the time it takes to create
|
|
||||||
// the postgres database tables which can take a while on travis.
|
|
||||||
timeoutString = test.Defaulting(os.Getenv("TIMEOUT"), "10s")
|
|
||||||
// The name of maintenance database to connect to in order to create the test database.
|
|
||||||
postgresDatabase = test.Defaulting(os.Getenv("POSTGRES_DATABASE"), "postgres")
|
|
||||||
// The name of the test database to create.
|
|
||||||
testDatabaseName = test.Defaulting(os.Getenv("DATABASE_NAME"), "mediaapi_test")
|
|
||||||
// Postgres docker container name (for running psql). If not set, psql must be in PATH.
|
|
||||||
postgresContainerName = os.Getenv("POSTGRES_CONTAINER")
|
|
||||||
// Test image to be uploaded/downloaded
|
|
||||||
testJPEG = test.Defaulting(os.Getenv("TEST_JPEG_PATH"), "cmd/mediaapi-integration-tests/totem.jpg")
|
|
||||||
kafkaURI = test.Defaulting(os.Getenv("KAFKA_URIS"), "localhost:9092")
|
|
||||||
)
|
|
||||||
|
|
||||||
var thumbnailSizes = (`
|
|
||||||
- width: 32
|
|
||||||
height: 32
|
|
||||||
method: crop
|
|
||||||
- width: 96
|
|
||||||
height: 96
|
|
||||||
method: crop
|
|
||||||
- width: 320
|
|
||||||
height: 240
|
|
||||||
method: scale
|
|
||||||
- width: 640
|
|
||||||
height: 480
|
|
||||||
method: scale
|
|
||||||
- width: 800
|
|
||||||
height: 600
|
|
||||||
method: scale
|
|
||||||
`)
|
|
||||||
|
|
||||||
const serverType = "media-api"
|
|
||||||
|
|
||||||
const testMediaID = "1VuVy8u_hmDllD8BrcY0deM34Bl7SPJeY9J6BkMmpx0"
|
|
||||||
const testContentType = "image/jpeg"
|
|
||||||
const testOrigin = "localhost:18001"
|
|
||||||
|
|
||||||
var testDatabaseTemplate = "dbname=%s sslmode=disable binary_parameters=yes"
|
|
||||||
|
|
||||||
var timeout time.Duration
|
|
||||||
|
|
||||||
var port = 10000
|
|
||||||
|
|
||||||
func startMediaAPI(suffix string, dynamicThumbnails bool) (*exec.Cmd, chan error, *exec.Cmd, string, string) {
|
|
||||||
dir, err := ioutil.TempDir("", serverType+"-server-test"+suffix)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyAddr := "localhost:1800" + suffix
|
|
||||||
|
|
||||||
database := fmt.Sprintf(testDatabaseTemplate, testDatabaseName+suffix)
|
|
||||||
cfg, nextPort, err := test.MakeConfig(dir, kafkaURI, database, "localhost", port)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
cfg.Global.ServerName = gomatrixserverlib.ServerName(proxyAddr)
|
|
||||||
cfg.MediaAPI.DynamicThumbnails = dynamicThumbnails
|
|
||||||
if err = yaml.Unmarshal([]byte(thumbnailSizes), &cfg.MediaAPI.ThumbnailSizes); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
port = nextPort
|
|
||||||
if err = test.WriteConfig(cfg, dir); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverArgs := []string{
|
|
||||||
"--config", filepath.Join(dir, test.ConfigFile),
|
|
||||||
}
|
|
||||||
|
|
||||||
databases := []string{
|
|
||||||
testDatabaseName + suffix,
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyCmd, _ := test.StartProxy(proxyAddr, cfg)
|
|
||||||
|
|
||||||
test.InitDatabase(
|
|
||||||
postgresDatabase,
|
|
||||||
postgresContainerName,
|
|
||||||
databases,
|
|
||||||
)
|
|
||||||
|
|
||||||
cmd, cmdChan := test.CreateBackgroundCommand(
|
|
||||||
filepath.Join(filepath.Dir(os.Args[0]), "dendrite-"+serverType+"-server"),
|
|
||||||
serverArgs,
|
|
||||||
)
|
|
||||||
|
|
||||||
fmt.Printf("==TESTSERVER== STARTED %v -> %v : %v\n", proxyAddr, cfg.MediaAPI.InternalAPI.Listen, dir)
|
|
||||||
return cmd, cmdChan, proxyCmd, proxyAddr, dir
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanUpServer(cmd *exec.Cmd, dir string) {
|
|
||||||
// ensure server is dead, only cleaning up so don't care about errors this returns
|
|
||||||
cmd.Process.Kill() // nolint: errcheck
|
|
||||||
if err := os.RemoveAll(dir); err != nil {
|
|
||||||
fmt.Printf("WARNING: Failed to remove temporary directory %v: %q\n", dir, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs a battery of media API server tests
|
|
||||||
// The tests will pause at various points in this list to conduct tests on the HTTP responses before continuing.
|
|
||||||
func main() {
|
|
||||||
fmt.Println("==TESTING==", os.Args[0])
|
|
||||||
|
|
||||||
var err error
|
|
||||||
timeout, err = time.ParseDuration(timeoutString)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("ERROR: Invalid timeout string %v: %q\n", timeoutString, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// create server1 with only pre-generated thumbnails allowed
|
|
||||||
server1Cmd, server1CmdChan, server1ProxyCmd, server1ProxyAddr, server1Dir := startMediaAPI("1", false)
|
|
||||||
defer cleanUpServer(server1Cmd, server1Dir)
|
|
||||||
defer server1ProxyCmd.Process.Kill() // nolint: errcheck
|
|
||||||
testDownload(server1ProxyAddr, server1ProxyAddr, "doesnotexist", 404, server1CmdChan)
|
|
||||||
|
|
||||||
// upload a JPEG file
|
|
||||||
testUpload(
|
|
||||||
server1ProxyAddr, testJPEG,
|
|
||||||
)
|
|
||||||
|
|
||||||
// download that JPEG file
|
|
||||||
testDownload(server1ProxyAddr, testOrigin, testMediaID, 200, server1CmdChan)
|
|
||||||
|
|
||||||
// thumbnail that JPEG file
|
|
||||||
testThumbnail(64, 64, "crop", server1ProxyAddr, server1CmdChan)
|
|
||||||
|
|
||||||
// create server2 with dynamic thumbnail generation
|
|
||||||
server2Cmd, server2CmdChan, server2ProxyCmd, server2ProxyAddr, server2Dir := startMediaAPI("2", true)
|
|
||||||
defer cleanUpServer(server2Cmd, server2Dir)
|
|
||||||
defer server2ProxyCmd.Process.Kill() // nolint: errcheck
|
|
||||||
testDownload(server2ProxyAddr, server2ProxyAddr, "doesnotexist", 404, server2CmdChan)
|
|
||||||
|
|
||||||
// pre-generated thumbnail that JPEG file via server2
|
|
||||||
testThumbnail(800, 600, "scale", server2ProxyAddr, server2CmdChan)
|
|
||||||
|
|
||||||
// download that JPEG file via server2
|
|
||||||
testDownload(server2ProxyAddr, testOrigin, testMediaID, 200, server2CmdChan)
|
|
||||||
|
|
||||||
// dynamic thumbnail that JPEG file via server2
|
|
||||||
testThumbnail(1920, 1080, "scale", server2ProxyAddr, server2CmdChan)
|
|
||||||
|
|
||||||
// thumbnail that JPEG file via server2
|
|
||||||
testThumbnail(10000, 10000, "scale", server2ProxyAddr, server2CmdChan)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMediaURI(host, endpoint, query string, components []string) string {
|
|
||||||
pathComponents := []string{host, "_matrix/media/v1", endpoint}
|
|
||||||
pathComponents = append(pathComponents, components...)
|
|
||||||
return "https://" + path.Join(pathComponents...) + query
|
|
||||||
}
|
|
||||||
|
|
||||||
func testUpload(host, filePath string) {
|
|
||||||
fmt.Printf("==TESTING== upload %v to %v\n", filePath, host)
|
|
||||||
file, err := os.Open(filePath)
|
|
||||||
defer file.Close() // nolint: errcheck, staticcheck, megacheck
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
filename := filepath.Base(filePath)
|
|
||||||
stat, err := file.Stat()
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fileSize := stat.Size()
|
|
||||||
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"POST",
|
|
||||||
getMediaURI(host, "upload", "?filename="+filename, nil),
|
|
||||||
file,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
req.ContentLength = fileSize
|
|
||||||
req.Header.Set("Content-Type", testContentType)
|
|
||||||
|
|
||||||
wantedBody := `{"content_uri": "mxc://localhost:18001/` + testMediaID + `"}`
|
|
||||||
testReq := &test.Request{
|
|
||||||
Req: req,
|
|
||||||
WantedStatusCode: 200,
|
|
||||||
WantedBody: test.CanonicalJSONInput([]string{wantedBody})[0],
|
|
||||||
}
|
|
||||||
if err := testReq.Do(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
fmt.Printf("==TESTING== upload %v to %v PASSED\n", filePath, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testDownload(host, origin, mediaID string, wantedStatusCode int, serverCmdChan chan error) {
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"GET",
|
|
||||||
getMediaURI(host, "download", "", []string{
|
|
||||||
origin,
|
|
||||||
mediaID,
|
|
||||||
}),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
testReq := &test.Request{
|
|
||||||
Req: req,
|
|
||||||
WantedStatusCode: wantedStatusCode,
|
|
||||||
WantedBody: "",
|
|
||||||
}
|
|
||||||
testReq.Run(fmt.Sprintf("download mxc://%v/%v from %v", origin, mediaID, host), timeout, serverCmdChan)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testThumbnail(width, height int, resizeMethod, host string, serverCmdChan chan error) {
|
|
||||||
query := fmt.Sprintf("?width=%v&height=%v", width, height)
|
|
||||||
if resizeMethod != "" {
|
|
||||||
query += "&method=" + resizeMethod
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"GET",
|
|
||||||
getMediaURI(host, "thumbnail", query, []string{
|
|
||||||
testOrigin,
|
|
||||||
testMediaID,
|
|
||||||
}),
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
testReq := &test.Request{
|
|
||||||
Req: req,
|
|
||||||
WantedStatusCode: 200,
|
|
||||||
WantedBody: "",
|
|
||||||
}
|
|
||||||
testReq.Run(fmt.Sprintf("thumbnail mxc://%v/%v%v from %v", testOrigin, testMediaID, query, host), timeout, serverCmdChan)
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 MiB |
|
|
@ -1,442 +0,0 @@
|
||||||
// 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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
|
||||||
"github.com/matrix-org/dendrite/internal/test"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/inthttp"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Path to where kafka is installed.
|
|
||||||
kafkaDir = defaulting(os.Getenv("KAFKA_DIR"), "kafka")
|
|
||||||
// The URI the kafka zookeeper is listening on.
|
|
||||||
zookeeperURI = defaulting(os.Getenv("ZOOKEEPER_URI"), "localhost:2181")
|
|
||||||
// The URI the kafka server is listening on.
|
|
||||||
kafkaURI = defaulting(os.Getenv("KAFKA_URIS"), "localhost:9092")
|
|
||||||
// How long to wait for the roomserver to write the expected output messages.
|
|
||||||
// This needs to be high enough to account for the time it takes to create
|
|
||||||
// the postgres database tables which can take a while on travis.
|
|
||||||
timeoutString = defaulting(os.Getenv("TIMEOUT"), "60s")
|
|
||||||
// Timeout for http client
|
|
||||||
timeoutHTTPClient = defaulting(os.Getenv("TIMEOUT_HTTP"), "30s")
|
|
||||||
// The name of maintenance database to connect to in order to create the test database.
|
|
||||||
postgresDatabase = defaulting(os.Getenv("POSTGRES_DATABASE"), "postgres")
|
|
||||||
// The name of the test database to create.
|
|
||||||
testDatabaseName = defaulting(os.Getenv("DATABASE_NAME"), "roomserver_test")
|
|
||||||
// The postgres connection config for connecting to the test database.
|
|
||||||
testDatabase = defaulting(os.Getenv("DATABASE"), fmt.Sprintf("dbname=%s binary_parameters=yes", testDatabaseName))
|
|
||||||
)
|
|
||||||
|
|
||||||
var exe = test.KafkaExecutor{
|
|
||||||
ZookeeperURI: zookeeperURI,
|
|
||||||
KafkaDirectory: kafkaDir,
|
|
||||||
KafkaURI: kafkaURI,
|
|
||||||
// Send stdout and stderr to our stderr so that we see error messages from
|
|
||||||
// the kafka process.
|
|
||||||
OutputWriter: os.Stderr,
|
|
||||||
}
|
|
||||||
|
|
||||||
func defaulting(value, defaultValue string) string {
|
|
||||||
if value == "" {
|
|
||||||
value = defaultValue
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
timeout time.Duration
|
|
||||||
timeoutHTTP time.Duration
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var err error
|
|
||||||
timeout, err = time.ParseDuration(timeoutString)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
timeoutHTTP, err = time.ParseDuration(timeoutHTTPClient)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDatabase(database string) error {
|
|
||||||
cmd := exec.Command("psql", postgresDatabase)
|
|
||||||
cmd.Stdin = strings.NewReader(
|
|
||||||
fmt.Sprintf("DROP DATABASE IF EXISTS %s; CREATE DATABASE %s;", database, database),
|
|
||||||
)
|
|
||||||
// Send stdout and stderr to our stderr so that we see error messages from
|
|
||||||
// the psql process
|
|
||||||
cmd.Stdout = os.Stderr
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// runAndReadFromTopic runs a command and waits for a number of messages to be
|
|
||||||
// written to a kafka topic. It returns if the command exits, the number of
|
|
||||||
// messages is reached or after a timeout. It kills the command before it returns.
|
|
||||||
// It returns a list of the messages read from the command on success or an error
|
|
||||||
// on failure.
|
|
||||||
func runAndReadFromTopic(runCmd *exec.Cmd, readyURL string, doInput func(), topic string, count int, checkQueryAPI func()) ([]string, error) {
|
|
||||||
type result struct {
|
|
||||||
// data holds all of stdout on success.
|
|
||||||
data []byte
|
|
||||||
// err is set on failure.
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
done := make(chan result)
|
|
||||||
readCmd := exec.Command(
|
|
||||||
filepath.Join(kafkaDir, "bin", "kafka-console-consumer.sh"),
|
|
||||||
"--bootstrap-server", kafkaURI,
|
|
||||||
"--topic", topic,
|
|
||||||
"--from-beginning",
|
|
||||||
"--max-messages", fmt.Sprintf("%d", count),
|
|
||||||
)
|
|
||||||
// Send stderr to our stderr so the user can see any error messages.
|
|
||||||
readCmd.Stderr = os.Stderr
|
|
||||||
|
|
||||||
// Kill both processes before we exit.
|
|
||||||
defer func() { runCmd.Process.Kill() }() // nolint: errcheck
|
|
||||||
defer func() { readCmd.Process.Kill() }() // nolint: errcheck
|
|
||||||
|
|
||||||
// Run the command, read the messages and wait for a timeout in parallel.
|
|
||||||
go func() {
|
|
||||||
// Read all of stdout.
|
|
||||||
defer func() {
|
|
||||||
if err := recover(); err != nil {
|
|
||||||
if errv, ok := err.(error); ok {
|
|
||||||
done <- result{nil, errv}
|
|
||||||
} else {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
data, err := readCmd.Output()
|
|
||||||
checkQueryAPI()
|
|
||||||
done <- result{data, err}
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
err := runCmd.Run()
|
|
||||||
done <- result{nil, err}
|
|
||||||
}()
|
|
||||||
go func() {
|
|
||||||
time.Sleep(timeout)
|
|
||||||
done <- result{nil, fmt.Errorf("Timeout reading %d messages from topic %q", count, topic)}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Poll the HTTP listener of the process waiting for it to be ready to receive requests.
|
|
||||||
ready := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
delay := 10 * time.Millisecond
|
|
||||||
for {
|
|
||||||
time.Sleep(delay)
|
|
||||||
if delay < 100*time.Millisecond {
|
|
||||||
delay *= 2
|
|
||||||
}
|
|
||||||
resp, err := http.Get(readyURL)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if resp.StatusCode == 200 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ready <- struct{}{}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// Wait for the roomserver to be ready to receive input or for it to crash.
|
|
||||||
select {
|
|
||||||
case <-ready:
|
|
||||||
case r := <-done:
|
|
||||||
return nil, r.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write the input now that the server is running.
|
|
||||||
doInput()
|
|
||||||
|
|
||||||
// Wait for one of the tasks to finsh.
|
|
||||||
r := <-done
|
|
||||||
|
|
||||||
if r.err != nil {
|
|
||||||
return nil, r.err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The kafka console consumer writes a newline character after each message.
|
|
||||||
// So we split on newline characters
|
|
||||||
lines := strings.Split(string(r.data), "\n")
|
|
||||||
if len(lines) > 0 {
|
|
||||||
// Remove the blank line at the end of the data.
|
|
||||||
lines = lines[:len(lines)-1]
|
|
||||||
}
|
|
||||||
return lines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeToRoomServer(input []string, roomserverURL string) error {
|
|
||||||
var request api.InputRoomEventsRequest
|
|
||||||
var response api.InputRoomEventsResponse
|
|
||||||
var err error
|
|
||||||
request.InputRoomEvents = make([]api.InputRoomEvent, len(input))
|
|
||||||
for i := range input {
|
|
||||||
if err = json.Unmarshal([]byte(input[i]), &request.InputRoomEvents[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
x, err := inthttp.NewRoomserverClient(roomserverURL, &http.Client{Timeout: timeoutHTTP}, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
x.InputRoomEvents(context.Background(), &request, &response)
|
|
||||||
return response.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRoomserver is used to run integration tests against a single roomserver.
|
|
||||||
// It creates new kafka topics for the input and output of the roomserver.
|
|
||||||
// It writes the input messages to the input kafka topic, formatting each message
|
|
||||||
// as canonical JSON so that it fits on a single line.
|
|
||||||
// It then runs the roomserver and waits for a number of messages to be written
|
|
||||||
// to the output topic.
|
|
||||||
// Once those messages have been written it runs the checkQueries function passing
|
|
||||||
// a api.RoomserverQueryAPI client. The caller can use this function to check the
|
|
||||||
// behaviour of the query API.
|
|
||||||
func testRoomserver(input []string, wantOutput []string, checkQueries func(api.RoomserverInternalAPI)) {
|
|
||||||
dir, err := ioutil.TempDir("", "room-server-test")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, _, err := test.MakeConfig(dir, kafkaURI, testDatabase, "localhost", 10000)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err = test.WriteConfig(cfg, dir); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
outputTopic := cfg.Global.Kafka.TopicFor(config.TopicOutputRoomEvent)
|
|
||||||
|
|
||||||
err = exe.DeleteTopic(outputTopic)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = exe.CreateTopic(outputTopic); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = createDatabase(testDatabaseName); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cache, err := caching.NewInMemoryLRUCache(false)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
doInput := func() {
|
|
||||||
fmt.Printf("Roomserver is ready to receive input, sending %d events\n", len(input))
|
|
||||||
if err = writeToRoomServer(input, cfg.RoomServerURL()); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(filepath.Join(filepath.Dir(os.Args[0]), "dendrite-room-server"))
|
|
||||||
|
|
||||||
// Append the roomserver config to the existing environment.
|
|
||||||
// We append to the environment rather than replacing so that any additional
|
|
||||||
// postgres and golang environment variables such as PGHOST are passed to
|
|
||||||
// the roomserver process.
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
cmd.Args = []string{"dendrite-room-server", "--config", filepath.Join(dir, test.ConfigFile)}
|
|
||||||
|
|
||||||
gotOutput, err := runAndReadFromTopic(cmd, cfg.RoomServerURL()+"/metrics", doInput, outputTopic, len(wantOutput), func() {
|
|
||||||
queryAPI, _ := inthttp.NewRoomserverClient("http://"+string(cfg.RoomServer.InternalAPI.Connect), &http.Client{Timeout: timeoutHTTP}, cache)
|
|
||||||
checkQueries(queryAPI)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(wantOutput) != len(gotOutput) {
|
|
||||||
panic(fmt.Errorf("Wanted %d lines of output got %d lines", len(wantOutput), len(gotOutput)))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := range wantOutput {
|
|
||||||
if !equalJSON(wantOutput[i], gotOutput[i]) {
|
|
||||||
panic(fmt.Errorf("Wanted %q at index %d got %q", wantOutput[i], i, gotOutput[i]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func equalJSON(a, b string) bool {
|
|
||||||
canonicalA, err := gomatrixserverlib.CanonicalJSON([]byte(a))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
canonicalB, err := gomatrixserverlib.CanonicalJSON([]byte(b))
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return string(canonicalA) == string(canonicalB)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
fmt.Println("==TESTING==", os.Args[0])
|
|
||||||
|
|
||||||
input := []string{
|
|
||||||
`{
|
|
||||||
"auth_event_ids": [],
|
|
||||||
"kind": 1,
|
|
||||||
"event": {
|
|
||||||
"origin": "matrix.org",
|
|
||||||
"signatures": {
|
|
||||||
"matrix.org": {
|
|
||||||
"ed25519:auto": "3kXGwNtdj+zqEXlI8PWLiB76xtrQ7SxcvPuXAEVCTo+QPoBoUvLi1RkHs6O5mDz7UzIowK5bi1seAN4vOh0OBA"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"origin_server_ts": 1463671337837,
|
|
||||||
"sender": "@richvdh:matrix.org",
|
|
||||||
"event_id": "$1463671337126266wrSBX:matrix.org",
|
|
||||||
"prev_events": [],
|
|
||||||
"state_key": "",
|
|
||||||
"content": {"creator": "@richvdh:matrix.org"},
|
|
||||||
"depth": 1,
|
|
||||||
"prev_state": [],
|
|
||||||
"room_id": "!HCXfdvrfksxuYnIFiJ:matrix.org",
|
|
||||||
"auth_events": [],
|
|
||||||
"hashes": {"sha256": "Q05VLC8nztN2tguy+KnHxxhitI95wK9NelnsDaXRqeo"},
|
|
||||||
"type": "m.room.create"}
|
|
||||||
}`, `{
|
|
||||||
"auth_event_ids": ["$1463671337126266wrSBX:matrix.org"],
|
|
||||||
"kind": 2,
|
|
||||||
"state_event_ids": ["$1463671337126266wrSBX:matrix.org"],
|
|
||||||
"event": {
|
|
||||||
"origin": "matrix.org",
|
|
||||||
"signatures": {
|
|
||||||
"matrix.org": {
|
|
||||||
"ed25519:auto": "a2b3xXYVPPFeG1sHCU3hmZnAaKqZFgzGZozijRGblG5Y//ewRPAn1A2mCrI2UM5I+0zqr70cNpHgF8bmNFu4BA"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"origin_server_ts": 1463671339844,
|
|
||||||
"sender": "@richvdh:matrix.org",
|
|
||||||
"event_id": "$1463671339126270PnVwC:matrix.org",
|
|
||||||
"prev_events": [[
|
|
||||||
"$1463671337126266wrSBX:matrix.org", {"sha256": "h/VS07u8KlMwT3Ee8JhpkC7sa1WUs0Srgs+l3iBv6c0"}
|
|
||||||
]],
|
|
||||||
"membership": "join",
|
|
||||||
"state_key": "@richvdh:matrix.org",
|
|
||||||
"content": {
|
|
||||||
"membership": "join",
|
|
||||||
"avatar_url": "mxc://matrix.org/ZafPzsxMJtLaSaJXloBEKiws",
|
|
||||||
"displayname": "richvdh"
|
|
||||||
},
|
|
||||||
"depth": 2,
|
|
||||||
"prev_state": [],
|
|
||||||
"room_id": "!HCXfdvrfksxuYnIFiJ:matrix.org",
|
|
||||||
"auth_events": [[
|
|
||||||
"$1463671337126266wrSBX:matrix.org", {"sha256": "h/VS07u8KlMwT3Ee8JhpkC7sa1WUs0Srgs+l3iBv6c0"}
|
|
||||||
]],
|
|
||||||
"hashes": {"sha256": "t9t3sZV1Eu0P9Jyrs7pge6UTa1zuTbRdVxeUHnrQVH0"},
|
|
||||||
"type": "m.room.member"},
|
|
||||||
"has_state": true
|
|
||||||
}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
want := []string{
|
|
||||||
`{"type":"new_room_event","new_room_event":{
|
|
||||||
"event":{
|
|
||||||
"auth_events":[[
|
|
||||||
"$1463671337126266wrSBX:matrix.org",{"sha256":"h/VS07u8KlMwT3Ee8JhpkC7sa1WUs0Srgs+l3iBv6c0"}
|
|
||||||
]],
|
|
||||||
"content":{
|
|
||||||
"avatar_url":"mxc://matrix.org/ZafPzsxMJtLaSaJXloBEKiws",
|
|
||||||
"displayname":"richvdh",
|
|
||||||
"membership":"join"
|
|
||||||
},
|
|
||||||
"depth": 2,
|
|
||||||
"event_id": "$1463671339126270PnVwC:matrix.org",
|
|
||||||
"hashes": {"sha256":"t9t3sZV1Eu0P9Jyrs7pge6UTa1zuTbRdVxeUHnrQVH0"},
|
|
||||||
"membership": "join",
|
|
||||||
"origin": "matrix.org",
|
|
||||||
"origin_server_ts": 1463671339844,
|
|
||||||
"prev_events": [[
|
|
||||||
"$1463671337126266wrSBX:matrix.org",{"sha256":"h/VS07u8KlMwT3Ee8JhpkC7sa1WUs0Srgs+l3iBv6c0"}
|
|
||||||
]],
|
|
||||||
"prev_state":[],
|
|
||||||
"room_id":"!HCXfdvrfksxuYnIFiJ:matrix.org",
|
|
||||||
"sender":"@richvdh:matrix.org",
|
|
||||||
"signatures":{
|
|
||||||
"matrix.org":{
|
|
||||||
"ed25519:auto":"a2b3xXYVPPFeG1sHCU3hmZnAaKqZFgzGZozijRGblG5Y//ewRPAn1A2mCrI2UM5I+0zqr70cNpHgF8bmNFu4BA"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"state_key":"@richvdh:matrix.org",
|
|
||||||
"type":"m.room.member"
|
|
||||||
},
|
|
||||||
"state_before_removes_event_ids":["$1463671339126270PnVwC:matrix.org"],
|
|
||||||
"state_before_adds_event_ids":null,
|
|
||||||
"latest_event_ids":["$1463671339126270PnVwC:matrix.org"],
|
|
||||||
"adds_state_event_ids":["$1463671337126266wrSBX:matrix.org", "$1463671339126270PnVwC:matrix.org"],
|
|
||||||
"removes_state_event_ids":null,
|
|
||||||
"last_sent_event_id":"",
|
|
||||||
"send_as_server":"",
|
|
||||||
"transaction_id": null
|
|
||||||
}}`,
|
|
||||||
}
|
|
||||||
|
|
||||||
testRoomserver(input, want, func(q api.RoomserverInternalAPI) {
|
|
||||||
var response api.QueryLatestEventsAndStateResponse
|
|
||||||
if err := q.QueryLatestEventsAndState(
|
|
||||||
context.Background(),
|
|
||||||
&api.QueryLatestEventsAndStateRequest{
|
|
||||||
RoomID: "!HCXfdvrfksxuYnIFiJ:matrix.org",
|
|
||||||
StateToFetch: []gomatrixserverlib.StateKeyTuple{
|
|
||||||
{EventType: "m.room.member", StateKey: "@richvdh:matrix.org"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
&response,
|
|
||||||
); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if !response.RoomExists {
|
|
||||||
panic(fmt.Errorf(`Wanted room "!HCXfdvrfksxuYnIFiJ:matrix.org" to exist`))
|
|
||||||
}
|
|
||||||
if len(response.LatestEvents) != 1 || response.LatestEvents[0].EventID != "$1463671339126270PnVwC:matrix.org" {
|
|
||||||
panic(fmt.Errorf(`Wanted "$1463671339126270PnVwC:matrix.org" to be the latest event got %#v`, response.LatestEvents))
|
|
||||||
}
|
|
||||||
if len(response.StateEvents) != 1 || response.StateEvents[0].EventID() != "$1463671339126270PnVwC:matrix.org" {
|
|
||||||
panic(fmt.Errorf(`Wanted "$1463671339126270PnVwC:matrix.org" to be the state event got %#v`, response.StateEvents))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
fmt.Println("==PASSED==", os.Args[0])
|
|
||||||
}
|
|
||||||
|
|
@ -1,563 +0,0 @@
|
||||||
// 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 main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/ioutil"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/internal/test"
|
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// Path to where kafka is installed.
|
|
||||||
kafkaDir = test.Defaulting(os.Getenv("KAFKA_DIR"), "kafka")
|
|
||||||
// The URI the kafka zookeeper is listening on.
|
|
||||||
zookeeperURI = test.Defaulting(os.Getenv("ZOOKEEPER_URI"), "localhost:2181")
|
|
||||||
// The URI the kafka server is listening on.
|
|
||||||
kafkaURI = test.Defaulting(os.Getenv("KAFKA_URIS"), "localhost:9092")
|
|
||||||
// The address the syncserver should listen on.
|
|
||||||
syncserverAddr = test.Defaulting(os.Getenv("SYNCSERVER_URI"), "localhost:9876")
|
|
||||||
// How long to wait for the syncserver to write the expected output messages.
|
|
||||||
// This needs to be high enough to account for the time it takes to create
|
|
||||||
// the postgres database tables which can take a while on travis.
|
|
||||||
timeoutString = test.Defaulting(os.Getenv("TIMEOUT"), "10s")
|
|
||||||
// The name of maintenance database to connect to in order to create the test database.
|
|
||||||
postgresDatabase = test.Defaulting(os.Getenv("POSTGRES_DATABASE"), "postgres")
|
|
||||||
// Postgres docker container name (for running psql). If not set, psql must be in PATH.
|
|
||||||
postgresContainerName = os.Getenv("POSTGRES_CONTAINER")
|
|
||||||
// The name of the test database to create.
|
|
||||||
testDatabaseName = test.Defaulting(os.Getenv("DATABASE_NAME"), "syncserver_test")
|
|
||||||
// The postgres connection config for connecting to the test database.
|
|
||||||
testDatabase = test.Defaulting(os.Getenv("DATABASE"), fmt.Sprintf("dbname=%s sslmode=disable binary_parameters=yes", testDatabaseName))
|
|
||||||
)
|
|
||||||
|
|
||||||
const inputTopic = "syncserverInput"
|
|
||||||
const clientTopic = "clientapiserverOutput"
|
|
||||||
|
|
||||||
var exe = test.KafkaExecutor{
|
|
||||||
ZookeeperURI: zookeeperURI,
|
|
||||||
KafkaDirectory: kafkaDir,
|
|
||||||
KafkaURI: kafkaURI,
|
|
||||||
// Send stdout and stderr to our stderr so that we see error messages from
|
|
||||||
// the kafka process.
|
|
||||||
OutputWriter: os.Stderr,
|
|
||||||
}
|
|
||||||
|
|
||||||
var timeout time.Duration
|
|
||||||
var clientEventTestData []string
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
var err error
|
|
||||||
timeout, err = time.ParseDuration(timeoutString)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, s := range outputRoomEventTestData {
|
|
||||||
clientEventTestData = append(clientEventTestData, clientEventJSONForOutputRoomEvent(s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func createTestUser(database, username, token string) error {
|
|
||||||
cmd := exec.Command(
|
|
||||||
filepath.Join(filepath.Dir(os.Args[0]), "create-account"),
|
|
||||||
"--database", database,
|
|
||||||
"--username", username,
|
|
||||||
"--token", token,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Send stdout and stderr to our stderr so that we see error messages from
|
|
||||||
// the create-account process
|
|
||||||
cmd.Stdout = os.Stderr
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// clientEventJSONForOutputRoomEvent parses the given output room event and extracts the 'Event' JSON. It is
|
|
||||||
// trimmed to the client format and then canonicalised and returned as a string.
|
|
||||||
// Panics if there are any problems.
|
|
||||||
func clientEventJSONForOutputRoomEvent(outputRoomEvent string) string {
|
|
||||||
var out api.OutputEvent
|
|
||||||
if err := json.Unmarshal([]byte(outputRoomEvent), &out); err != nil {
|
|
||||||
panic("failed to unmarshal output room event: " + err.Error())
|
|
||||||
}
|
|
||||||
clientEvs := gomatrixserverlib.ToClientEvents([]*gomatrixserverlib.Event{
|
|
||||||
out.NewRoomEvent.Event.Event,
|
|
||||||
}, gomatrixserverlib.FormatSync)
|
|
||||||
b, err := json.Marshal(clientEvs[0])
|
|
||||||
if err != nil {
|
|
||||||
panic("failed to marshal client event as json: " + err.Error())
|
|
||||||
}
|
|
||||||
jsonBytes, err := gomatrixserverlib.CanonicalJSON(b)
|
|
||||||
if err != nil {
|
|
||||||
panic("failed to turn event json into canonical json: " + err.Error())
|
|
||||||
}
|
|
||||||
return string(jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
// startSyncServer creates the database and config file needed for the sync server to run and
|
|
||||||
// then starts the sync server. The Cmd being executed is returned. A channel is also returned,
|
|
||||||
// which will have any termination errors sent down it, followed immediately by the channel being closed.
|
|
||||||
func startSyncServer() (*exec.Cmd, chan error) {
|
|
||||||
|
|
||||||
dir, err := ioutil.TempDir("", "syncapi-server-test")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg, _, err := test.MakeConfig(dir, kafkaURI, testDatabase, "localhost", 10000)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
// TODO use the address assigned by the config generator rather than clobbering.
|
|
||||||
cfg.Global.ServerName = "localhost"
|
|
||||||
cfg.SyncAPI.InternalAPI.Listen = config.HTTPAddress("http://" + syncserverAddr)
|
|
||||||
cfg.SyncAPI.InternalAPI.Connect = cfg.SyncAPI.InternalAPI.Listen
|
|
||||||
|
|
||||||
if err := test.WriteConfig(cfg, dir); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
serverArgs := []string{
|
|
||||||
"--config", filepath.Join(dir, test.ConfigFile),
|
|
||||||
}
|
|
||||||
|
|
||||||
databases := []string{
|
|
||||||
testDatabaseName,
|
|
||||||
}
|
|
||||||
|
|
||||||
test.InitDatabase(
|
|
||||||
postgresDatabase,
|
|
||||||
postgresContainerName,
|
|
||||||
databases,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := createTestUser(testDatabase, "alice", "@alice:localhost"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := createTestUser(testDatabase, "bob", "@bob:localhost"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := createTestUser(testDatabase, "charlie", "@charlie:localhost"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd, cmdChan := test.CreateBackgroundCommand(
|
|
||||||
filepath.Join(filepath.Dir(os.Args[0]), "dendrite-sync-api-server"),
|
|
||||||
serverArgs,
|
|
||||||
)
|
|
||||||
|
|
||||||
return cmd, cmdChan
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepareKafka creates the topics which will be written to by the tests.
|
|
||||||
func prepareKafka() {
|
|
||||||
err := exe.DeleteTopic(inputTopic)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = exe.CreateTopic(inputTopic); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = exe.DeleteTopic(clientTopic)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = exe.CreateTopic(clientTopic); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSyncServer(syncServerCmdChan chan error, userID, since, want string) {
|
|
||||||
fmt.Printf("==TESTING== testSyncServer(%s,%s)\n", userID, since)
|
|
||||||
sinceQuery := ""
|
|
||||||
if since != "" {
|
|
||||||
sinceQuery = "&since=" + since
|
|
||||||
}
|
|
||||||
req, err := http.NewRequest(
|
|
||||||
"GET",
|
|
||||||
"http://"+syncserverAddr+"/api/_matrix/client/r0/sync?timeout=100&access_token="+userID+sinceQuery,
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
testReq := &test.Request{
|
|
||||||
Req: req,
|
|
||||||
WantedStatusCode: 200,
|
|
||||||
WantedBody: test.CanonicalJSONInput([]string{want})[0],
|
|
||||||
}
|
|
||||||
testReq.Run("sync-api", timeout, syncServerCmdChan)
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeToRoomServerLog(indexes ...int) {
|
|
||||||
var roomEvents []string
|
|
||||||
for _, i := range indexes {
|
|
||||||
roomEvents = append(roomEvents, outputRoomEventTestData[i])
|
|
||||||
}
|
|
||||||
if err := exe.WriteToTopic(inputTopic, test.CanonicalJSONInput(roomEvents)); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Runs a battery of sync server tests against test data in testdata.go
|
|
||||||
// testdata.go has a list of OutputRoomEvents which will be fed into the kafka log which the sync server will consume.
|
|
||||||
// The tests will pause at various points in this list to conduct tests on the /sync responses before continuing.
|
|
||||||
// For ease of understanding, the curl commands used to create the OutputRoomEvents are listed along with each write to kafka.
|
|
||||||
func main() {
|
|
||||||
fmt.Println("==TESTING==", os.Args[0])
|
|
||||||
prepareKafka()
|
|
||||||
cmd, syncServerCmdChan := startSyncServer()
|
|
||||||
// ensure server is dead, only cleaning up so don't care about errors this returns.
|
|
||||||
defer cmd.Process.Kill() // nolint: errcheck
|
|
||||||
|
|
||||||
// $ curl -XPOST -d '{}' "http://localhost:8009/_matrix/client/r0/createRoom?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/1?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world 2"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/2?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world 3"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"name":"Custom Room Name"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
writeToRoomServerLog(
|
|
||||||
i0StateRoomCreate, i1StateAliceJoin, i2StatePowerLevels, i3StateJoinRules, i4StateHistoryVisibility,
|
|
||||||
i5AliceMsg, i6AliceMsg, i7AliceMsg, i8StateAliceRoomName,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Make sure initial sync works TODO: prev_batch
|
|
||||||
testSyncServer(syncServerCmdChan, "@alice:localhost", "", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "9",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"ephemeral": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i0StateRoomCreate]+","+
|
|
||||||
clientEventTestData[i1StateAliceJoin]+","+
|
|
||||||
clientEventTestData[i2StatePowerLevels]+","+
|
|
||||||
clientEventTestData[i3StateJoinRules]+","+
|
|
||||||
clientEventTestData[i4StateHistoryVisibility]+","+
|
|
||||||
clientEventTestData[i5AliceMsg]+","+
|
|
||||||
clientEventTestData[i6AliceMsg]+","+
|
|
||||||
clientEventTestData[i7AliceMsg]+","+
|
|
||||||
clientEventTestData[i8StateAliceRoomName]+`],
|
|
||||||
"limited": true,
|
|
||||||
"prev_batch": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
// Make sure alice's rooms don't leak to bob
|
|
||||||
testSyncServer(syncServerCmdChan, "@bob:localhost", "", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "9",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
// Make sure polling with an up-to-date token returns nothing new
|
|
||||||
testSyncServer(syncServerCmdChan, "@alice:localhost", "9", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "9",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
writeToRoomServerLog(i9StateBobJoin)
|
|
||||||
|
|
||||||
// Make sure alice sees it TODO: prev_batch
|
|
||||||
testSyncServer(syncServerCmdChan, "@alice:localhost", "9", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "10",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"ephemeral": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limited": false,
|
|
||||||
"prev_batch": "",
|
|
||||||
"events": [`+clientEventTestData[i9StateBobJoin]+`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// Make sure bob sees the room AND all the current room state TODO: history visibility
|
|
||||||
testSyncServer(syncServerCmdChan, "@bob:localhost", "9", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "10",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"ephemeral": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i0StateRoomCreate]+","+
|
|
||||||
clientEventTestData[i1StateAliceJoin]+","+
|
|
||||||
clientEventTestData[i2StatePowerLevels]+","+
|
|
||||||
clientEventTestData[i3StateJoinRules]+","+
|
|
||||||
clientEventTestData[i4StateHistoryVisibility]+","+
|
|
||||||
clientEventTestData[i8StateAliceRoomName]+`]
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limited": false,
|
|
||||||
"prev_batch": "",
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i9StateBobJoin]+`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello alice"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/1?access_token=@bob:localhost"
|
|
||||||
writeToRoomServerLog(i10BobMsg)
|
|
||||||
|
|
||||||
// Make sure alice can see everything around the join point for bob TODO: prev_batch
|
|
||||||
testSyncServer(syncServerCmdChan, "@alice:localhost", "7", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "11",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"ephemeral": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"state": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limited": false,
|
|
||||||
"prev_batch": "",
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i7AliceMsg]+","+
|
|
||||||
clientEventTestData[i8StateAliceRoomName]+","+
|
|
||||||
clientEventTestData[i9StateBobJoin]+","+
|
|
||||||
clientEventTestData[i10BobMsg]+`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"name":"A Different Custom Room Name"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello bob"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/2?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"invite"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@bob:localhost"
|
|
||||||
writeToRoomServerLog(i11StateAliceRoomName, i12AliceMsg, i13StateBobInviteCharlie)
|
|
||||||
|
|
||||||
// Make sure charlie sees the invite both with and without a ?since= token
|
|
||||||
// TODO: Invite state should include the invite event and the room name.
|
|
||||||
charlieInviteData := `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "14",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"invite_state": {
|
|
||||||
"events": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"join": {},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`
|
|
||||||
testSyncServer(syncServerCmdChan, "@charlie:localhost", "7", charlieInviteData)
|
|
||||||
testSyncServer(syncServerCmdChan, "@charlie:localhost", "", charlieInviteData)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@charlie:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"not charlie..."}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"why did you kick charlie"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@bob:localhost"
|
|
||||||
writeToRoomServerLog(i14StateCharlieJoin, i15AliceMsg, i16StateAliceKickCharlie, i17BobMsg)
|
|
||||||
|
|
||||||
// Check transitions to leave work
|
|
||||||
testSyncServer(syncServerCmdChan, "@charlie:localhost", "15", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "18",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {},
|
|
||||||
"leave": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"state": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limited": false,
|
|
||||||
"prev_batch": "",
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i15AliceMsg]+","+
|
|
||||||
clientEventTestData[i16StateAliceKickCharlie]+`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// Test joining and leaving the same room in a single /sync request puts the room in the 'leave' section.
|
|
||||||
// TODO: Use an earlier since value to assert that the /sync response doesn't leak messages
|
|
||||||
// from before charlie was joined to the room. Currently it does leak because RecentEvents doesn't
|
|
||||||
// take membership into account.
|
|
||||||
testSyncServer(syncServerCmdChan, "@charlie:localhost", "14", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "18",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {},
|
|
||||||
"leave": {
|
|
||||||
"!PjrbIMW2cIiaYF4t:localhost": {
|
|
||||||
"state": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"timeline": {
|
|
||||||
"limited": false,
|
|
||||||
"prev_batch": "",
|
|
||||||
"events": [`+
|
|
||||||
clientEventTestData[i14StateCharlieJoin]+","+
|
|
||||||
clientEventTestData[i15AliceMsg]+","+
|
|
||||||
clientEventTestData[i16StateAliceKickCharlie]+`]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"name":"No Charlies"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
writeToRoomServerLog(i18StateAliceRoomName)
|
|
||||||
|
|
||||||
// Check that users don't see state changes in rooms after they have left
|
|
||||||
testSyncServer(syncServerCmdChan, "@charlie:localhost", "17", `{
|
|
||||||
"account_data": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"next_batch": "19",
|
|
||||||
"presence": {
|
|
||||||
"events": []
|
|
||||||
},
|
|
||||||
"rooms": {
|
|
||||||
"invite": {},
|
|
||||||
"join": {},
|
|
||||||
"leave": {}
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"whatever"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@bob:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"im alone now"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"invite"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"so alone"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"name":"Everyone welcome"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@charlie:localhost"
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hiiiii"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@charlie:localhost"
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
// 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 main
|
|
||||||
|
|
||||||
// nolint: varcheck, deadcode, unused, megacheck
|
|
||||||
const (
|
|
||||||
i0StateRoomCreate = iota
|
|
||||||
i1StateAliceJoin
|
|
||||||
i2StatePowerLevels
|
|
||||||
i3StateJoinRules
|
|
||||||
i4StateHistoryVisibility
|
|
||||||
i5AliceMsg
|
|
||||||
i6AliceMsg
|
|
||||||
i7AliceMsg
|
|
||||||
i8StateAliceRoomName
|
|
||||||
i9StateBobJoin
|
|
||||||
i10BobMsg
|
|
||||||
i11StateAliceRoomName
|
|
||||||
i12AliceMsg
|
|
||||||
i13StateBobInviteCharlie
|
|
||||||
i14StateCharlieJoin
|
|
||||||
i15AliceMsg
|
|
||||||
i16StateAliceKickCharlie
|
|
||||||
i17BobMsg
|
|
||||||
i18StateAliceRoomName
|
|
||||||
i19BobMsg
|
|
||||||
i20StateBobLeave
|
|
||||||
i21AliceMsg
|
|
||||||
i22StateAliceInviteBob
|
|
||||||
i23StateBobRejectInvite
|
|
||||||
i24AliceMsg
|
|
||||||
i25StateAliceRoomName
|
|
||||||
i26StateCharlieJoin
|
|
||||||
i27CharlieMsg
|
|
||||||
)
|
|
||||||
|
|
||||||
var outputRoomEventTestData = []string{
|
|
||||||
// $ curl -XPOST -d '{}' "http://localhost:8009/_matrix/client/r0/createRoom?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[],"content":{"creator":"@alice:localhost"},"depth":1,"event_id":"$xz0fUB8zNMTGFh1W:localhost","hashes":{"sha256":"KKkpxS8NoH0igBbL3J+nJ39MRlmA7QgW4BGL7Fv4ASI"},"origin":"localhost","origin_server_ts":1494411218382,"prev_events":[],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"uZG5Q/Hs2Z611gFlZPdwomomRJKf70xV2FQV+gLWM1XgzkLDRlRF3cBZc9y3CnHKnV/upTcXs7Op2/GmgD3UBw"}},"state_key":"","type":"m.room.create"},"latest_event_ids":["$xz0fUB8zNMTGFh1W:localhost"],"adds_state_event_ids":["$xz0fUB8zNMTGFh1W:localhost"],"last_sent_event_id":""}}`,
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}]],"content":{"membership":"join"},"depth":2,"event_id":"$QTen1vksfcRTpUCk:localhost","hashes":{"sha256":"tTukc9ab1fJfzgc5EMA/UD3swqfl/ic9Y9Zkt4fJo0Q"},"origin":"localhost","origin_server_ts":1494411218385,"prev_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"OPysDn/wT7yHeALXLTcEgR+iaKjv0p7VPuR/Mzvyg2IMAwPUjSOw8SQZlhSioWRtVPUp9VHbhIhJxQaPUg9yBQ"}},"state_key":"@alice:localhost","type":"m.room.member"},"latest_event_ids":["$QTen1vksfcRTpUCk:localhost"],"adds_state_event_ids":["$QTen1vksfcRTpUCk:localhost"],"last_sent_event_id":"$xz0fUB8zNMTGFh1W:localhost"}}`,
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}]],"content":{"ban":50,"events":{"m.room.avatar":50,"m.room.canonical_alias":50,"m.room.history_visibility":100,"m.room.name":50,"m.room.power_levels":100},"events_default":0,"invite":0,"kick":50,"redact":50,"state_default":50,"users":{"@alice:localhost":100},"users_default":0},"depth":3,"event_id":"$RWsxGlfPHAcijTgu:localhost","hashes":{"sha256":"ueZWiL/Q8bagRQGFktpnYJAJV6V6U3QKcUEmWYeyaaM"},"origin":"localhost","origin_server_ts":1494411218385,"prev_events":[["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"hZwWx3lyW61zMYmqLOxLTlfW2CnbjJQsZPLjZFa97TVG4ISz8CixMPsnVAIu5is29UCmiHyP8RvLecJjbLCtAQ"}},"state_key":"","type":"m.room.power_levels"},"latest_event_ids":["$RWsxGlfPHAcijTgu:localhost"],"adds_state_event_ids":["$RWsxGlfPHAcijTgu:localhost"],"last_sent_event_id":"$QTen1vksfcRTpUCk:localhost"}}`,
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}]],"content":{"join_rule":"public"},"depth":4,"event_id":"$2O2DpHB37CuwwJOe:localhost","hashes":{"sha256":"3P3HxAXI8gc094i020EoV/gissYiMVWv8+JAbrakM4E"},"origin":"localhost","origin_server_ts":1494411218386,"prev_events":[["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"L2yZoBbG/6TNsRHz+UtHY0SK4FgrdAYPR1l7RBWaNFbm+k/7kVhnoGlJ9yptpdLJjPMR2InqKXH8BBxRC83BCg"}},"state_key":"","type":"m.room.join_rules"},"latest_event_ids":["$2O2DpHB37CuwwJOe:localhost"],"adds_state_event_ids":["$2O2DpHB37CuwwJOe:localhost"],"last_sent_event_id":"$RWsxGlfPHAcijTgu:localhost"}}`,
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}]],"content":{"history_visibility":"joined"},"depth":5,"event_id":"$5LRiBskVCROnL5WY:localhost","hashes":{"sha256":"341alVufcKSVKLPr9WsJNTnW33QkBTn9eTfVWbyoa0o"},"origin":"localhost","origin_server_ts":1494411218387,"prev_events":[["$2O2DpHB37CuwwJOe:localhost",{"sha256":"ulaRD63dbCyolLTwvInIQpcrtU2c7ex/BHmhpLXAUoE"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"kRyt68cstwYgK8NtYzf0V5CnAbqUO47ixCCWYzRCi0WNstEwUw4XW1GHc8BllQsXwSj+nNv9g/66zZgG0DtxCA"}},"state_key":"","type":"m.room.history_visibility"},"latest_event_ids":["$5LRiBskVCROnL5WY:localhost"],"adds_state_event_ids":["$5LRiBskVCROnL5WY:localhost"],"last_sent_event_id":"$2O2DpHB37CuwwJOe:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/1?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"hello world","msgtype":"m.text"},"depth":0,"event_id":"$Z8ZJik7ghwzSYTH9:localhost","hashes":{"sha256":"ahN1T5aiSZCzllf0pqNWJkF+x2h2S3kic+40pQ1X6BE"},"origin":"localhost","origin_server_ts":1494411339207,"prev_events":[["$5LRiBskVCROnL5WY:localhost",{"sha256":"3jULNC9b9Q0AhvnDQqpjhbtYwmkioHzPzdTJZvn8vOI"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"ylEpahRwEfGpqk+UCv0IF8YAxmut7w7udgHy3sVDfdJhs/4uJ6EkFEsKLknpXRc1vTIy1etKCBQ63QbCmRC2Bw"}},"type":"m.room.message"},"latest_event_ids":["$Z8ZJik7ghwzSYTH9:localhost"],"last_sent_event_id":"$5LRiBskVCROnL5WY:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world 2"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/2?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"hello world 2","msgtype":"m.text"},"depth":0,"event_id":"$8382Ah682eL4hxjN:localhost","hashes":{"sha256":"hQElDGSYc6KOdylrbMMm3+LlvUiCKo6S9G9n58/qtns"},"origin":"localhost","origin_server_ts":1494411380282,"prev_events":[["$Z8ZJik7ghwzSYTH9:localhost",{"sha256":"FBDwP+2FeqDENe7AEa3iAFAVKl1/IVq43mCH0uPRn90"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"LFXi6jTG7qn9xzi4rhIiHbkLD+4AZ9Yg7UTS2gqm1gt2lXQsgTYH1wE4Fol2fq4lvGlQVpxhtEr2huAYSbT7DA"}},"type":"m.room.message"},"latest_event_ids":["$8382Ah682eL4hxjN:localhost"],"last_sent_event_id":"$Z8ZJik7ghwzSYTH9:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello world 3"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"hello world 3","msgtype":"m.text"},"depth":0,"event_id":"$17SfHsvSeTQthSWF:localhost","hashes":{"sha256":"eS6VFQI0l2U8rA8U17jgSHr9lQ73SNSnlnZu+HD0IjE"},"origin":"localhost","origin_server_ts":1494411396560,"prev_events":[["$8382Ah682eL4hxjN:localhost",{"sha256":"c6I/PUY7WnvxQ+oUEp/w2HEEuD3g8Vq7QwPUOSUjuc8"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"dvu9bSHZmX+yZoEqHioK7YDMtLH9kol0DdFqc5aHsbhZe/fKRZpfJMrlf1iXQdXSCMhikvnboPAXN3guiZCUBQ"}},"type":"m.room.message"},"latest_event_ids":["$17SfHsvSeTQthSWF:localhost"],"last_sent_event_id":"$8382Ah682eL4hxjN:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"name":"Custom Room Name"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"name":"Custom Room Name"},"depth":0,"event_id":"$j7KtuOzM0K15h3Kr:localhost","hashes":{"sha256":"QIKj5Klr50ugll4EjaNUATJmrru4CDp6TvGPv0v15bo"},"origin":"localhost","origin_server_ts":1494411482625,"prev_events":[["$17SfHsvSeTQthSWF:localhost",{"sha256":"iMTefewJ4W5sKQy7osQv4ilJAi7X0NsK791kqEUmYX0"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"WU7lwSWUAk7bsyDnBs128PyXxPZZoD1sN4AiDcvk+W1mDezJbFvWHDWymclxWESlP7TDrFTZEumRWGGCakjyAg"}},"state_key":"","type":"m.room.name"},"latest_event_ids":["$j7KtuOzM0K15h3Kr:localhost"],"adds_state_event_ids":["$j7KtuOzM0K15h3Kr:localhost"],"last_sent_event_id":"$17SfHsvSeTQthSWF:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$2O2DpHB37CuwwJOe:localhost",{"sha256":"ulaRD63dbCyolLTwvInIQpcrtU2c7ex/BHmhpLXAUoE"}]],"content":{"membership":"join"},"depth":0,"event_id":"$wPepDhIla765Odre:localhost","hashes":{"sha256":"KeKqWLvM+LTvyFbwx6y3Y4W5Pj6nBSFUQ6jpkSf1oTE"},"origin":"localhost","origin_server_ts":1494411534290,"prev_events":[["$j7KtuOzM0K15h3Kr:localhost",{"sha256":"oDrWG5/sy1Ea3hYDOSJZRuGKCcjaHQlDYPDn2gB0/L0"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"oVtvjZbWFe+iJhoDvLcQKnFpSYQ94dOodM4gGsx26P6fs2sFJissYwSIqpoxlElCJnmBAgy5iv4JK/5x21R2CQ"}},"state_key":"@bob:localhost","type":"m.room.member"},"latest_event_ids":["$wPepDhIla765Odre:localhost"],"adds_state_event_ids":["$wPepDhIla765Odre:localhost"],"last_sent_event_id":"$j7KtuOzM0K15h3Kr:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello alice"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/1?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"content":{"body":"hello alice","msgtype":"m.text"},"depth":0,"event_id":"$RHNjeYUvXVZfb93t:localhost","hashes":{"sha256":"Ic1QLxTWFrWt1o31DS93ftrNHkunf4O6ubFvdD4ydNI"},"origin":"localhost","origin_server_ts":1494411593196,"prev_events":[["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"8BHHkiThWwiIZbXCegRjIKNVGIa2kqrZW8VuL7nASfJBORhZ9R9p34UsmhsxVwTs/2/dX7M2ogMB28gIGdLQCg"}},"type":"m.room.message"},"latest_event_ids":["$RHNjeYUvXVZfb93t:localhost"],"last_sent_event_id":"$wPepDhIla765Odre:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"name":"A Different Custom Room Name"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"name":"A Different Custom Room Name"},"depth":0,"event_id":"$1xoUuqOFjFFJgwA5:localhost","hashes":{"sha256":"2pNnLhoHxNeSUpqxrd3c0kZUA4I+cdWZgYcJ8V3e2tk"},"origin":"localhost","origin_server_ts":1494411643348,"prev_events":[["$RHNjeYUvXVZfb93t:localhost",{"sha256":"LqFmTIzULgUDSf5xM3REObvnsRGLQliWBUf1hEDT4+w"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"gsY4B6TIBdVvLyFAaXw0xez9N5/Cn/ZaJ4z+j9gJU/ZR8j1t3OYlcVQN6uln9JwEU1k20AsGnIqvOaayd+bfCg"}},"state_key":"","type":"m.room.name"},"latest_event_ids":["$1xoUuqOFjFFJgwA5:localhost"],"adds_state_event_ids":["$1xoUuqOFjFFJgwA5:localhost"],"removes_state_event_ids":["$j7KtuOzM0K15h3Kr:localhost"],"last_sent_event_id":"$RHNjeYUvXVZfb93t:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hello bob"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/2?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"hello bob","msgtype":"m.text"},"depth":0,"event_id":"$4NBTdIwDxq5fDGpv:localhost","hashes":{"sha256":"msCIESAya8kD7nLCopxkEqrgVuGfrlr9YBIADH5czTA"},"origin":"localhost","origin_server_ts":1494411674630,"prev_events":[["$1xoUuqOFjFFJgwA5:localhost",{"sha256":"ZXj+kY6sqQpf5vsNqvCMSvNoXXKDKxRE4R7+gZD9Tkk"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"bZRT3NxVlfBWw1PxSlKlgfnJixG+NI5H9QmUK2AjECg+l887BZJNCvAK0eD27N8e9V+c2glyXWYje2wexP2CBw"}},"type":"m.room.message"},"latest_event_ids":["$4NBTdIwDxq5fDGpv:localhost"],"last_sent_event_id":"$1xoUuqOFjFFJgwA5:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"invite"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"content":{"membership":"invite"},"depth":0,"event_id":"$zzLHVlHIWPrnE7DI:localhost","hashes":{"sha256":"LKk7tnYJAHsyffbi9CzfdP+TU4KQ5g6YTgYGKjJ7NxU"},"origin":"localhost","origin_server_ts":1494411709192,"prev_events":[["$4NBTdIwDxq5fDGpv:localhost",{"sha256":"EpqmxEoJP93Zb2Nt2fS95SJWTqqIutHm/Ne8OHqp6Ps"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"GdUzkC+7YKl1XDi7kYuD39yi2L/+nv+YrecIQHS+0BLDQqnEj+iRXfNBuZfTk6lUBCJCHXZlk7MnEIjvWDlZCg"}},"state_key":"@charlie:localhost","type":"m.room.member"},"latest_event_ids":["$zzLHVlHIWPrnE7DI:localhost"],"adds_state_event_ids":["$zzLHVlHIWPrnE7DI:localhost"],"last_sent_event_id":"$4NBTdIwDxq5fDGpv:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@charlie:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$2O2DpHB37CuwwJOe:localhost",{"sha256":"ulaRD63dbCyolLTwvInIQpcrtU2c7ex/BHmhpLXAUoE"}],["$zzLHVlHIWPrnE7DI:localhost",{"sha256":"Jw28x9W+GoZYw7sEynsi1fcRzqRQiLddolOa/p26PV0"}]],"content":{"membership":"join"},"unsigned":{"prev_content":{"membership":"invite"},"prev_sender":"@bob:localhost","replaces_state":"$zzLHVlHIWPrnE7DI:localhost"},"depth":0,"event_id":"$uJVKyzZi8ZX0kOd9:localhost","hashes":{"sha256":"9ZZs/Cg0ewpBiCB6iFXXYlmW8koFiesCNGFrOLDTolE"},"origin":"localhost","origin_server_ts":1494411745015,"prev_events":[["$zzLHVlHIWPrnE7DI:localhost",{"sha256":"Jw28x9W+GoZYw7sEynsi1fcRzqRQiLddolOa/p26PV0"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@charlie:localhost","signatures":{"localhost":{"ed25519:something":"+TM0gFPM/M3Ji2BjYuTUTgDyCOWlOq8aTMCxLg7EBvS62yPxJ558f13OWWTczUO5aRAt+PvXsMVM/bp8u6c8DQ"}},"state_key":"@charlie:localhost","type":"m.room.member"},"latest_event_ids":["$uJVKyzZi8ZX0kOd9:localhost"],"adds_state_event_ids":["$uJVKyzZi8ZX0kOd9:localhost"],"removes_state_event_ids":["$zzLHVlHIWPrnE7DI:localhost"],"last_sent_event_id":"$zzLHVlHIWPrnE7DI:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"not charlie..."}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"not charlie...","msgtype":"m.text"},"depth":0,"event_id":"$Ixfn5WT9ocWTYxfy:localhost","hashes":{"sha256":"hRChdyMQ3AY4jvrPpI8PEX6Taux83Qo5hdSeHlhPxGo"},"origin":"localhost","origin_server_ts":1494411792737,"prev_events":[["$uJVKyzZi8ZX0kOd9:localhost",{"sha256":"BtesLFnHZOREQCeilFM+xvDU/Wdj+nyHMw7IGTh/9gU"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"LC/Zqwu/XdqjmLdTOp/NQaFaE0niSAGgEpa39gCxsnsqEX80P7P5WDn/Kzx6rjWTnhIszrLsnoycqkXQT0Z4DQ"}},"type":"m.room.message"},"latest_event_ids":["$Ixfn5WT9ocWTYxfy:localhost"],"last_sent_event_id":"$uJVKyzZi8ZX0kOd9:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$uJVKyzZi8ZX0kOd9:localhost",{"sha256":"BtesLFnHZOREQCeilFM+xvDU/Wdj+nyHMw7IGTh/9gU"}]],"content":{"membership":"leave"},"unsigned":{"prev_content":{"membership":"join"},"prev_sender":"@charlie:localhost","replaces_state":"$uJVKyzZi8ZX0kOd9:localhost"},"depth":0,"event_id":"$om1F4AI8tCYlHUSp:localhost","hashes":{"sha256":"7JVI0uCxSUyEqDJ+o36/zUIlIZkXVK/R6wkrZGvQXDE"},"origin":"localhost","origin_server_ts":1494411855278,"prev_events":[["$Ixfn5WT9ocWTYxfy:localhost",{"sha256":"hOoPIDQFvvNqQJzA5ggjoQi4v1BOELnhnmwU4UArDOY"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"3sxoDLUPnKuDJgFgS3C647BbiXrozxhhxrZOlFP3KgJKzBYv/ht+Jd2V2iSZOvsv94wgRBf0A/lEcJRIqeLgDA"}},"state_key":"@charlie:localhost","type":"m.room.member"},"latest_event_ids":["$om1F4AI8tCYlHUSp:localhost"],"adds_state_event_ids":["$om1F4AI8tCYlHUSp:localhost"],"removes_state_event_ids":["$uJVKyzZi8ZX0kOd9:localhost"],"last_sent_event_id":"$Ixfn5WT9ocWTYxfy:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"why did you kick charlie"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"content":{"body":"why did you kick charlie","msgtype":"m.text"},"depth":0,"event_id":"$hgao5gTmr3r9TtK2:localhost","hashes":{"sha256":"Aa2ZCrvwjX5xhvkVqIOFUeEGqrnrQZjjNFiZRybjsPY"},"origin":"localhost","origin_server_ts":1494411912809,"prev_events":[["$om1F4AI8tCYlHUSp:localhost",{"sha256":"yVs+CW7AiJrJOYouL8xPIBrtIHAhnbxaegna8MxeCto"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"sGkpbEXGsvAuCvE3wb5E9H5fjCVKpRdWNt6csj1bCB9Fmg4Rg4mvj3TAJ+91DjO8IPsgSxDKdqqRYF0OtcynBA"}},"type":"m.room.message"},"latest_event_ids":["$hgao5gTmr3r9TtK2:localhost"],"last_sent_event_id":"$om1F4AI8tCYlHUSp:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"name":"No Charlies"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"name":"No Charlies"},"depth":0,"event_id":"$CY4XDoxjbns3a4Pc:localhost","hashes":{"sha256":"chk72pVkp3AGR2FtdC0mORBWS1b9ePnRN4WK3BP0BiI"},"origin":"localhost","origin_server_ts":1494411959114,"prev_events":[["$hgao5gTmr3r9TtK2:localhost",{"sha256":"/4/OG4Q2YalIeBtN76BEPIieBKA/3UFshR9T+WJip4o"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"mapvA3KJYgw5FmzJMhSFa/+JSuNyv2eKAkiGomAeBB7LQ1e9nK9XhW/Fp7a5Z2Sy2ENwHyd3ij7FEGiLOnSIAw"}},"state_key":"","type":"m.room.name"},"latest_event_ids":["$CY4XDoxjbns3a4Pc:localhost"],"adds_state_event_ids":["$CY4XDoxjbns3a4Pc:localhost"],"removes_state_event_ids":["$1xoUuqOFjFFJgwA5:localhost"],"last_sent_event_id":"$hgao5gTmr3r9TtK2:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"whatever"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"content":{"body":"whatever","msgtype":"m.text"},"depth":0,"event_id":"$pl8VBHRPYDmsnDh4:localhost","hashes":{"sha256":"FYqY9+/cepwIxxjfFV3AjOFBXkTlyEI2jep87dUc+SU"},"origin":"localhost","origin_server_ts":1494411988548,"prev_events":[["$CY4XDoxjbns3a4Pc:localhost",{"sha256":"hCoV63fp8eiquVdEefsOqJtLmJhw4wTlRv+wNTS20Ac"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"sQKwRzE59eZyb8rDySo/pVwZXBh0nA5zx+kjEyXglxIQrTre+8Gj3R7Prni+RE3Dq7oWfKYV7QklTLURAaSICQ"}},"type":"m.room.message"},"latest_event_ids":["$pl8VBHRPYDmsnDh4:localhost"],"last_sent_event_id":"$CY4XDoxjbns3a4Pc:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$wPepDhIla765Odre:localhost",{"sha256":"GqUhRiAkRvPrNBDyUxj+emRfK2P8j6iWtvsXDOUltiI"}]],"content":{"membership":"leave"},"depth":0,"event_id":"$acCW4IgnBo8YD3jw:localhost","hashes":{"sha256":"porP+E2yftBGjfS381+WpZeDM9gZHsM3UydlBcRKBLw"},"origin":"localhost","origin_server_ts":1494412037042,"prev_events":[["$pl8VBHRPYDmsnDh4:localhost",{"sha256":"b+qQ380JDFq7quVU9EbIJ2sbpUKM1LAUNX0ZZUoVMZw"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"kxbjTIC0/UR4cOYUAOTNiUc0SSVIF4BY6Rq6IEgYJemq4jcU2fYqum4mFxIQTDKKXMSRHEoNPDmYMFIJwkrsCg"}},"state_key":"@bob:localhost","type":"m.room.member"},"latest_event_ids":["$acCW4IgnBo8YD3jw:localhost"],"adds_state_event_ids":["$acCW4IgnBo8YD3jw:localhost"],"removes_state_event_ids":["$wPepDhIla765Odre:localhost"],"last_sent_event_id":"$pl8VBHRPYDmsnDh4:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"im alone now"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"im alone now","msgtype":"m.text"},"depth":0,"event_id":"$nYdEXrvTDeb7DfkC:localhost","hashes":{"sha256":"qibC5NmlJpSRMBWSWxy1pv73FXymhPDXQFMmGosfsV0"},"origin":"localhost","origin_server_ts":1494412084668,"prev_events":[["$acCW4IgnBo8YD3jw:localhost",{"sha256":"8h3uXoE6pnI9iLnXI6493qJ0HeuRQfenRIu9PcgH72g"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"EHRoZznhXywhYeIn83o4FSFm3No/aOdLQPHQ68YGtNgESWwpuWLkkGVjoISjz3QgXQ06Fl3cHt7nlTaAHpCNAg"}},"type":"m.room.message"},"latest_event_ids":["$nYdEXrvTDeb7DfkC:localhost"],"last_sent_event_id":"$acCW4IgnBo8YD3jw:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"invite"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$acCW4IgnBo8YD3jw:localhost",{"sha256":"8h3uXoE6pnI9iLnXI6493qJ0HeuRQfenRIu9PcgH72g"}]],"content":{"membership":"invite"},"depth":0,"event_id":"$gKNfcXLlWvs2cFad:localhost","hashes":{"sha256":"iYDOUjYkaGSFbVp7TRVFvGJyGMEuBHMQrJ9XqwhzmPI"},"origin":"localhost","origin_server_ts":1494412135845,"prev_events":[["$nYdEXrvTDeb7DfkC:localhost",{"sha256":"83T5Q3+nDvtS0oJTEhHxIw02twBDa1A7QR2bHtnxv1Y"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"ofw009aMJMqVjww9eDXgeTjOQqSlJl/GN/AAb+6mZAPcUI8aVgRlXOSESfhu1ONEuV/yNUycxNXWfMwuvoWsDg"}},"state_key":"@bob:localhost","type":"m.room.member"},"latest_event_ids":["$gKNfcXLlWvs2cFad:localhost"],"adds_state_event_ids":["$gKNfcXLlWvs2cFad:localhost"],"removes_state_event_ids":["$acCW4IgnBo8YD3jw:localhost"],"last_sent_event_id":"$nYdEXrvTDeb7DfkC:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"leave"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@bob:localhost?access_token=@bob:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$gKNfcXLlWvs2cFad:localhost",{"sha256":"/TYIY+L9qjg516Bzl8sadu+Np21KkxE4KdPXALeJ9eE"}]],"content":{"membership":"leave"},"depth":0,"event_id":"$B2q9Tepb6Xc1Rku0:localhost","hashes":{"sha256":"RbHTVdceAEfTALQDZdGrOmakKeTYnChaKjlVuoNUdSY"},"origin":"localhost","origin_server_ts":1494412187614,"prev_events":[["$gKNfcXLlWvs2cFad:localhost",{"sha256":"/TYIY+L9qjg516Bzl8sadu+Np21KkxE4KdPXALeJ9eE"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@bob:localhost","signatures":{"localhost":{"ed25519:something":"dNtUL86j2zUe5+DkfOkil5VujvFZg4FeTjbtcpeF+3E4SUChCAG3lyR6YOAIYBnjtD0/kqT7OcP3pM6vMEp1Aw"}},"state_key":"@bob:localhost","type":"m.room.member"},"latest_event_ids":["$B2q9Tepb6Xc1Rku0:localhost"],"adds_state_event_ids":["$B2q9Tepb6Xc1Rku0:localhost"],"removes_state_event_ids":["$gKNfcXLlWvs2cFad:localhost"],"last_sent_event_id":"$gKNfcXLlWvs2cFad:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"so alone"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"body":"so alone","msgtype":"m.text"},"depth":0,"event_id":"$W1nrYHQIbCTTSJOV:localhost","hashes":{"sha256":"uUKSa4U1coDoT3LUcNF25dt+UpUa2pLXzRJ3ljgxXZs"},"origin":"localhost","origin_server_ts":1494412229742,"prev_events":[["$B2q9Tepb6Xc1Rku0:localhost",{"sha256":"0CLru7nGPgyF9AWlZnarCElscSVrXl2MMY2atrz80Uc"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"YlBJyDnE34UhaCB9hirQN5OySfTDoqiBDnNvxomXjU94z4a8g2CLWKjApwd/q/j4HamCUtjgkjJ2um6hNjsVBA"}},"type":"m.room.message"},"latest_event_ids":["$W1nrYHQIbCTTSJOV:localhost"],"last_sent_event_id":"$B2q9Tepb6Xc1Rku0:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"name":"Everyone welcome"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.name?access_token=@alice:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$QTen1vksfcRTpUCk:localhost",{"sha256":"znwhbYzdueh0grYkUX4jgXmP9AjKphzyesMZWMiF4IY"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}]],"content":{"name":"Everyone welcome"},"depth":0,"event_id":"$nLzxoBC4A0QRvJ1k:localhost","hashes":{"sha256":"PExCybjaMW1TfgFr57MdIRYJ642FY2jnrdW/tpPOf1Y"},"origin":"localhost","origin_server_ts":1494412294551,"prev_events":[["$W1nrYHQIbCTTSJOV:localhost",{"sha256":"HXk/ACcsiaZ/z1f2aZSIhJF8Ih3BWeh1vp+cV/fwoE0"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@alice:localhost","signatures":{"localhost":{"ed25519:something":"RK09L8sQv78y69PNbOLaX8asq5kp51mbqUuct5gd7ZNmaHKnVds6ew06QEn+gHSDAxqQo2tpcfoajp+yMj1HBw"}},"state_key":"","type":"m.room.name"},"latest_event_ids":["$nLzxoBC4A0QRvJ1k:localhost"],"adds_state_event_ids":["$nLzxoBC4A0QRvJ1k:localhost"],"removes_state_event_ids":["$CY4XDoxjbns3a4Pc:localhost"],"last_sent_event_id":"$W1nrYHQIbCTTSJOV:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"membership":"join"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/state/m.room.member/@charlie:localhost?access_token=@charlie:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$2O2DpHB37CuwwJOe:localhost",{"sha256":"ulaRD63dbCyolLTwvInIQpcrtU2c7ex/BHmhpLXAUoE"}],["$om1F4AI8tCYlHUSp:localhost",{"sha256":"yVs+CW7AiJrJOYouL8xPIBrtIHAhnbxaegna8MxeCto"}]],"content":{"membership":"join"},"depth":0,"event_id":"$Zo6P8r9bczF6kctV:localhost","hashes":{"sha256":"R3J2iUWnGxVdmly8ah+Dgb5VbJ2i/e8BLaWM0z9eZKU"},"origin":"localhost","origin_server_ts":1494412338689,"prev_events":[["$nLzxoBC4A0QRvJ1k:localhost",{"sha256":"TDcFaArAXpxIJ1noSubcFqkLXiQTrc1Dw1+kgCtx3XY"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@charlie:localhost","signatures":{"localhost":{"ed25519:something":"tVnjLVoJ9SLlMQIJSK/6zANWaEu8tVVkx3AEJiC3y5JmhPORb3PyG8eE+e/9hC4aJSQL8LGLaJNWXukMpb2SBg"}},"state_key":"@charlie:localhost","type":"m.room.member"},"latest_event_ids":["$Zo6P8r9bczF6kctV:localhost"],"adds_state_event_ids":["$Zo6P8r9bczF6kctV:localhost"],"removes_state_event_ids":["$om1F4AI8tCYlHUSp:localhost"],"last_sent_event_id":"$nLzxoBC4A0QRvJ1k:localhost"}}`,
|
|
||||||
// $ curl -XPUT -d '{"msgtype":"m.text","body":"hiiiii"}' "http://localhost:8009/_matrix/client/r0/rooms/%21PjrbIMW2cIiaYF4t:localhost/send/m.room.message/3?access_token=@charlie:localhost"
|
|
||||||
`{"type":"new_room_event","new_room_event":{"event":{"auth_events":[["$xz0fUB8zNMTGFh1W:localhost",{"sha256":"F4tTLtltC6f2XKeXq4ZKpMZ5EpditaW+RYQSnYzq3lI"}],["$RWsxGlfPHAcijTgu:localhost",{"sha256":"1zc+86U9vLK1BvTJbeLuYpw9dZqvX2fr8rc3pOF69f8"}],["$Zo6P8r9bczF6kctV:localhost",{"sha256":"mnjt3WTYqwtuyl2Fca+0cgm6moHaNL+W9BqRJTQzdEY"}]],"content":{"body":"hiiiii","msgtype":"m.text"},"depth":0,"event_id":"$YAEvK8u2zkTsjf5P:localhost","hashes":{"sha256":"6hKy61h1tuHjYdfpq2MnaPtGEBAZOUz8FLTtxLwjK5A"},"origin":"localhost","origin_server_ts":1494412375465,"prev_events":[["$Zo6P8r9bczF6kctV:localhost",{"sha256":"mnjt3WTYqwtuyl2Fca+0cgm6moHaNL+W9BqRJTQzdEY"}]],"room_id":"!PjrbIMW2cIiaYF4t:localhost","sender":"@charlie:localhost","signatures":{"localhost":{"ed25519:something":"BsSLaMM5U/YkyvBZ00J/+si9My+wAJZOcBhBeato0oHayiag7FW77ZpSTfADazPdNH62kjB0sdP9CN6vQA7yDg"}},"type":"m.room.message"},"latest_event_ids":["$YAEvK8u2zkTsjf5P:localhost"],"last_sent_event_id":"$Zo6P8r9bczF6kctV:localhost"}}`,
|
|
||||||
}
|
|
||||||
|
|
@ -112,7 +112,7 @@ global:
|
||||||
# Maximum number of entries to hold in the DNS cache, and
|
# Maximum number of entries to hold in the DNS cache, and
|
||||||
# for how long those items should be considered valid in seconds.
|
# for how long those items should be considered valid in seconds.
|
||||||
cache_size: 256
|
cache_size: 256
|
||||||
cache_lifetime: 300
|
cache_lifetime: "5m" # 5minutes; see https://pkg.go.dev/time@master#ParseDuration for more
|
||||||
|
|
||||||
# Configuration for the Appservice API.
|
# Configuration for the Appservice API.
|
||||||
app_service_api:
|
app_service_api:
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use in production environments just yet!
|
||||||
|
|
||||||
Dendrite requires:
|
Dendrite requires:
|
||||||
|
|
||||||
* Go 1.14 or higher
|
* Go 1.15 or higher
|
||||||
* Postgres 9.6 or higher (if using Postgres databases, not needed for SQLite)
|
* Postgres 9.6 or higher (if using Postgres databases, not needed for SQLite)
|
||||||
|
|
||||||
If you want to run a polylith deployment, you also need:
|
If you want to run a polylith deployment, you also need:
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
location /.well-known/matrix/client {
|
location /.well-known/matrix/client {
|
||||||
|
# If your sever_name here doesn't match your matrix homeserver URL
|
||||||
|
# (e.g. hostname.com as server_name and matrix.hostname.com as homeserver URL)
|
||||||
|
# add_header Access-Control-Allow-Origin '*';
|
||||||
return 200 '{ "m.homeserver": { "base_url": "https://my.hostname.com" } }';
|
return 200 '{ "m.homeserver": { "base_url": "https://my.hostname.com" } }';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@ server {
|
||||||
}
|
}
|
||||||
|
|
||||||
location /.well-known/matrix/client {
|
location /.well-known/matrix/client {
|
||||||
|
# If your sever_name here doesn't match your matrix homeserver URL
|
||||||
|
# (e.g. hostname.com as server_name and matrix.hostname.com as homeserver URL)
|
||||||
|
# add_header Access-Control-Allow-Origin '*';
|
||||||
return 200 '{ "m.homeserver": { "base_url": "https://my.hostname.com" } }';
|
return 200 '{ "m.homeserver": { "base_url": "https://my.hostname.com" } }';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
15
docs/p2p.md
15
docs/p2p.md
|
|
@ -2,14 +2,23 @@
|
||||||
|
|
||||||
These are the instructions for setting up P2P Dendrite, current as of May 2020. There's both Go stuff and JS stuff to do to set this up.
|
These are the instructions for setting up P2P Dendrite, current as of May 2020. There's both Go stuff and JS stuff to do to set this up.
|
||||||
|
|
||||||
|
|
||||||
### Dendrite
|
### Dendrite
|
||||||
|
|
||||||
|
#### Build
|
||||||
|
|
||||||
- The `master` branch has a WASM-only binary for dendrite: `./cmd/dendritejs`.
|
- The `master` branch has a WASM-only binary for dendrite: `./cmd/dendritejs`.
|
||||||
- Build it and copy assets to riot-web.
|
- Build it and copy assets to riot-web.
|
||||||
```
|
```
|
||||||
$ GOOS=js GOARCH=wasm go build -o main.wasm ./cmd/dendritejs
|
$ ./build-dendritejs.sh
|
||||||
$ cp main.wasm ../riot-web/src/vector/dendrite.wasm
|
$ cp bin/main.wasm ../riot-web/src/vector/dendrite.wasm
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Test
|
||||||
|
|
||||||
|
To check that the Dendrite side is working well as Wasm, you can run the
|
||||||
|
Wasm-specific tests:
|
||||||
|
```
|
||||||
|
$ ./test-dendritejs.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
### Rendezvous
|
### Rendezvous
|
||||||
|
|
|
||||||
11
federationapi/api/servers.go
Normal file
11
federationapi/api/servers.go
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServersInRoomProvider interface {
|
||||||
|
GetServersForRoom(ctx context.Context, roomID string, event *gomatrixserverlib.Event) []gomatrixserverlib.ServerName
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ package federationapi
|
||||||
import (
|
import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||||
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
|
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
|
||||||
keyserverAPI "github.com/matrix-org/dendrite/keyserver/api"
|
keyserverAPI "github.com/matrix-org/dendrite/keyserver/api"
|
||||||
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
|
@ -39,10 +40,12 @@ func AddPublicRoutes(
|
||||||
eduAPI eduserverAPI.EDUServerInputAPI,
|
eduAPI eduserverAPI.EDUServerInputAPI,
|
||||||
keyAPI keyserverAPI.KeyInternalAPI,
|
keyAPI keyserverAPI.KeyInternalAPI,
|
||||||
mscCfg *config.MSCs,
|
mscCfg *config.MSCs,
|
||||||
|
servers federationAPI.ServersInRoomProvider,
|
||||||
) {
|
) {
|
||||||
routing.Setup(
|
routing.Setup(
|
||||||
fedRouter, keyRouter, cfg, rsAPI,
|
fedRouter, keyRouter, cfg, rsAPI,
|
||||||
eduAPI, federationSenderAPI, keyRing,
|
eduAPI, federationSenderAPI, keyRing,
|
||||||
federation, userAPI, keyAPI, mscCfg,
|
federation, userAPI, keyAPI, mscCfg,
|
||||||
|
servers,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ func TestRoomsV3URLEscapeDoNot404(t *testing.T) {
|
||||||
fsAPI := base.FederationSenderHTTPClient()
|
fsAPI := base.FederationSenderHTTPClient()
|
||||||
// TODO: This is pretty fragile, as if anything calls anything on these nils this test will break.
|
// TODO: This is pretty fragile, as if anything calls anything on these nils this test will break.
|
||||||
// Unfortunately, it makes little sense to instantiate these dependencies when we just want to test routing.
|
// Unfortunately, it makes little sense to instantiate these dependencies when we just want to test routing.
|
||||||
federationapi.AddPublicRoutes(base.PublicFederationAPIMux, base.PublicKeyAPIMux, &cfg.FederationAPI, nil, nil, keyRing, nil, fsAPI, nil, nil, &cfg.MSCs)
|
federationapi.AddPublicRoutes(base.PublicFederationAPIMux, base.PublicKeyAPIMux, &cfg.FederationAPI, nil, nil, keyRing, nil, fsAPI, nil, nil, &cfg.MSCs, nil)
|
||||||
baseURL, cancel := test.ListenAndServe(t, base.PublicFederationAPIMux, true)
|
baseURL, cancel := test.ListenAndServe(t, base.PublicFederationAPIMux, true)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://"))
|
serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://"))
|
||||||
|
|
|
||||||
|
|
@ -37,12 +37,27 @@ func GetUserDevices(
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sigReq := &keyapi.QuerySignaturesRequest{
|
||||||
|
TargetIDs: map[string][]gomatrixserverlib.KeyID{
|
||||||
|
userID: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
sigRes := &keyapi.QuerySignaturesResponse{}
|
||||||
|
keyAPI.QuerySignatures(req.Context(), sigReq, sigRes)
|
||||||
|
|
||||||
response := gomatrixserverlib.RespUserDevices{
|
response := gomatrixserverlib.RespUserDevices{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
StreamID: res.StreamID,
|
StreamID: res.StreamID,
|
||||||
Devices: []gomatrixserverlib.RespUserDevice{},
|
Devices: []gomatrixserverlib.RespUserDevice{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if masterKey, ok := sigRes.MasterKeys[userID]; ok {
|
||||||
|
response.MasterKey = &masterKey
|
||||||
|
}
|
||||||
|
if selfSigningKey, ok := sigRes.SelfSigningKeys[userID]; ok {
|
||||||
|
response.SelfSigningKey = &selfSigningKey
|
||||||
|
}
|
||||||
|
|
||||||
for _, dev := range res.Devices {
|
for _, dev := range res.Devices {
|
||||||
var key gomatrixserverlib.RespUserDeviceKeys
|
var key gomatrixserverlib.RespUserDeviceKeys
|
||||||
err := json.Unmarshal(dev.DeviceKeys.KeyJSON, &key)
|
err := json.Unmarshal(dev.DeviceKeys.KeyJSON, &key)
|
||||||
|
|
@ -56,6 +71,20 @@ func GetUserDevices(
|
||||||
DisplayName: dev.DisplayName,
|
DisplayName: dev.DisplayName,
|
||||||
Keys: key,
|
Keys: key,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if targetUser, ok := sigRes.Signatures[userID]; ok {
|
||||||
|
if targetKey, ok := targetUser[gomatrixserverlib.KeyID(dev.DeviceID)]; ok {
|
||||||
|
for sourceUserID, forSourceUser := range targetKey {
|
||||||
|
for sourceKeyID, sourceKey := range forSourceUser {
|
||||||
|
if _, ok := device.Keys.Signatures[sourceUserID]; !ok {
|
||||||
|
device.Keys.Signatures[sourceUserID] = map[gomatrixserverlib.KeyID]gomatrixserverlib.Base64Bytes{}
|
||||||
|
}
|
||||||
|
device.Keys.Signatures[sourceUserID][sourceKeyID] = sourceKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
response.Devices = append(response.Devices, device)
|
response.Devices = append(response.Devices, device)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,22 +40,29 @@ func InviteV2(
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
inviteReq := gomatrixserverlib.InviteV2Request{}
|
inviteReq := gomatrixserverlib.InviteV2Request{}
|
||||||
err := json.Unmarshal(request.Content(), &inviteReq)
|
err := json.Unmarshal(request.Content(), &inviteReq)
|
||||||
switch err.(type) {
|
switch e := err.(type) {
|
||||||
|
case gomatrixserverlib.UnsupportedRoomVersionError:
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.UnsupportedRoomVersion(
|
||||||
|
fmt.Sprintf("Room version %q is not supported by this server.", e.Version),
|
||||||
|
),
|
||||||
|
}
|
||||||
case gomatrixserverlib.BadJSONError:
|
case gomatrixserverlib.BadJSONError:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.BadJSON(err.Error()),
|
JSON: jsonerror.BadJSON(err.Error()),
|
||||||
}
|
}
|
||||||
case nil:
|
case nil:
|
||||||
|
return processInvite(
|
||||||
|
httpReq.Context(), true, inviteReq.Event(), inviteReq.RoomVersion(), inviteReq.InviteRoomState(), roomID, eventID, cfg, rsAPI, keys,
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusBadRequest,
|
Code: http.StatusBadRequest,
|
||||||
JSON: jsonerror.NotJSON("The request body could not be decoded into an invite request. " + err.Error()),
|
JSON: jsonerror.NotJSON("The request body could not be decoded into an invite request. " + err.Error()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return processInvite(
|
|
||||||
httpReq.Context(), true, inviteReq.Event(), inviteReq.RoomVersion(), inviteReq.InviteRoomState(), roomID, eventID, cfg, rsAPI, keys,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// InviteV1 implements /_matrix/federation/v1/invite/{roomID}/{eventID}
|
// InviteV1 implements /_matrix/federation/v1/invite/{roomID}/{eventID}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MakeJoin implements the /make_join API
|
// MakeJoin implements the /make_join API
|
||||||
|
|
@ -228,6 +229,21 @@ func SendJoin(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that this is in fact a join event
|
||||||
|
membership, err := event.Membership()
|
||||||
|
if err != nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("missing content.membership key"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if membership != gomatrixserverlib.Join {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("membership must be 'join'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check that the event is signed by the server sending the request.
|
// Check that the event is signed by the server sending the request.
|
||||||
redacted := event.Redact()
|
redacted := event.Redact()
|
||||||
verifyRequests := []gomatrixserverlib.VerifyJSONRequest{{
|
verifyRequests := []gomatrixserverlib.VerifyJSONRequest{{
|
||||||
|
|
@ -271,31 +287,51 @@ func SendJoin(
|
||||||
|
|
||||||
// Check if the user is already in the room. If they're already in then
|
// Check if the user is already in the room. If they're already in then
|
||||||
// there isn't much point in sending another join event into the room.
|
// there isn't much point in sending another join event into the room.
|
||||||
|
// Also check to see if they are banned: if they are then we reject them.
|
||||||
alreadyJoined := false
|
alreadyJoined := false
|
||||||
|
isBanned := false
|
||||||
for _, se := range stateAndAuthChainResponse.StateEvents {
|
for _, se := range stateAndAuthChainResponse.StateEvents {
|
||||||
if !se.StateKeyEquals(*event.StateKey()) {
|
if !se.StateKeyEquals(*event.StateKey()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if membership, merr := se.Membership(); merr == nil {
|
if membership, merr := se.Membership(); merr == nil {
|
||||||
alreadyJoined = (membership == gomatrixserverlib.Join)
|
alreadyJoined = (membership == gomatrixserverlib.Join)
|
||||||
|
isBanned = (membership == gomatrixserverlib.Ban)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isBanned {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
JSON: jsonerror.Forbidden("user is banned"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Send the events to the room server.
|
// Send the events to the room server.
|
||||||
// We are responsible for notifying other servers that the user has joined
|
// We are responsible for notifying other servers that the user has joined
|
||||||
// the room, so set SendAsServer to cfg.Matrix.ServerName
|
// the room, so set SendAsServer to cfg.Matrix.ServerName
|
||||||
if !alreadyJoined {
|
if !alreadyJoined {
|
||||||
if err = api.SendEvents(
|
var response api.InputRoomEventsResponse
|
||||||
httpReq.Context(), rsAPI,
|
rsAPI.InputRoomEvents(httpReq.Context(), &api.InputRoomEventsRequest{
|
||||||
api.KindNew,
|
InputRoomEvents: []api.InputRoomEvent{
|
||||||
[]*gomatrixserverlib.HeaderedEvent{
|
{
|
||||||
event.Headered(stateAndAuthChainResponse.RoomVersion),
|
Kind: api.KindNew,
|
||||||
|
Event: event.Headered(stateAndAuthChainResponse.RoomVersion),
|
||||||
|
AuthEventIDs: event.AuthEventIDs(),
|
||||||
|
SendAsServer: string(cfg.Matrix.ServerName),
|
||||||
|
TransactionID: nil,
|
||||||
},
|
},
|
||||||
cfg.Matrix.ServerName,
|
},
|
||||||
nil,
|
}, &response)
|
||||||
); err != nil {
|
if response.ErrMsg != "" {
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("SendEvents failed")
|
util.GetLogger(httpReq.Context()).WithField(logrus.ErrorKey, response.ErrMsg).Error("SendEvents failed")
|
||||||
|
if response.NotAllowed {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.Forbidden(response.ErrMsg),
|
||||||
|
}
|
||||||
|
}
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,13 @@ func QueryDeviceKeys(
|
||||||
Code: 200,
|
Code: 200,
|
||||||
JSON: struct {
|
JSON: struct {
|
||||||
DeviceKeys interface{} `json:"device_keys"`
|
DeviceKeys interface{} `json:"device_keys"`
|
||||||
}{queryRes.DeviceKeys},
|
MasterKeys interface{} `json:"master_keys"`
|
||||||
|
SelfSigningKeys interface{} `json:"self_signing_keys"`
|
||||||
|
}{
|
||||||
|
queryRes.DeviceKeys,
|
||||||
|
queryRes.MasterKeys,
|
||||||
|
queryRes.SelfSigningKeys,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -188,25 +194,30 @@ func NotaryKeys(
|
||||||
}
|
}
|
||||||
response.ServerKeys = []json.RawMessage{}
|
response.ServerKeys = []json.RawMessage{}
|
||||||
|
|
||||||
for serverName := range req.ServerKeys {
|
for serverName, kidToCriteria := range req.ServerKeys {
|
||||||
var keys *gomatrixserverlib.ServerKeys
|
var keyList []gomatrixserverlib.ServerKeys
|
||||||
if serverName == cfg.Matrix.ServerName {
|
if serverName == cfg.Matrix.ServerName {
|
||||||
if k, err := localKeys(cfg, time.Now().Add(cfg.Matrix.KeyValidityPeriod)); err == nil {
|
if k, err := localKeys(cfg, time.Now().Add(cfg.Matrix.KeyValidityPeriod)); err == nil {
|
||||||
keys = k
|
keyList = append(keyList, *k)
|
||||||
} else {
|
} else {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if k, err := fsAPI.GetServerKeys(httpReq.Context(), serverName); err == nil {
|
var resp federationSenderAPI.QueryServerKeysResponse
|
||||||
keys = &k
|
err := fsAPI.QueryServerKeys(httpReq.Context(), &federationSenderAPI.QueryServerKeysRequest{
|
||||||
} else {
|
ServerName: serverName,
|
||||||
|
KeyIDToCriteria: kidToCriteria,
|
||||||
|
}, &resp)
|
||||||
|
if err != nil {
|
||||||
return util.ErrorResponse(err)
|
return util.ErrorResponse(err)
|
||||||
}
|
}
|
||||||
|
keyList = append(keyList, resp.ServerKeys...)
|
||||||
}
|
}
|
||||||
if keys == nil {
|
if len(keyList) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, keys := range keyList {
|
||||||
j, err := json.Marshal(keys)
|
j, err := json.Marshal(keys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.WithError(err).Errorf("Failed to marshal %q response", serverName)
|
logrus.WithError(err).Errorf("Failed to marshal %q response", serverName)
|
||||||
|
|
@ -223,6 +234,7 @@ func NotaryKeys(
|
||||||
|
|
||||||
response.ServerKeys = append(response.ServerKeys, js)
|
response.ServerKeys = append(response.ServerKeys, js)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
Code: http.StatusOK,
|
Code: http.StatusOK,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/matrix-org/dendrite/setup/config"
|
"github.com/matrix-org/dendrite/setup/config"
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MakeLeave implements the /make_leave API
|
// MakeLeave implements the /make_leave API
|
||||||
|
|
@ -174,6 +175,13 @@ func SendLeave(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if event.StateKey() == nil {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.InvalidArgumentValue("missing state_key"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the user has already left. If so, no-op!
|
// Check if the user has already left. If so, no-op!
|
||||||
queryReq := &api.QueryLatestEventsAndStateRequest{
|
queryReq := &api.QueryLatestEventsAndStateRequest{
|
||||||
RoomID: roomID,
|
RoomID: roomID,
|
||||||
|
|
@ -240,7 +248,10 @@ func SendLeave(
|
||||||
mem, err := event.Membership()
|
mem, err := event.Membership()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("event.Membership failed")
|
util.GetLogger(httpReq.Context()).WithError(err).Error("event.Membership failed")
|
||||||
return jsonerror.InternalServerError()
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.BadJSON("missing content.membership key"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if mem != gomatrixserverlib.Leave {
|
if mem != gomatrixserverlib.Leave {
|
||||||
return util.JSONResponse{
|
return util.JSONResponse{
|
||||||
|
|
@ -252,16 +263,27 @@ func SendLeave(
|
||||||
// Send the events to the room server.
|
// Send the events to the room server.
|
||||||
// We are responsible for notifying other servers that the user has left
|
// We are responsible for notifying other servers that the user has left
|
||||||
// the room, so set SendAsServer to cfg.Matrix.ServerName
|
// the room, so set SendAsServer to cfg.Matrix.ServerName
|
||||||
if err = api.SendEvents(
|
var response api.InputRoomEventsResponse
|
||||||
httpReq.Context(), rsAPI,
|
rsAPI.InputRoomEvents(httpReq.Context(), &api.InputRoomEventsRequest{
|
||||||
api.KindNew,
|
InputRoomEvents: []api.InputRoomEvent{
|
||||||
[]*gomatrixserverlib.HeaderedEvent{
|
{
|
||||||
event.Headered(verRes.RoomVersion),
|
Kind: api.KindNew,
|
||||||
|
Event: event.Headered(verRes.RoomVersion),
|
||||||
|
AuthEventIDs: event.AuthEventIDs(),
|
||||||
|
SendAsServer: string(cfg.Matrix.ServerName),
|
||||||
|
TransactionID: nil,
|
||||||
},
|
},
|
||||||
cfg.Matrix.ServerName,
|
},
|
||||||
nil,
|
}, &response)
|
||||||
); err != nil {
|
|
||||||
util.GetLogger(httpReq.Context()).WithError(err).Error("producer.SendEvents failed")
|
if response.ErrMsg != "" {
|
||||||
|
util.GetLogger(httpReq.Context()).WithField(logrus.ErrorKey, response.ErrMsg).WithField("not_allowed", response.NotAllowed).Error("producer.SendEvents failed")
|
||||||
|
if response.NotAllowed {
|
||||||
|
return util.JSONResponse{
|
||||||
|
Code: http.StatusBadRequest,
|
||||||
|
JSON: jsonerror.Forbidden(response.ErrMsg),
|
||||||
|
}
|
||||||
|
}
|
||||||
return jsonerror.InternalServerError()
|
return jsonerror.InternalServerError()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ import (
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||||
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
|
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/internal"
|
||||||
"github.com/matrix-org/dendrite/internal/httputil"
|
"github.com/matrix-org/dendrite/internal/httputil"
|
||||||
|
|
@ -50,6 +51,7 @@ func Setup(
|
||||||
userAPI userapi.UserInternalAPI,
|
userAPI userapi.UserInternalAPI,
|
||||||
keyAPI keyserverAPI.KeyInternalAPI,
|
keyAPI keyserverAPI.KeyInternalAPI,
|
||||||
mscCfg *config.MSCs,
|
mscCfg *config.MSCs,
|
||||||
|
servers federationAPI.ServersInRoomProvider,
|
||||||
) {
|
) {
|
||||||
v2keysmux := keyMux.PathPrefix("/v2").Subrouter()
|
v2keysmux := keyMux.PathPrefix("/v2").Subrouter()
|
||||||
v1fedmux := fedMux.PathPrefix("/v1").Subrouter()
|
v1fedmux := fedMux.PathPrefix("/v1").Subrouter()
|
||||||
|
|
@ -99,7 +101,7 @@ func Setup(
|
||||||
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse {
|
func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse {
|
||||||
return Send(
|
return Send(
|
||||||
httpReq, request, gomatrixserverlib.TransactionID(vars["txnID"]),
|
httpReq, request, gomatrixserverlib.TransactionID(vars["txnID"]),
|
||||||
cfg, rsAPI, eduAPI, keyAPI, keys, federation, mu,
|
cfg, rsAPI, eduAPI, keyAPI, keys, federation, mu, servers,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)).Methods(http.MethodPut, http.MethodOptions)
|
)).Methods(http.MethodPut, http.MethodOptions)
|
||||||
|
|
|
||||||
|
|
@ -16,16 +16,16 @@ package routing
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
|
||||||
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
||||||
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
eduserverAPI "github.com/matrix-org/dendrite/eduserver/api"
|
||||||
|
federationAPI "github.com/matrix-org/dendrite/federationapi/api"
|
||||||
"github.com/matrix-org/dendrite/internal"
|
"github.com/matrix-org/dendrite/internal"
|
||||||
keyapi "github.com/matrix-org/dendrite/keyserver/api"
|
keyapi "github.com/matrix-org/dendrite/keyserver/api"
|
||||||
"github.com/matrix-org/dendrite/roomserver/api"
|
"github.com/matrix-org/dendrite/roomserver/api"
|
||||||
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/matrix-org/util"
|
"github.com/matrix-org/util"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
@ -88,6 +89,67 @@ func init() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type sendFIFOQueue struct {
|
||||||
|
tasks []*inputTask
|
||||||
|
count int
|
||||||
|
mutex sync.Mutex
|
||||||
|
notifs chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSendFIFOQueue() *sendFIFOQueue {
|
||||||
|
q := &sendFIFOQueue{
|
||||||
|
notifs: make(chan struct{}, 1),
|
||||||
|
}
|
||||||
|
return q
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *sendFIFOQueue) push(frame *inputTask) {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
q.tasks = append(q.tasks, frame)
|
||||||
|
q.count++
|
||||||
|
select {
|
||||||
|
case q.notifs <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pop returns the first item of the queue, if there is one.
|
||||||
|
// The second return value will indicate if a task was returned.
|
||||||
|
func (q *sendFIFOQueue) pop() (*inputTask, bool) {
|
||||||
|
q.mutex.Lock()
|
||||||
|
defer q.mutex.Unlock()
|
||||||
|
if q.count == 0 {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
frame := q.tasks[0]
|
||||||
|
q.tasks[0] = nil
|
||||||
|
q.tasks = q.tasks[1:]
|
||||||
|
q.count--
|
||||||
|
if q.count == 0 {
|
||||||
|
// Force a GC of the underlying array, since it might have
|
||||||
|
// grown significantly if the queue was hammered for some reason
|
||||||
|
q.tasks = nil
|
||||||
|
}
|
||||||
|
return frame, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type inputTask struct {
|
||||||
|
ctx context.Context
|
||||||
|
t *txnReq
|
||||||
|
event *gomatrixserverlib.Event
|
||||||
|
wg *sync.WaitGroup
|
||||||
|
err error // written back by worker, only safe to read when all tasks are done
|
||||||
|
duration time.Duration // written back by worker, only safe to read when all tasks are done
|
||||||
|
}
|
||||||
|
|
||||||
|
type inputWorker struct {
|
||||||
|
running atomic.Bool
|
||||||
|
input *sendFIFOQueue
|
||||||
|
}
|
||||||
|
|
||||||
|
var inputWorkers sync.Map // room ID -> *inputWorker
|
||||||
|
|
||||||
// Send implements /_matrix/federation/v1/send/{txnID}
|
// Send implements /_matrix/federation/v1/send/{txnID}
|
||||||
func Send(
|
func Send(
|
||||||
httpReq *http.Request,
|
httpReq *http.Request,
|
||||||
|
|
@ -100,14 +162,16 @@ func Send(
|
||||||
keys gomatrixserverlib.JSONVerifier,
|
keys gomatrixserverlib.JSONVerifier,
|
||||||
federation *gomatrixserverlib.FederationClient,
|
federation *gomatrixserverlib.FederationClient,
|
||||||
mu *internal.MutexByRoom,
|
mu *internal.MutexByRoom,
|
||||||
|
servers federationAPI.ServersInRoomProvider,
|
||||||
) util.JSONResponse {
|
) util.JSONResponse {
|
||||||
t := txnReq{
|
t := txnReq{
|
||||||
rsAPI: rsAPI,
|
rsAPI: rsAPI,
|
||||||
eduAPI: eduAPI,
|
eduAPI: eduAPI,
|
||||||
keys: keys,
|
keys: keys,
|
||||||
federation: federation,
|
federation: federation,
|
||||||
|
hadEvents: make(map[string]bool),
|
||||||
haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent),
|
haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent),
|
||||||
newEvents: make(map[string]bool),
|
servers: servers,
|
||||||
keyAPI: keyAPI,
|
keyAPI: keyAPI,
|
||||||
roomsMu: mu,
|
roomsMu: mu,
|
||||||
}
|
}
|
||||||
|
|
@ -141,7 +205,7 @@ func Send(
|
||||||
|
|
||||||
util.GetLogger(httpReq.Context()).Infof("Received transaction %q from %q containing %d PDUs, %d EDUs", txnID, request.Origin(), len(t.PDUs), len(t.EDUs))
|
util.GetLogger(httpReq.Context()).Infof("Received transaction %q from %q containing %d PDUs, %d EDUs", txnID, request.Origin(), len(t.PDUs), len(t.EDUs))
|
||||||
|
|
||||||
resp, jsonErr := t.processTransaction(context.Background())
|
resp, jsonErr := t.processTransaction(httpReq.Context())
|
||||||
if jsonErr != nil {
|
if jsonErr != nil {
|
||||||
util.GetLogger(httpReq.Context()).WithField("jsonErr", jsonErr).Error("t.processTransaction failed")
|
util.GetLogger(httpReq.Context()).WithField("jsonErr", jsonErr).Error("t.processTransaction failed")
|
||||||
return *jsonErr
|
return *jsonErr
|
||||||
|
|
@ -164,18 +228,25 @@ type txnReq struct {
|
||||||
keyAPI keyapi.KeyInternalAPI
|
keyAPI keyapi.KeyInternalAPI
|
||||||
keys gomatrixserverlib.JSONVerifier
|
keys gomatrixserverlib.JSONVerifier
|
||||||
federation txnFederationClient
|
federation txnFederationClient
|
||||||
servers []gomatrixserverlib.ServerName
|
|
||||||
serversMutex sync.RWMutex
|
|
||||||
roomsMu *internal.MutexByRoom
|
roomsMu *internal.MutexByRoom
|
||||||
|
// something that can tell us about which servers are in a room right now
|
||||||
|
servers federationAPI.ServersInRoomProvider
|
||||||
|
// a list of events from the auth and prev events which we already had
|
||||||
|
hadEvents map[string]bool
|
||||||
|
hadEventsMutex sync.Mutex
|
||||||
// local cache of events for auth checks, etc - this may include events
|
// local cache of events for auth checks, etc - this may include events
|
||||||
// which the roomserver is unaware of.
|
// which the roomserver is unaware of.
|
||||||
haveEvents map[string]*gomatrixserverlib.HeaderedEvent
|
haveEvents map[string]*gomatrixserverlib.HeaderedEvent
|
||||||
// new events which the roomserver does not know about
|
haveEventsMutex sync.Mutex
|
||||||
newEvents map[string]bool
|
|
||||||
newEventsMutex sync.RWMutex
|
|
||||||
work string // metrics
|
work string // metrics
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t *txnReq) hadEvent(eventID string, had bool) {
|
||||||
|
t.hadEventsMutex.Lock()
|
||||||
|
defer t.hadEventsMutex.Unlock()
|
||||||
|
t.hadEvents[eventID] = had
|
||||||
|
}
|
||||||
|
|
||||||
// A subset of FederationClient functionality that txn requires. Useful for testing.
|
// A subset of FederationClient functionality that txn requires. Useful for testing.
|
||||||
type txnFederationClient interface {
|
type txnFederationClient interface {
|
||||||
LookupState(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, eventID string, roomVersion gomatrixserverlib.RoomVersion) (
|
LookupState(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, eventID string, roomVersion gomatrixserverlib.RoomVersion) (
|
||||||
|
|
@ -189,8 +260,9 @@ type txnFederationClient interface {
|
||||||
|
|
||||||
func (t *txnReq) processTransaction(ctx context.Context) (*gomatrixserverlib.RespSend, *util.JSONResponse) {
|
func (t *txnReq) processTransaction(ctx context.Context) (*gomatrixserverlib.RespSend, *util.JSONResponse) {
|
||||||
results := make(map[string]gomatrixserverlib.PDUResult)
|
results := make(map[string]gomatrixserverlib.PDUResult)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
var tasks []*inputTask
|
||||||
|
|
||||||
pdus := []*gomatrixserverlib.HeaderedEvent{}
|
|
||||||
for _, pdu := range t.PDUs {
|
for _, pdu := range t.PDUs {
|
||||||
pduCountTotal.WithLabelValues("total").Inc()
|
pduCountTotal.WithLabelValues("total").Inc()
|
||||||
var header struct {
|
var header struct {
|
||||||
|
|
@ -241,83 +313,94 @@ func (t *txnReq) processTransaction(ctx context.Context) (*gomatrixserverlib.Res
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pdus = append(pdus, event.Headered(verRes.RoomVersion))
|
v, _ := inputWorkers.LoadOrStore(event.RoomID(), &inputWorker{
|
||||||
|
input: newSendFIFOQueue(),
|
||||||
|
})
|
||||||
|
worker := v.(*inputWorker)
|
||||||
|
wg.Add(1)
|
||||||
|
task := &inputTask{
|
||||||
|
ctx: ctx,
|
||||||
|
t: t,
|
||||||
|
event: event,
|
||||||
|
wg: &wg,
|
||||||
}
|
}
|
||||||
|
tasks = append(tasks, task)
|
||||||
// Process the events.
|
worker.input.push(task)
|
||||||
for _, e := range pdus {
|
if worker.running.CAS(false, true) {
|
||||||
evStart := time.Now()
|
go worker.run()
|
||||||
if err := t.processEvent(ctx, e.Unwrap()); err != nil {
|
|
||||||
// If the error is due to the event itself being bad then we skip
|
|
||||||
// it and move onto the next event. We report an error so that the
|
|
||||||
// sender knows that we have skipped processing it.
|
|
||||||
//
|
|
||||||
// However if the event is due to a temporary failure in our server
|
|
||||||
// such as a database being unavailable then we should bail, and
|
|
||||||
// hope that the sender will retry when we are feeling better.
|
|
||||||
//
|
|
||||||
// It is uncertain what we should do if an event fails because
|
|
||||||
// we failed to fetch more information from the sending server.
|
|
||||||
// For example if a request to /state fails.
|
|
||||||
// If we skip the event then we risk missing the event until we
|
|
||||||
// receive another event referencing it.
|
|
||||||
// If we bail and stop processing then we risk wedging incoming
|
|
||||||
// transactions from that server forever.
|
|
||||||
if isProcessingErrorFatal(err) {
|
|
||||||
sentry.CaptureException(err)
|
|
||||||
// Any other error should be the result of a temporary error in
|
|
||||||
// our server so we should bail processing the transaction entirely.
|
|
||||||
util.GetLogger(ctx).Warnf("Processing %s failed fatally: %s", e.EventID(), err)
|
|
||||||
jsonErr := util.ErrorResponse(err)
|
|
||||||
processEventSummary.WithLabelValues(t.work, MetricsOutcomeFatal).Observe(
|
|
||||||
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
|
||||||
)
|
|
||||||
return nil, &jsonErr
|
|
||||||
} else {
|
|
||||||
// Auth errors mean the event is 'rejected' which have to be silent to appease sytest
|
|
||||||
errMsg := ""
|
|
||||||
outcome := MetricsOutcomeRejected
|
|
||||||
_, rejected := err.(*gomatrixserverlib.NotAllowed)
|
|
||||||
if !rejected {
|
|
||||||
errMsg = err.Error()
|
|
||||||
outcome = MetricsOutcomeFail
|
|
||||||
}
|
|
||||||
util.GetLogger(ctx).WithError(err).WithField("event_id", e.EventID()).WithField("rejected", rejected).Warn(
|
|
||||||
"Failed to process incoming federation event, skipping",
|
|
||||||
)
|
|
||||||
processEventSummary.WithLabelValues(t.work, outcome).Observe(
|
|
||||||
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
|
||||||
)
|
|
||||||
results[e.EventID()] = gomatrixserverlib.PDUResult{
|
|
||||||
Error: errMsg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
results[e.EventID()] = gomatrixserverlib.PDUResult{}
|
|
||||||
pduCountTotal.WithLabelValues("success").Inc()
|
|
||||||
processEventSummary.WithLabelValues(t.work, MetricsOutcomeOK).Observe(
|
|
||||||
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
t.processEDUs(ctx)
|
t.processEDUs(ctx)
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
for _, task := range tasks {
|
||||||
|
if task.err != nil {
|
||||||
|
results[task.event.EventID()] = gomatrixserverlib.PDUResult{
|
||||||
|
Error: task.err.Error(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
results[task.event.EventID()] = gomatrixserverlib.PDUResult{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if c := len(results); c > 0 {
|
if c := len(results); c > 0 {
|
||||||
util.GetLogger(ctx).Infof("Processed %d PDUs from transaction %q", c, t.TransactionID)
|
util.GetLogger(ctx).Infof("Processed %d PDUs from transaction %q", c, t.TransactionID)
|
||||||
}
|
}
|
||||||
return &gomatrixserverlib.RespSend{PDUs: results}, nil
|
return &gomatrixserverlib.RespSend{PDUs: results}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// isProcessingErrorFatal returns true if the error is really bad and
|
func (t *inputWorker) run() {
|
||||||
// we should stop processing the transaction, and returns false if it
|
defer t.running.Store(false)
|
||||||
// is just some less serious error about a specific event.
|
for {
|
||||||
func isProcessingErrorFatal(err error) bool {
|
task, ok := t.input.pop()
|
||||||
switch err {
|
if !ok {
|
||||||
case sql.ErrConnDone:
|
return
|
||||||
case sql.ErrTxDone:
|
}
|
||||||
return true
|
if task == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
func() {
|
||||||
|
defer task.wg.Done()
|
||||||
|
select {
|
||||||
|
case <-task.ctx.Done():
|
||||||
|
task.err = context.DeadlineExceeded
|
||||||
|
pduCountTotal.WithLabelValues("expired").Inc()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
evStart := time.Now()
|
||||||
|
// TODO: Is 5 minutes too long?
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
|
||||||
|
task.err = task.t.processEvent(ctx, task.event)
|
||||||
|
cancel()
|
||||||
|
task.duration = time.Since(evStart)
|
||||||
|
if err := task.err; err != nil {
|
||||||
|
switch err.(type) {
|
||||||
|
case *gomatrixserverlib.NotAllowed:
|
||||||
|
processEventSummary.WithLabelValues(task.t.work, MetricsOutcomeRejected).Observe(
|
||||||
|
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
||||||
|
)
|
||||||
|
util.GetLogger(task.ctx).WithError(err).WithField("event_id", task.event.EventID()).WithField("rejected", true).Warn(
|
||||||
|
"Failed to process incoming federation event, skipping",
|
||||||
|
)
|
||||||
|
task.err = nil // make "rejected" failures silent
|
||||||
|
default:
|
||||||
|
processEventSummary.WithLabelValues(task.t.work, MetricsOutcomeFail).Observe(
|
||||||
|
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
||||||
|
)
|
||||||
|
util.GetLogger(task.ctx).WithError(err).WithField("event_id", task.event.EventID()).WithField("rejected", false).Warn(
|
||||||
|
"Failed to process incoming federation event, skipping",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pduCountTotal.WithLabelValues("success").Inc()
|
||||||
|
processEventSummary.WithLabelValues(task.t.work, MetricsOutcomeOK).Observe(
|
||||||
|
float64(time.Since(evStart).Nanoseconds()) / 1000.,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type roomNotFoundError struct {
|
type roomNotFoundError struct {
|
||||||
|
|
@ -340,19 +423,6 @@ func (e missingPrevEventsError) Error() string {
|
||||||
return fmt.Sprintf("unable to get prev_events for event %q: %s", e.eventID, e.err)
|
return fmt.Sprintf("unable to get prev_events for event %q: %s", e.eventID, e.err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txnReq) haveEventIDs() map[string]bool {
|
|
||||||
t.newEventsMutex.RLock()
|
|
||||||
defer t.newEventsMutex.RUnlock()
|
|
||||||
result := make(map[string]bool, len(t.haveEvents))
|
|
||||||
for eventID := range t.haveEvents {
|
|
||||||
if t.newEvents[eventID] {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
result[eventID] = true
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *txnReq) processEDUs(ctx context.Context) {
|
func (t *txnReq) processEDUs(ctx context.Context) {
|
||||||
for _, e := range t.EDUs {
|
for _, e := range t.EDUs {
|
||||||
eduCountTotal.Inc()
|
eduCountTotal.Inc()
|
||||||
|
|
@ -479,28 +549,47 @@ func (t *txnReq) processDeviceListUpdate(ctx context.Context, e gomatrixserverli
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txnReq) getServers(ctx context.Context, roomID string) []gomatrixserverlib.ServerName {
|
func (t *txnReq) getServers(ctx context.Context, roomID string, event *gomatrixserverlib.Event) []gomatrixserverlib.ServerName {
|
||||||
t.serversMutex.Lock()
|
// The server that sent us the event should be sufficient to tell us about missing
|
||||||
defer t.serversMutex.Unlock()
|
// prev and auth events.
|
||||||
|
servers := []gomatrixserverlib.ServerName{t.Origin}
|
||||||
|
// If the event origin is different to the transaction origin then we can use
|
||||||
|
// this as a last resort. The origin server that created the event would have
|
||||||
|
// had to know the auth and prev events.
|
||||||
|
if event != nil {
|
||||||
|
if origin := event.Origin(); origin != t.Origin {
|
||||||
|
servers = append(servers, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a specific room-to-server provider exists then use that. This will primarily
|
||||||
|
// be used for the P2P demos.
|
||||||
if t.servers != nil {
|
if t.servers != nil {
|
||||||
return t.servers
|
servers = append(servers, t.servers.GetServersForRoom(ctx, roomID, event)...)
|
||||||
}
|
}
|
||||||
t.servers = []gomatrixserverlib.ServerName{t.Origin}
|
return servers
|
||||||
serverReq := &api.QueryServerJoinedToRoomRequest{
|
|
||||||
RoomID: roomID,
|
|
||||||
}
|
|
||||||
serverRes := &api.QueryServerJoinedToRoomResponse{}
|
|
||||||
if err := t.rsAPI.QueryServerJoinedToRoom(ctx, serverReq, serverRes); err == nil {
|
|
||||||
t.servers = append(t.servers, serverRes.ServerNames...)
|
|
||||||
util.GetLogger(ctx).Infof("Found %d server(s) to query for missing events in %q", len(t.servers), roomID)
|
|
||||||
}
|
|
||||||
return t.servers
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txnReq) processEvent(ctx context.Context, e *gomatrixserverlib.Event) error {
|
func (t *txnReq) processEvent(ctx context.Context, e *gomatrixserverlib.Event) error {
|
||||||
logger := util.GetLogger(ctx).WithField("event_id", e.EventID()).WithField("room_id", e.RoomID())
|
logger := util.GetLogger(ctx).WithField("event_id", e.EventID()).WithField("room_id", e.RoomID())
|
||||||
t.work = "" // reset from previous event
|
t.work = "" // reset from previous event
|
||||||
|
|
||||||
|
// Ask the roomserver if we know about the room and/or if we're joined
|
||||||
|
// to it. If we aren't then we won't bother processing the event.
|
||||||
|
joinedReq := api.QueryServerJoinedToRoomRequest{
|
||||||
|
RoomID: e.RoomID(),
|
||||||
|
}
|
||||||
|
var joinedRes api.QueryServerJoinedToRoomResponse
|
||||||
|
if err := t.rsAPI.QueryServerJoinedToRoom(ctx, &joinedReq, &joinedRes); err != nil {
|
||||||
|
return fmt.Errorf("t.rsAPI.QueryServerJoinedToRoom: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !joinedRes.RoomExists || !joinedRes.IsInRoom {
|
||||||
|
// We don't believe we're a member of this room, therefore there's
|
||||||
|
// no point in wasting work trying to figure out what to do with
|
||||||
|
// missing auth or prev events. Drop the event.
|
||||||
|
return roomNotFoundError{e.RoomID()}
|
||||||
|
}
|
||||||
|
|
||||||
// Work out if the roomserver knows everything it needs to know to auth
|
// Work out if the roomserver knows everything it needs to know to auth
|
||||||
// the event. This includes the prev_events and auth_events.
|
// the event. This includes the prev_events and auth_events.
|
||||||
// NOTE! This is going to include prev_events that have an empty state
|
// NOTE! This is going to include prev_events that have an empty state
|
||||||
|
|
@ -517,14 +606,13 @@ func (t *txnReq) processEvent(ctx context.Context, e *gomatrixserverlib.Event) e
|
||||||
return fmt.Errorf("t.rsAPI.QueryMissingAuthPrevEvents: %w", err)
|
return fmt.Errorf("t.rsAPI.QueryMissingAuthPrevEvents: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !stateResp.RoomExists {
|
// Prepare a map of all the events we already had before this point, so
|
||||||
// TODO: When synapse receives a message for a room it is not in it
|
// that we don't send them to the roomserver again.
|
||||||
// asks the remote server for the state of the room so that it can
|
for _, eventID := range append(e.AuthEventIDs(), e.PrevEventIDs()...) {
|
||||||
// check if the remote server knows of a join "m.room.member" event
|
t.hadEvent(eventID, true)
|
||||||
// that this server is unaware of.
|
}
|
||||||
// However generally speaking we should reject events for rooms we
|
for _, eventID := range append(stateResp.MissingAuthEventIDs, stateResp.MissingPrevEventIDs...) {
|
||||||
// aren't a member of.
|
t.hadEvent(eventID, false)
|
||||||
return roomNotFoundError{e.RoomID()}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(stateResp.MissingAuthEventIDs) > 0 {
|
if len(stateResp.MissingAuthEventIDs) > 0 {
|
||||||
|
|
@ -570,11 +658,14 @@ func (t *txnReq) retrieveMissingAuthEvents(
|
||||||
withNextEvent:
|
withNextEvent:
|
||||||
for missingAuthEventID := range missingAuthEvents {
|
for missingAuthEventID := range missingAuthEvents {
|
||||||
withNextServer:
|
withNextServer:
|
||||||
for _, server := range t.getServers(ctx, e.RoomID()) {
|
for _, server := range t.getServers(ctx, e.RoomID(), e) {
|
||||||
logger.Infof("Retrieving missing auth event %q from %q", missingAuthEventID, server)
|
logger.Infof("Retrieving missing auth event %q from %q", missingAuthEventID, server)
|
||||||
tx, err := t.federation.GetEvent(ctx, server, missingAuthEventID)
|
tx, err := t.federation.GetEvent(ctx, server, missingAuthEventID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WithError(err).Warnf("Failed to retrieve auth event %q", missingAuthEventID)
|
logger.WithError(err).Warnf("Failed to retrieve auth event %q", missingAuthEventID)
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
continue withNextServer
|
continue withNextServer
|
||||||
}
|
}
|
||||||
ev, err := gomatrixserverlib.NewEventFromUntrustedJSON(tx.PDUs[0], stateResp.RoomVersion)
|
ev, err := gomatrixserverlib.NewEventFromUntrustedJSON(tx.PDUs[0], stateResp.RoomVersion)
|
||||||
|
|
@ -596,6 +687,8 @@ withNextEvent:
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return fmt.Errorf("api.SendEvents: %w", err)
|
return fmt.Errorf("api.SendEvents: %w", err)
|
||||||
}
|
}
|
||||||
|
t.hadEvent(ev.EventID(), true) // if the roomserver didn't know about the event before, it does now
|
||||||
|
t.cacheAndReturn(ev.Headered(stateResp.RoomVersion))
|
||||||
delete(missingAuthEvents, missingAuthEventID)
|
delete(missingAuthEvents, missingAuthEventID)
|
||||||
continue withNextEvent
|
continue withNextEvent
|
||||||
}
|
}
|
||||||
|
|
@ -621,11 +714,6 @@ func checkAllowedByState(e *gomatrixserverlib.Event, stateEvents []*gomatrixserv
|
||||||
func (t *txnReq) processEventWithMissingState(
|
func (t *txnReq) processEventWithMissingState(
|
||||||
ctx context.Context, e *gomatrixserverlib.Event, roomVersion gomatrixserverlib.RoomVersion,
|
ctx context.Context, e *gomatrixserverlib.Event, roomVersion gomatrixserverlib.RoomVersion,
|
||||||
) error {
|
) error {
|
||||||
// Do this with a fresh context, so that we keep working even if the
|
|
||||||
// original request times out. With any luck, by the time the remote
|
|
||||||
// side retries, we'll have fetched the missing state.
|
|
||||||
gmectx, cancel := context.WithTimeout(context.Background(), time.Minute*5)
|
|
||||||
defer cancel()
|
|
||||||
// We are missing the previous events for this events.
|
// We are missing the previous events for this events.
|
||||||
// This means that there is a gap in our view of the history of the
|
// This means that there is a gap in our view of the history of the
|
||||||
// room. There two ways that we can handle such a gap:
|
// room. There two ways that we can handle such a gap:
|
||||||
|
|
@ -646,7 +734,7 @@ func (t *txnReq) processEventWithMissingState(
|
||||||
// - fill in the gap completely then process event `e` returning no backwards extremity
|
// - fill in the gap completely then process event `e` returning no backwards extremity
|
||||||
// - fail to fill in the gap and tell us to terminate the transaction err=not nil
|
// - fail to fill in the gap and tell us to terminate the transaction err=not nil
|
||||||
// - fail to fill in the gap and tell us to fetch state at the new backwards extremity, and to not terminate the transaction
|
// - fail to fill in the gap and tell us to fetch state at the new backwards extremity, and to not terminate the transaction
|
||||||
newEvents, err := t.getMissingEvents(gmectx, e, roomVersion)
|
newEvents, err := t.getMissingEvents(ctx, e, roomVersion)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -673,7 +761,7 @@ func (t *txnReq) processEventWithMissingState(
|
||||||
// Look up what the state is after the backward extremity. This will either
|
// Look up what the state is after the backward extremity. This will either
|
||||||
// come from the roomserver, if we know all the required events, or it will
|
// come from the roomserver, if we know all the required events, or it will
|
||||||
// come from a remote server via /state_ids if not.
|
// come from a remote server via /state_ids if not.
|
||||||
prevState, trustworthy, lerr := t.lookupStateAfterEvent(gmectx, roomVersion, backwardsExtremity.RoomID(), prevEventID)
|
prevState, trustworthy, lerr := t.lookupStateAfterEvent(ctx, roomVersion, backwardsExtremity.RoomID(), prevEventID)
|
||||||
if lerr != nil {
|
if lerr != nil {
|
||||||
util.GetLogger(ctx).WithError(lerr).Errorf("Failed to lookup state after prev_event: %s", prevEventID)
|
util.GetLogger(ctx).WithError(lerr).Errorf("Failed to lookup state after prev_event: %s", prevEventID)
|
||||||
return lerr
|
return lerr
|
||||||
|
|
@ -717,7 +805,7 @@ func (t *txnReq) processEventWithMissingState(
|
||||||
}
|
}
|
||||||
// There's more than one previous state - run them all through state res
|
// There's more than one previous state - run them all through state res
|
||||||
t.roomsMu.Lock(e.RoomID())
|
t.roomsMu.Lock(e.RoomID())
|
||||||
resolvedState, err = t.resolveStatesAndCheck(gmectx, roomVersion, respStates, backwardsExtremity)
|
resolvedState, err = t.resolveStatesAndCheck(ctx, roomVersion, respStates, backwardsExtremity)
|
||||||
t.roomsMu.Unlock(e.RoomID())
|
t.roomsMu.Unlock(e.RoomID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.GetLogger(ctx).WithError(err).Errorf("Failed to resolve state conflicts for event %s", backwardsExtremity.EventID())
|
util.GetLogger(ctx).WithError(err).Errorf("Failed to resolve state conflicts for event %s", backwardsExtremity.EventID())
|
||||||
|
|
@ -727,14 +815,23 @@ func (t *txnReq) processEventWithMissingState(
|
||||||
|
|
||||||
// First of all, send the backward extremity into the roomserver with the
|
// First of all, send the backward extremity into the roomserver with the
|
||||||
// newly resolved state. This marks the "oldest" point in the backfill and
|
// newly resolved state. This marks the "oldest" point in the backfill and
|
||||||
// sets the baseline state for any new events after this.
|
// sets the baseline state for any new events after this. We'll make a
|
||||||
|
// copy of the hadEvents map so that it can be taken downstream without
|
||||||
|
// worrying about concurrent map reads/writes, since t.hadEvents is meant
|
||||||
|
// to be protected by a mutex.
|
||||||
|
hadEvents := map[string]bool{}
|
||||||
|
t.hadEventsMutex.Lock()
|
||||||
|
for k, v := range t.hadEvents {
|
||||||
|
hadEvents[k] = v
|
||||||
|
}
|
||||||
|
t.hadEventsMutex.Unlock()
|
||||||
err = api.SendEventWithState(
|
err = api.SendEventWithState(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
t.rsAPI,
|
t.rsAPI,
|
||||||
api.KindOld,
|
api.KindOld,
|
||||||
resolvedState,
|
resolvedState,
|
||||||
backwardsExtremity.Headered(roomVersion),
|
backwardsExtremity.Headered(roomVersion),
|
||||||
t.haveEventIDs(),
|
hadEvents,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("api.SendEventWithState: %w", err)
|
return fmt.Errorf("api.SendEventWithState: %w", err)
|
||||||
|
|
@ -786,7 +883,7 @@ func (t *txnReq) lookupStateAfterEvent(ctx context.Context, roomVersion gomatrix
|
||||||
default:
|
default:
|
||||||
return nil, false, fmt.Errorf("t.lookupEvent: %w", err)
|
return nil, false, fmt.Errorf("t.lookupEvent: %w", err)
|
||||||
}
|
}
|
||||||
t.cacheAndReturn(h)
|
h = t.cacheAndReturn(h)
|
||||||
if h.StateKey() != nil {
|
if h.StateKey() != nil {
|
||||||
addedToState := false
|
addedToState := false
|
||||||
for i := range respState.StateEvents {
|
for i := range respState.StateEvents {
|
||||||
|
|
@ -806,6 +903,8 @@ func (t *txnReq) lookupStateAfterEvent(ctx context.Context, roomVersion gomatrix
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *txnReq) cacheAndReturn(ev *gomatrixserverlib.HeaderedEvent) *gomatrixserverlib.HeaderedEvent {
|
func (t *txnReq) cacheAndReturn(ev *gomatrixserverlib.HeaderedEvent) *gomatrixserverlib.HeaderedEvent {
|
||||||
|
t.haveEventsMutex.Lock()
|
||||||
|
defer t.haveEventsMutex.Unlock()
|
||||||
if cached, exists := t.haveEvents[ev.EventID()]; exists {
|
if cached, exists := t.haveEvents[ev.EventID()]; exists {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
|
|
@ -828,6 +927,7 @@ func (t *txnReq) lookupStateAfterEventLocally(ctx context.Context, roomID, event
|
||||||
// set the event from the haveEvents cache - this means we will share pointers with other prev_event branches for this
|
// set the event from the haveEvents cache - this means we will share pointers with other prev_event branches for this
|
||||||
// processEvent request, which is better for memory.
|
// processEvent request, which is better for memory.
|
||||||
stateEvents[i] = t.cacheAndReturn(ev)
|
stateEvents[i] = t.cacheAndReturn(ev)
|
||||||
|
t.hadEvent(ev.EventID(), true)
|
||||||
}
|
}
|
||||||
// we should never access res.StateEvents again so we delete it here to make GC faster
|
// we should never access res.StateEvents again so we delete it here to make GC faster
|
||||||
res.StateEvents = nil
|
res.StateEvents = nil
|
||||||
|
|
@ -835,6 +935,7 @@ func (t *txnReq) lookupStateAfterEventLocally(ctx context.Context, roomID, event
|
||||||
var authEvents []*gomatrixserverlib.Event
|
var authEvents []*gomatrixserverlib.Event
|
||||||
missingAuthEvents := map[string]bool{}
|
missingAuthEvents := map[string]bool{}
|
||||||
for _, ev := range stateEvents {
|
for _, ev := range stateEvents {
|
||||||
|
t.haveEventsMutex.Lock()
|
||||||
for _, ae := range ev.AuthEventIDs() {
|
for _, ae := range ev.AuthEventIDs() {
|
||||||
if aev, ok := t.haveEvents[ae]; ok {
|
if aev, ok := t.haveEvents[ae]; ok {
|
||||||
authEvents = append(authEvents, aev.Unwrap())
|
authEvents = append(authEvents, aev.Unwrap())
|
||||||
|
|
@ -842,6 +943,7 @@ func (t *txnReq) lookupStateAfterEventLocally(ctx context.Context, roomID, event
|
||||||
missingAuthEvents[ae] = true
|
missingAuthEvents[ae] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.haveEventsMutex.Unlock()
|
||||||
}
|
}
|
||||||
// QueryStateAfterEvents does not return the auth events, so fetch them now. We know the roomserver has them else it wouldn't
|
// QueryStateAfterEvents does not return the auth events, so fetch them now. We know the roomserver has them else it wouldn't
|
||||||
// have stored the event.
|
// have stored the event.
|
||||||
|
|
@ -858,8 +960,9 @@ func (t *txnReq) lookupStateAfterEventLocally(ctx context.Context, roomID, event
|
||||||
if err = t.rsAPI.QueryEventsByID(ctx, &queryReq, &queryRes); err != nil {
|
if err = t.rsAPI.QueryEventsByID(ctx, &queryReq, &queryRes); err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
for i := range queryRes.Events {
|
for i, ev := range queryRes.Events {
|
||||||
authEvents = append(authEvents, t.cacheAndReturn(queryRes.Events[i]).Unwrap())
|
authEvents = append(authEvents, t.cacheAndReturn(queryRes.Events[i]).Unwrap())
|
||||||
|
t.hadEvent(ev.EventID(), true)
|
||||||
}
|
}
|
||||||
queryRes.Events = nil
|
queryRes.Events = nil
|
||||||
}
|
}
|
||||||
|
|
@ -934,12 +1037,13 @@ func (t *txnReq) getMissingEvents(ctx context.Context, e *gomatrixserverlib.Even
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
latestEvents := make([]string, len(res.LatestEvents))
|
latestEvents := make([]string, len(res.LatestEvents))
|
||||||
for i := range res.LatestEvents {
|
for i, ev := range res.LatestEvents {
|
||||||
latestEvents[i] = res.LatestEvents[i].EventID
|
latestEvents[i] = res.LatestEvents[i].EventID
|
||||||
|
t.hadEvent(ev.EventID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var missingResp *gomatrixserverlib.RespMissingEvents
|
var missingResp *gomatrixserverlib.RespMissingEvents
|
||||||
servers := t.getServers(ctx, e.RoomID())
|
servers := t.getServers(ctx, e.RoomID(), e)
|
||||||
for _, server := range servers {
|
for _, server := range servers {
|
||||||
var m gomatrixserverlib.RespMissingEvents
|
var m gomatrixserverlib.RespMissingEvents
|
||||||
if m, err = t.federation.LookupMissingEvents(ctx, server, e.RoomID(), gomatrixserverlib.MissingEvents{
|
if m, err = t.federation.LookupMissingEvents(ctx, server, e.RoomID(), gomatrixserverlib.MissingEvents{
|
||||||
|
|
@ -953,6 +1057,9 @@ func (t *txnReq) getMissingEvents(ctx context.Context, e *gomatrixserverlib.Even
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
logger.WithError(err).Errorf("%s pushed us an event but %q did not respond to /get_missing_events", t.Origin, server)
|
logger.WithError(err).Errorf("%s pushed us an event but %q did not respond to /get_missing_events", t.Origin, server)
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -980,6 +1087,12 @@ func (t *txnReq) getMissingEvents(ctx context.Context, e *gomatrixserverlib.Even
|
||||||
// For now, we do not allow Case B, so reject the event.
|
// For now, we do not allow Case B, so reject the event.
|
||||||
logger.Infof("get_missing_events returned %d events", len(missingResp.Events))
|
logger.Infof("get_missing_events returned %d events", len(missingResp.Events))
|
||||||
|
|
||||||
|
// Make sure events from the missingResp are using the cache - missing events
|
||||||
|
// will be added and duplicates will be removed.
|
||||||
|
for i, ev := range missingResp.Events {
|
||||||
|
missingResp.Events[i] = t.cacheAndReturn(ev.Headered(roomVersion)).Unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
// topologically sort and sanity check that we are making forward progress
|
// topologically sort and sanity check that we are making forward progress
|
||||||
newEvents = gomatrixserverlib.ReverseTopologicalOrdering(missingResp.Events, gomatrixserverlib.TopologicalOrderByPrevEvents)
|
newEvents = gomatrixserverlib.ReverseTopologicalOrdering(missingResp.Events, gomatrixserverlib.TopologicalOrderByPrevEvents)
|
||||||
shouldHaveSomeEventIDs := e.PrevEventIDs()
|
shouldHaveSomeEventIDs := e.PrevEventIDs()
|
||||||
|
|
@ -1018,6 +1131,14 @@ func (t *txnReq) lookupMissingStateViaState(ctx context.Context, roomID, eventID
|
||||||
if err := state.Check(ctx, t.keys, nil); err != nil {
|
if err := state.Check(ctx, t.keys, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Cache the results of this state lookup and deduplicate anything we already
|
||||||
|
// have in the cache, freeing up memory.
|
||||||
|
for i, ev := range state.AuthEvents {
|
||||||
|
state.AuthEvents[i] = t.cacheAndReturn(ev.Headered(roomVersion)).Unwrap()
|
||||||
|
}
|
||||||
|
for i, ev := range state.StateEvents {
|
||||||
|
state.StateEvents[i] = t.cacheAndReturn(ev.Headered(roomVersion)).Unwrap()
|
||||||
|
}
|
||||||
return &state, nil
|
return &state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1033,6 +1154,7 @@ func (t *txnReq) lookupMissingStateViaStateIDs(ctx context.Context, roomID, even
|
||||||
wantIDs := append(stateIDs.StateEventIDs, stateIDs.AuthEventIDs...)
|
wantIDs := append(stateIDs.StateEventIDs, stateIDs.AuthEventIDs...)
|
||||||
missing := make(map[string]bool)
|
missing := make(map[string]bool)
|
||||||
var missingEventList []string
|
var missingEventList []string
|
||||||
|
t.haveEventsMutex.Lock()
|
||||||
for _, sid := range wantIDs {
|
for _, sid := range wantIDs {
|
||||||
if _, ok := t.haveEvents[sid]; !ok {
|
if _, ok := t.haveEvents[sid]; !ok {
|
||||||
if !missing[sid] {
|
if !missing[sid] {
|
||||||
|
|
@ -1041,6 +1163,7 @@ func (t *txnReq) lookupMissingStateViaStateIDs(ctx context.Context, roomID, even
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
t.haveEventsMutex.Unlock()
|
||||||
|
|
||||||
// fetch as many as we can from the roomserver
|
// fetch as many as we can from the roomserver
|
||||||
queryReq := api.QueryEventsByIDRequest{
|
queryReq := api.QueryEventsByIDRequest{
|
||||||
|
|
@ -1050,9 +1173,10 @@ func (t *txnReq) lookupMissingStateViaStateIDs(ctx context.Context, roomID, even
|
||||||
if err = t.rsAPI.QueryEventsByID(ctx, &queryReq, &queryRes); err != nil {
|
if err = t.rsAPI.QueryEventsByID(ctx, &queryReq, &queryRes); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for i := range queryRes.Events {
|
for i, ev := range queryRes.Events {
|
||||||
|
queryRes.Events[i] = t.cacheAndReturn(queryRes.Events[i])
|
||||||
|
t.hadEvent(ev.EventID(), true)
|
||||||
evID := queryRes.Events[i].EventID()
|
evID := queryRes.Events[i].EventID()
|
||||||
t.cacheAndReturn(queryRes.Events[i])
|
|
||||||
if missing[evID] {
|
if missing[evID] {
|
||||||
delete(missing, evID)
|
delete(missing, evID)
|
||||||
}
|
}
|
||||||
|
|
@ -1153,6 +1277,9 @@ func (t *txnReq) lookupMissingStateViaStateIDs(ctx context.Context, roomID, even
|
||||||
|
|
||||||
func (t *txnReq) createRespStateFromStateIDs(stateIDs gomatrixserverlib.RespStateIDs) (
|
func (t *txnReq) createRespStateFromStateIDs(stateIDs gomatrixserverlib.RespStateIDs) (
|
||||||
*gomatrixserverlib.RespState, error) { // nolint:unparam
|
*gomatrixserverlib.RespState, error) { // nolint:unparam
|
||||||
|
t.haveEventsMutex.Lock()
|
||||||
|
defer t.haveEventsMutex.Unlock()
|
||||||
|
|
||||||
// create a RespState response using the response to /state_ids as a guide
|
// create a RespState response using the response to /state_ids as a guide
|
||||||
respState := gomatrixserverlib.RespState{}
|
respState := gomatrixserverlib.RespState{}
|
||||||
|
|
||||||
|
|
@ -1193,11 +1320,14 @@ func (t *txnReq) lookupEvent(ctx context.Context, roomVersion gomatrixserverlib.
|
||||||
}
|
}
|
||||||
var event *gomatrixserverlib.Event
|
var event *gomatrixserverlib.Event
|
||||||
found := false
|
found := false
|
||||||
servers := t.getServers(ctx, roomID)
|
servers := t.getServers(ctx, roomID, nil)
|
||||||
for _, serverName := range servers {
|
for _, serverName := range servers {
|
||||||
txn, err := t.federation.GetEvent(ctx, serverName, missingEventID)
|
txn, err := t.federation.GetEvent(ctx, serverName, missingEventID)
|
||||||
if err != nil || len(txn.PDUs) == 0 {
|
if err != nil || len(txn.PDUs) == 0 {
|
||||||
util.GetLogger(ctx).WithError(err).WithField("event_id", missingEventID).Warn("Failed to get missing /event for event ID")
|
util.GetLogger(ctx).WithError(err).WithField("event_id", missingEventID).Warn("Failed to get missing /event for event ID")
|
||||||
|
if errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
break
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
event, err = gomatrixserverlib.NewEventFromUntrustedJSON(txn.PDUs[0], roomVersion)
|
event, err = gomatrixserverlib.NewEventFromUntrustedJSON(txn.PDUs[0], roomVersion)
|
||||||
|
|
@ -1216,9 +1346,5 @@ func (t *txnReq) lookupEvent(ctx context.Context, roomVersion gomatrixserverlib.
|
||||||
util.GetLogger(ctx).WithError(err).Warnf("Transaction: Couldn't validate signature of event %q", event.EventID())
|
util.GetLogger(ctx).WithError(err).Warnf("Transaction: Couldn't validate signature of event %q", event.EventID())
|
||||||
return nil, verifySigError{event.EventID(), err}
|
return nil, verifySigError{event.EventID(), err}
|
||||||
}
|
}
|
||||||
h := event.Headered(roomVersion)
|
return t.cacheAndReturn(event.Headered(roomVersion)), nil
|
||||||
t.newEventsMutex.Lock()
|
|
||||||
t.newEvents[h.EventID()] = true
|
|
||||||
t.newEventsMutex.Unlock()
|
|
||||||
return h, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,86 +158,15 @@ func (t *testRoomserverAPI) QueryEventsByID(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Query the membership event for an user for a room.
|
|
||||||
func (t *testRoomserverAPI) QueryMembershipForUser(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryMembershipForUserRequest,
|
|
||||||
response *api.QueryMembershipForUserResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryPublishedRooms(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryPublishedRoomsRequest,
|
|
||||||
response *api.QueryPublishedRoomsResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query a list of membership events for a room
|
|
||||||
func (t *testRoomserverAPI) QueryMembershipsForRoom(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryMembershipsForRoomRequest,
|
|
||||||
response *api.QueryMembershipsForRoomResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query if a server is joined to a room
|
// Query if a server is joined to a room
|
||||||
func (t *testRoomserverAPI) QueryServerJoinedToRoom(
|
func (t *testRoomserverAPI) QueryServerJoinedToRoom(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
request *api.QueryServerJoinedToRoomRequest,
|
request *api.QueryServerJoinedToRoomRequest,
|
||||||
response *api.QueryServerJoinedToRoomResponse,
|
response *api.QueryServerJoinedToRoomResponse,
|
||||||
) error {
|
) error {
|
||||||
return fmt.Errorf("not implemented")
|
response.RoomExists = true
|
||||||
}
|
response.IsInRoom = true
|
||||||
|
return nil
|
||||||
// Query whether a server is allowed to see an event
|
|
||||||
func (t *testRoomserverAPI) QueryServerAllowedToSeeEvent(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryServerAllowedToSeeEventRequest,
|
|
||||||
response *api.QueryServerAllowedToSeeEventResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query missing events for a room from roomserver
|
|
||||||
func (t *testRoomserverAPI) QueryMissingEvents(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryMissingEventsRequest,
|
|
||||||
response *api.QueryMissingEventsResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query to get state and auth chain for a (potentially hypothetical) event.
|
|
||||||
// Takes lists of PrevEventIDs and AuthEventsIDs and uses them to calculate
|
|
||||||
// the state and auth chain to return.
|
|
||||||
func (t *testRoomserverAPI) QueryStateAndAuthChain(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryStateAndAuthChainRequest,
|
|
||||||
response *api.QueryStateAndAuthChainResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query a given amount (or less) of events prior to a given set of events.
|
|
||||||
func (t *testRoomserverAPI) PerformBackfill(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.PerformBackfillRequest,
|
|
||||||
response *api.PerformBackfillResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Asks for the default room version as preferred by the server.
|
|
||||||
func (t *testRoomserverAPI) QueryRoomVersionCapabilities(
|
|
||||||
ctx context.Context,
|
|
||||||
request *api.QueryRoomVersionCapabilitiesRequest,
|
|
||||||
response *api.QueryRoomVersionCapabilitiesResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Asks for the room version for a given room.
|
// Asks for the room version for a given room.
|
||||||
|
|
@ -250,72 +179,10 @@ func (t *testRoomserverAPI) QueryRoomVersionForRoom(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set a room alias
|
func (t *testRoomserverAPI) QueryServerBannedFromRoom(
|
||||||
func (t *testRoomserverAPI) SetRoomAlias(
|
ctx context.Context, req *api.QueryServerBannedFromRoomRequest, res *api.QueryServerBannedFromRoomResponse,
|
||||||
ctx context.Context,
|
|
||||||
req *api.SetRoomAliasRequest,
|
|
||||||
response *api.SetRoomAliasResponse,
|
|
||||||
) error {
|
) error {
|
||||||
return fmt.Errorf("not implemented")
|
res.Banned = false
|
||||||
}
|
|
||||||
|
|
||||||
// Get the room ID for an alias
|
|
||||||
func (t *testRoomserverAPI) GetRoomIDForAlias(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.GetRoomIDForAliasRequest,
|
|
||||||
response *api.GetRoomIDForAliasResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get all known aliases for a room ID
|
|
||||||
func (t *testRoomserverAPI) GetAliasesForRoomID(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.GetAliasesForRoomIDRequest,
|
|
||||||
response *api.GetAliasesForRoomIDResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the user ID of the creator of an alias
|
|
||||||
func (t *testRoomserverAPI) GetCreatorIDForAlias(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.GetCreatorIDForAliasRequest,
|
|
||||||
response *api.GetCreatorIDForAliasResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a room alias
|
|
||||||
func (t *testRoomserverAPI) RemoveRoomAlias(
|
|
||||||
ctx context.Context,
|
|
||||||
req *api.RemoveRoomAliasRequest,
|
|
||||||
response *api.RemoveRoomAliasResponse,
|
|
||||||
) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryCurrentState(ctx context.Context, req *api.QueryCurrentStateRequest, res *api.QueryCurrentStateResponse) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryRoomsForUser(ctx context.Context, req *api.QueryRoomsForUserRequest, res *api.QueryRoomsForUserResponse) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryBulkStateContent(ctx context.Context, req *api.QueryBulkStateContentRequest, res *api.QueryBulkStateContentResponse) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QuerySharedUsers(ctx context.Context, req *api.QuerySharedUsersRequest, res *api.QuerySharedUsersResponse) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryKnownUsers(ctx context.Context, req *api.QueryKnownUsersRequest, res *api.QueryKnownUsersResponse) error {
|
|
||||||
return fmt.Errorf("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *testRoomserverAPI) QueryServerBannedFromRoom(ctx context.Context, req *api.QueryServerBannedFromRoomRequest, res *api.QueryServerBannedFromRoomResponse) error {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,7 +237,7 @@ func mustCreateTransaction(rsAPI api.RoomserverInternalAPI, fedClient txnFederat
|
||||||
keys: &test.NopJSONVerifier{},
|
keys: &test.NopJSONVerifier{},
|
||||||
federation: fedClient,
|
federation: fedClient,
|
||||||
haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent),
|
haveEvents: make(map[string]*gomatrixserverlib.HeaderedEvent),
|
||||||
newEvents: make(map[string]bool),
|
hadEvents: make(map[string]bool),
|
||||||
roomsMu: internal.NewMutexByRoom(),
|
roomsMu: internal.NewMutexByRoom(),
|
||||||
}
|
}
|
||||||
t.PDUs = pdus
|
t.PDUs = pdus
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,6 @@ type FederationClient interface {
|
||||||
ClaimKeys(ctx context.Context, s gomatrixserverlib.ServerName, oneTimeKeys map[string]map[string]string) (res gomatrixserverlib.RespClaimKeys, err error)
|
ClaimKeys(ctx context.Context, s gomatrixserverlib.ServerName, oneTimeKeys map[string]map[string]string) (res gomatrixserverlib.RespClaimKeys, err error)
|
||||||
QueryKeys(ctx context.Context, s gomatrixserverlib.ServerName, keys map[string][]string) (res gomatrixserverlib.RespQueryKeys, err error)
|
QueryKeys(ctx context.Context, s gomatrixserverlib.ServerName, keys map[string][]string) (res gomatrixserverlib.RespQueryKeys, err error)
|
||||||
GetEvent(ctx context.Context, s gomatrixserverlib.ServerName, eventID string) (res gomatrixserverlib.Transaction, err error)
|
GetEvent(ctx context.Context, s gomatrixserverlib.ServerName, eventID string) (res gomatrixserverlib.Transaction, err error)
|
||||||
GetServerKeys(ctx context.Context, matrixServer gomatrixserverlib.ServerName) (gomatrixserverlib.ServerKeys, error)
|
|
||||||
MSC2836EventRelationships(ctx context.Context, dst gomatrixserverlib.ServerName, r gomatrixserverlib.MSC2836EventRelationshipsRequest, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.MSC2836EventRelationshipsResponse, err error)
|
MSC2836EventRelationships(ctx context.Context, dst gomatrixserverlib.ServerName, r gomatrixserverlib.MSC2836EventRelationshipsRequest, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.MSC2836EventRelationshipsResponse, err error)
|
||||||
MSC2946Spaces(ctx context.Context, dst gomatrixserverlib.ServerName, roomID string, r gomatrixserverlib.MSC2946SpacesRequest) (res gomatrixserverlib.MSC2946SpacesResponse, err error)
|
MSC2946Spaces(ctx context.Context, dst gomatrixserverlib.ServerName, roomID string, r gomatrixserverlib.MSC2946SpacesRequest) (res gomatrixserverlib.MSC2946SpacesResponse, err error)
|
||||||
LookupServerKeys(ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp) ([]gomatrixserverlib.ServerKeys, error)
|
LookupServerKeys(ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp) ([]gomatrixserverlib.ServerKeys, error)
|
||||||
|
|
@ -41,6 +40,8 @@ func (e *FederationClientError) Error() string {
|
||||||
type FederationSenderInternalAPI interface {
|
type FederationSenderInternalAPI interface {
|
||||||
FederationClient
|
FederationClient
|
||||||
|
|
||||||
|
QueryServerKeys(ctx context.Context, request *QueryServerKeysRequest, response *QueryServerKeysResponse) error
|
||||||
|
|
||||||
// PerformDirectoryLookup looks up a remote room ID from a room alias.
|
// PerformDirectoryLookup looks up a remote room ID from a room alias.
|
||||||
PerformDirectoryLookup(
|
PerformDirectoryLookup(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
|
|
@ -94,6 +95,25 @@ type FederationSenderInternalAPI interface {
|
||||||
) error
|
) error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type QueryServerKeysRequest struct {
|
||||||
|
ServerName gomatrixserverlib.ServerName
|
||||||
|
KeyIDToCriteria map[gomatrixserverlib.KeyID]gomatrixserverlib.PublicKeyNotaryQueryCriteria
|
||||||
|
}
|
||||||
|
|
||||||
|
func (q *QueryServerKeysRequest) KeyIDs() []gomatrixserverlib.KeyID {
|
||||||
|
kids := make([]gomatrixserverlib.KeyID, len(q.KeyIDToCriteria))
|
||||||
|
i := 0
|
||||||
|
for keyID := range q.KeyIDToCriteria {
|
||||||
|
kids[i] = keyID
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return kids
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryServerKeysResponse struct {
|
||||||
|
ServerKeys []gomatrixserverlib.ServerKeys
|
||||||
|
}
|
||||||
|
|
||||||
type PerformDirectoryLookupRequest struct {
|
type PerformDirectoryLookupRequest struct {
|
||||||
RoomAlias string `json:"room_alias"`
|
RoomAlias string `json:"room_alias"`
|
||||||
ServerName gomatrixserverlib.ServerName `json:"server_name"`
|
ServerName gomatrixserverlib.ServerName `json:"server_name"`
|
||||||
|
|
|
||||||
|
|
@ -202,20 +202,6 @@ func (a *FederationSenderInternalAPI) GetEvent(
|
||||||
return ires.(gomatrixserverlib.Transaction), nil
|
return ires.(gomatrixserverlib.Transaction), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *FederationSenderInternalAPI) GetServerKeys(
|
|
||||||
ctx context.Context, s gomatrixserverlib.ServerName,
|
|
||||||
) (gomatrixserverlib.ServerKeys, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
|
||||||
defer cancel()
|
|
||||||
ires, err := a.doRequest(s, func() (interface{}, error) {
|
|
||||||
return a.federation.GetServerKeys(ctx, s)
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return gomatrixserverlib.ServerKeys{}, err
|
|
||||||
}
|
|
||||||
return ires.(gomatrixserverlib.ServerKeys), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *FederationSenderInternalAPI) LookupServerKeys(
|
func (a *FederationSenderInternalAPI) LookupServerKeys(
|
||||||
ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp,
|
ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp,
|
||||||
) ([]gomatrixserverlib.ServerKeys, error) {
|
) ([]gomatrixserverlib.ServerKeys, error) {
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,12 @@ package internal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/federationsender/api"
|
"github.com/matrix-org/dendrite/federationsender/api"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
"github.com/matrix-org/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
// QueryJoinedHostServerNamesInRoom implements api.FederationSenderInternalAPI
|
// QueryJoinedHostServerNamesInRoom implements api.FederationSenderInternalAPI
|
||||||
|
|
@ -20,3 +24,74 @@ func (f *FederationSenderInternalAPI) QueryJoinedHostServerNamesInRoom(
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *FederationSenderInternalAPI) fetchServerKeysDirectly(ctx context.Context, serverName gomatrixserverlib.ServerName) (*gomatrixserverlib.ServerKeys, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||||
|
defer cancel()
|
||||||
|
ires, err := a.doRequest(serverName, func() (interface{}, error) {
|
||||||
|
return a.federation.GetServerKeys(ctx, serverName)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sks := ires.(gomatrixserverlib.ServerKeys)
|
||||||
|
return &sks, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FederationSenderInternalAPI) fetchServerKeysFromCache(
|
||||||
|
ctx context.Context, req *api.QueryServerKeysRequest,
|
||||||
|
) ([]gomatrixserverlib.ServerKeys, error) {
|
||||||
|
var results []gomatrixserverlib.ServerKeys
|
||||||
|
for keyID, criteria := range req.KeyIDToCriteria {
|
||||||
|
serverKeysResponses, _ := a.db.GetNotaryKeys(ctx, req.ServerName, []gomatrixserverlib.KeyID{keyID})
|
||||||
|
if len(serverKeysResponses) == 0 {
|
||||||
|
return nil, fmt.Errorf("failed to find server key response for key ID %s", keyID)
|
||||||
|
}
|
||||||
|
// we should only get 1 result as we only gave 1 key ID
|
||||||
|
sk := serverKeysResponses[0]
|
||||||
|
util.GetLogger(ctx).Infof("fetchServerKeysFromCache: minvalid:%v keys: %+v", criteria.MinimumValidUntilTS, sk)
|
||||||
|
if criteria.MinimumValidUntilTS != 0 {
|
||||||
|
// check if it's still valid. if they have the same value that's also valid
|
||||||
|
if sk.ValidUntilTS < criteria.MinimumValidUntilTS {
|
||||||
|
return nil, fmt.Errorf(
|
||||||
|
"found server response for key ID %s but it is no longer valid, min: %v valid_until: %v",
|
||||||
|
keyID, criteria.MinimumValidUntilTS, sk.ValidUntilTS,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
results = append(results, sk)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *FederationSenderInternalAPI) QueryServerKeys(
|
||||||
|
ctx context.Context, req *api.QueryServerKeysRequest, res *api.QueryServerKeysResponse,
|
||||||
|
) error {
|
||||||
|
// attempt to satisfy the entire request from the cache first
|
||||||
|
results, err := a.fetchServerKeysFromCache(ctx, req)
|
||||||
|
if err == nil {
|
||||||
|
// satisfied entirely from cache, return it
|
||||||
|
res.ServerKeys = results
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
util.GetLogger(ctx).WithField("server", req.ServerName).WithError(err).Warn("notary: failed to satisfy keys request entirely from cache, hitting direct")
|
||||||
|
|
||||||
|
serverKeys, err := a.fetchServerKeysDirectly(ctx, req.ServerName)
|
||||||
|
if err != nil {
|
||||||
|
// try to load as much as we can from the cache in a best effort basis
|
||||||
|
util.GetLogger(ctx).WithField("server", req.ServerName).WithError(err).Warn("notary: failed to ask server for keys, returning best effort keys")
|
||||||
|
serverKeysResponses, dbErr := a.db.GetNotaryKeys(ctx, req.ServerName, req.KeyIDs())
|
||||||
|
if dbErr != nil {
|
||||||
|
return fmt.Errorf("notary: server returned %s, and db returned %s", err, dbErr)
|
||||||
|
}
|
||||||
|
res.ServerKeys = serverKeysResponses
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// cache it!
|
||||||
|
if err = a.db.UpdateNotaryKeys(context.Background(), req.ServerName, *serverKeys); err != nil {
|
||||||
|
// non-fatal, still return the response
|
||||||
|
util.GetLogger(ctx).WithError(err).Warn("failed to UpdateNotaryKeys")
|
||||||
|
}
|
||||||
|
res.ServerKeys = []gomatrixserverlib.ServerKeys{*serverKeys}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import (
|
||||||
// HTTP paths for the internal HTTP API
|
// HTTP paths for the internal HTTP API
|
||||||
const (
|
const (
|
||||||
FederationSenderQueryJoinedHostServerNamesInRoomPath = "/federationsender/queryJoinedHostServerNamesInRoom"
|
FederationSenderQueryJoinedHostServerNamesInRoomPath = "/federationsender/queryJoinedHostServerNamesInRoom"
|
||||||
|
FederationSenderQueryServerKeysPath = "/federationsender/queryServerKeys"
|
||||||
|
|
||||||
FederationSenderPerformDirectoryLookupRequestPath = "/federationsender/performDirectoryLookup"
|
FederationSenderPerformDirectoryLookupRequestPath = "/federationsender/performDirectoryLookup"
|
||||||
FederationSenderPerformJoinRequestPath = "/federationsender/performJoinRequest"
|
FederationSenderPerformJoinRequestPath = "/federationsender/performJoinRequest"
|
||||||
|
|
@ -31,7 +32,6 @@ const (
|
||||||
FederationSenderLookupStatePath = "/federationsender/client/lookupState"
|
FederationSenderLookupStatePath = "/federationsender/client/lookupState"
|
||||||
FederationSenderLookupStateIDsPath = "/federationsender/client/lookupStateIDs"
|
FederationSenderLookupStateIDsPath = "/federationsender/client/lookupStateIDs"
|
||||||
FederationSenderGetEventPath = "/federationsender/client/getEvent"
|
FederationSenderGetEventPath = "/federationsender/client/getEvent"
|
||||||
FederationSenderGetServerKeysPath = "/federationsender/client/getServerKeys"
|
|
||||||
FederationSenderLookupServerKeysPath = "/federationsender/client/lookupServerKeys"
|
FederationSenderLookupServerKeysPath = "/federationsender/client/lookupServerKeys"
|
||||||
FederationSenderEventRelationshipsPath = "/federationsender/client/msc2836eventRelationships"
|
FederationSenderEventRelationshipsPath = "/federationsender/client/msc2836eventRelationships"
|
||||||
FederationSenderSpacesSummaryPath = "/federationsender/client/msc2946spacesSummary"
|
FederationSenderSpacesSummaryPath = "/federationsender/client/msc2946spacesSummary"
|
||||||
|
|
@ -377,31 +377,14 @@ func (h *httpFederationSenderInternalAPI) GetEvent(
|
||||||
return *response.Res, nil
|
return *response.Res, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type getServerKeys struct {
|
func (h *httpFederationSenderInternalAPI) QueryServerKeys(
|
||||||
S gomatrixserverlib.ServerName
|
ctx context.Context, req *api.QueryServerKeysRequest, res *api.QueryServerKeysResponse,
|
||||||
ServerKeys gomatrixserverlib.ServerKeys
|
) error {
|
||||||
Err *api.FederationClientError
|
span, ctx := opentracing.StartSpanFromContext(ctx, "QueryServerKeys")
|
||||||
}
|
|
||||||
|
|
||||||
func (h *httpFederationSenderInternalAPI) GetServerKeys(
|
|
||||||
ctx context.Context, s gomatrixserverlib.ServerName,
|
|
||||||
) (gomatrixserverlib.ServerKeys, error) {
|
|
||||||
span, ctx := opentracing.StartSpanFromContext(ctx, "GetServerKeys")
|
|
||||||
defer span.Finish()
|
defer span.Finish()
|
||||||
|
|
||||||
request := getServerKeys{
|
apiURL := h.federationSenderURL + FederationSenderQueryServerKeysPath
|
||||||
S: s,
|
return httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res)
|
||||||
}
|
|
||||||
var response getServerKeys
|
|
||||||
apiURL := h.federationSenderURL + FederationSenderGetServerKeysPath
|
|
||||||
err := httputil.PostJSON(ctx, span, h.httpClient, apiURL, &request, &response)
|
|
||||||
if err != nil {
|
|
||||||
return gomatrixserverlib.ServerKeys{}, err
|
|
||||||
}
|
|
||||||
if response.Err != nil {
|
|
||||||
return gomatrixserverlib.ServerKeys{}, response.Err
|
|
||||||
}
|
|
||||||
return response.ServerKeys, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type lookupServerKeys struct {
|
type lookupServerKeys struct {
|
||||||
|
|
|
||||||
|
|
@ -264,25 +264,17 @@ func AddRoutes(intAPI api.FederationSenderInternalAPI, internalAPIMux *mux.Route
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
internalAPIMux.Handle(
|
internalAPIMux.Handle(
|
||||||
FederationSenderGetServerKeysPath,
|
FederationSenderQueryServerKeysPath,
|
||||||
httputil.MakeInternalAPI("GetServerKeys", func(req *http.Request) util.JSONResponse {
|
httputil.MakeInternalAPI("QueryServerKeys", func(req *http.Request) util.JSONResponse {
|
||||||
var request getServerKeys
|
var request api.QueryServerKeysRequest
|
||||||
|
var response api.QueryServerKeysResponse
|
||||||
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
if err := json.NewDecoder(req.Body).Decode(&request); err != nil {
|
||||||
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
return util.MessageResponse(http.StatusBadRequest, err.Error())
|
||||||
}
|
}
|
||||||
res, err := intAPI.GetServerKeys(req.Context(), request.S)
|
if err := intAPI.QueryServerKeys(req.Context(), &request, &response); err != nil {
|
||||||
if err != nil {
|
return util.ErrorResponse(err)
|
||||||
ferr, ok := err.(*api.FederationClientError)
|
|
||||||
if ok {
|
|
||||||
request.Err = ferr
|
|
||||||
} else {
|
|
||||||
request.Err = &api.FederationClientError{
|
|
||||||
Err: err.Error(),
|
|
||||||
}
|
}
|
||||||
}
|
return util.JSONResponse{Code: http.StatusOK, JSON: &response}
|
||||||
}
|
|
||||||
request.ServerKeys = res
|
|
||||||
return util.JSONResponse{Code: http.StatusOK, JSON: request}
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
internalAPIMux.Handle(
|
internalAPIMux.Handle(
|
||||||
|
|
|
||||||
|
|
@ -66,4 +66,10 @@ type Database interface {
|
||||||
RenewInboundPeek(ctx context.Context, serverName gomatrixserverlib.ServerName, roomID, peekID string, renewalInterval int64) error
|
RenewInboundPeek(ctx context.Context, serverName gomatrixserverlib.ServerName, roomID, peekID string, renewalInterval int64) error
|
||||||
GetInboundPeek(ctx context.Context, serverName gomatrixserverlib.ServerName, roomID, peekID string) (*types.InboundPeek, error)
|
GetInboundPeek(ctx context.Context, serverName gomatrixserverlib.ServerName, roomID, peekID string) (*types.InboundPeek, error)
|
||||||
GetInboundPeeks(ctx context.Context, roomID string) ([]types.InboundPeek, error)
|
GetInboundPeeks(ctx context.Context, roomID string) ([]types.InboundPeek, error)
|
||||||
|
|
||||||
|
// Update the notary with the given server keys from the given server name.
|
||||||
|
UpdateNotaryKeys(ctx context.Context, serverName gomatrixserverlib.ServerName, serverKeys gomatrixserverlib.ServerKeys) error
|
||||||
|
// Query the notary for the server keys for the given server. If `optKeyIDs` is not empty, multiple server keys may be returned (between 1 - len(optKeyIDs))
|
||||||
|
// such that the combination of all server keys will include all the `optKeyIDs`.
|
||||||
|
GetNotaryKeys(ctx context.Context, serverName gomatrixserverlib.ServerName, optKeyIDs []gomatrixserverlib.KeyID) ([]gomatrixserverlib.ServerKeys, error)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
// 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 postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const notaryServerKeysJSONSchema = `
|
||||||
|
CREATE SEQUENCE IF NOT EXISTS federationsender_notary_server_keys_json_pkey;
|
||||||
|
CREATE TABLE IF NOT EXISTS federationsender_notary_server_keys_json (
|
||||||
|
notary_id BIGINT PRIMARY KEY NOT NULL DEFAULT nextval('federationsender_notary_server_keys_json_pkey'),
|
||||||
|
response_json TEXT NOT NULL,
|
||||||
|
server_name TEXT NOT NULL,
|
||||||
|
valid_until BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const insertServerKeysJSONSQL = "" +
|
||||||
|
"INSERT INTO federationsender_notary_server_keys_json (response_json, server_name, valid_until) VALUES ($1, $2, $3)" +
|
||||||
|
" RETURNING notary_id"
|
||||||
|
|
||||||
|
type notaryServerKeysStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
insertServerKeysJSONStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresNotaryServerKeysTable(db *sql.DB) (s *notaryServerKeysStatements, err error) {
|
||||||
|
s = ¬aryServerKeysStatements{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
_, err = db.Exec(notaryServerKeysJSONSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.insertServerKeysJSONStmt, err = db.Prepare(insertServerKeysJSONSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysStatements) InsertJSONResponse(
|
||||||
|
ctx context.Context, txn *sql.Tx, keyQueryResponseJSON gomatrixserverlib.ServerKeys, serverName gomatrixserverlib.ServerName, validUntil gomatrixserverlib.Timestamp,
|
||||||
|
) (tables.NotaryID, error) {
|
||||||
|
var notaryID tables.NotaryID
|
||||||
|
return notaryID, txn.Stmt(s.insertServerKeysJSONStmt).QueryRowContext(ctx, string(keyQueryResponseJSON.Raw), serverName, validUntil).Scan(¬aryID)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
// 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 postgres
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/lib/pq"
|
||||||
|
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const notaryServerKeysMetadataSchema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS federationsender_notary_server_keys_metadata (
|
||||||
|
notary_id BIGINT NOT NULL,
|
||||||
|
server_name TEXT NOT NULL,
|
||||||
|
key_id TEXT NOT NULL,
|
||||||
|
UNIQUE (server_name, key_id)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const upsertServerKeysSQL = "" +
|
||||||
|
"INSERT INTO federationsender_notary_server_keys_metadata (notary_id, server_name, key_id) VALUES ($1, $2, $3)" +
|
||||||
|
" ON CONFLICT (server_name, key_id) DO UPDATE SET notary_id = $1"
|
||||||
|
|
||||||
|
// for a given (server_name, key_id), find the existing notary ID and valid until. Used to check if we will replace it
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyMetadataSQL = `
|
||||||
|
SELECT federationsender_notary_server_keys_metadata.notary_id, valid_until FROM federationsender_notary_server_keys_json
|
||||||
|
JOIN federationsender_notary_server_keys_metadata ON
|
||||||
|
federationsender_notary_server_keys_metadata.notary_id = federationsender_notary_server_keys_json.notary_id
|
||||||
|
WHERE federationsender_notary_server_keys_metadata.server_name = $1 AND federationsender_notary_server_keys_metadata.key_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
// select the response which has the highest valid_until value
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyResponsesSQL = `
|
||||||
|
SELECT response_json FROM federationsender_notary_server_keys_json
|
||||||
|
WHERE server_name = $1 AND valid_until = (
|
||||||
|
SELECT MAX(valid_until) FROM federationsender_notary_server_keys_json WHERE server_name = $1
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
// select the responses which have the given key IDs
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyResponsesWithKeyIDsSQL = `
|
||||||
|
SELECT response_json FROM federationsender_notary_server_keys_json
|
||||||
|
JOIN federationsender_notary_server_keys_metadata ON
|
||||||
|
federationsender_notary_server_keys_metadata.notary_id = federationsender_notary_server_keys_json.notary_id
|
||||||
|
WHERE federationsender_notary_server_keys_json.server_name = $1 AND federationsender_notary_server_keys_metadata.key_id = ANY ($2)
|
||||||
|
GROUP BY federationsender_notary_server_keys_json.notary_id
|
||||||
|
`
|
||||||
|
|
||||||
|
// JOINs with the metadata table
|
||||||
|
const deleteUnusedServerKeysJSONSQL = `
|
||||||
|
DELETE FROM federationsender_notary_server_keys_json WHERE federationsender_notary_server_keys_json.notary_id NOT IN (
|
||||||
|
SELECT DISTINCT notary_id FROM federationsender_notary_server_keys_metadata
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type notaryServerKeysMetadataStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
upsertServerKeysStmt *sql.Stmt
|
||||||
|
selectNotaryKeyResponsesStmt *sql.Stmt
|
||||||
|
selectNotaryKeyResponsesWithKeyIDsStmt *sql.Stmt
|
||||||
|
selectNotaryKeyMetadataStmt *sql.Stmt
|
||||||
|
deleteUnusedServerKeysJSONStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresNotaryServerKeysMetadataTable(db *sql.DB) (s *notaryServerKeysMetadataStatements, err error) {
|
||||||
|
s = ¬aryServerKeysMetadataStatements{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
_, err = db.Exec(notaryServerKeysMetadataSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.upsertServerKeysStmt, err = db.Prepare(upsertServerKeysSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.selectNotaryKeyResponsesStmt, err = db.Prepare(selectNotaryKeyResponsesSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.selectNotaryKeyResponsesWithKeyIDsStmt, err = db.Prepare(selectNotaryKeyResponsesWithKeyIDsSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.selectNotaryKeyMetadataStmt, err = db.Prepare(selectNotaryKeyMetadataSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.deleteUnusedServerKeysJSONStmt, err = db.Prepare(deleteUnusedServerKeysJSONSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) UpsertKey(
|
||||||
|
ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyID gomatrixserverlib.KeyID, newNotaryID tables.NotaryID, newValidUntil gomatrixserverlib.Timestamp,
|
||||||
|
) (tables.NotaryID, error) {
|
||||||
|
notaryID := newNotaryID
|
||||||
|
// see if the existing notary ID a) exists, b) has a longer valid_until
|
||||||
|
var existingNotaryID tables.NotaryID
|
||||||
|
var existingValidUntil gomatrixserverlib.Timestamp
|
||||||
|
if err := txn.Stmt(s.selectNotaryKeyMetadataStmt).QueryRowContext(ctx, serverName, keyID).Scan(&existingNotaryID, &existingValidUntil); err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existingValidUntil.Time().After(newValidUntil.Time()) {
|
||||||
|
// the existing valid_until is valid longer, so use that.
|
||||||
|
return existingNotaryID, nil
|
||||||
|
}
|
||||||
|
// overwrite the notary_id for this (server_name, key_id) tuple
|
||||||
|
_, err := txn.Stmt(s.upsertServerKeysStmt).ExecContext(ctx, notaryID, serverName, keyID)
|
||||||
|
return notaryID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) SelectKeys(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyIDs []gomatrixserverlib.KeyID) ([]gomatrixserverlib.ServerKeys, error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
if len(keyIDs) == 0 {
|
||||||
|
rows, err = txn.Stmt(s.selectNotaryKeyResponsesStmt).QueryContext(ctx, string(serverName))
|
||||||
|
} else {
|
||||||
|
keyIDstr := make([]string, len(keyIDs))
|
||||||
|
for i := range keyIDs {
|
||||||
|
keyIDstr[i] = string(keyIDs[i])
|
||||||
|
}
|
||||||
|
rows, err = txn.Stmt(s.selectNotaryKeyResponsesWithKeyIDsStmt).QueryContext(ctx, string(serverName), pq.StringArray(keyIDstr))
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer internal.CloseAndLogIfError(ctx, rows, "selectNotaryKeyResponsesStmt close failed")
|
||||||
|
var results []gomatrixserverlib.ServerKeys
|
||||||
|
for rows.Next() {
|
||||||
|
var sk gomatrixserverlib.ServerKeys
|
||||||
|
var raw string
|
||||||
|
if err = rows.Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal([]byte(raw), &sk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, sk)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) DeleteOldJSONResponses(ctx context.Context, txn *sql.Tx) error {
|
||||||
|
_, err := txn.Stmt(s.deleteUnusedServerKeysJSONStmt).ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,7 @@ package postgres
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/federationsender/storage/postgres/deltas"
|
"github.com/matrix-org/dendrite/federationsender/storage/postgres/deltas"
|
||||||
"github.com/matrix-org/dendrite/federationsender/storage/shared"
|
"github.com/matrix-org/dendrite/federationsender/storage/shared"
|
||||||
|
|
@ -69,6 +70,14 @@ func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationS
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
notaryJSON, err := NewPostgresNotaryServerKeysTable(d.db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewPostgresNotaryServerKeysTable: %s", err)
|
||||||
|
}
|
||||||
|
notaryMetadata, err := NewPostgresNotaryServerKeysMetadataTable(d.db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewPostgresNotaryServerKeysMetadataTable: %s", err)
|
||||||
|
}
|
||||||
m := sqlutil.NewMigrations()
|
m := sqlutil.NewMigrations()
|
||||||
deltas.LoadRemoveRoomsTable(m)
|
deltas.LoadRemoveRoomsTable(m)
|
||||||
if err = m.RunDeltas(d.db, dbProperties); err != nil {
|
if err = m.RunDeltas(d.db, dbProperties); err != nil {
|
||||||
|
|
@ -85,6 +94,8 @@ func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationS
|
||||||
FederationSenderBlacklist: blacklist,
|
FederationSenderBlacklist: blacklist,
|
||||||
FederationSenderInboundPeeks: inboundPeeks,
|
FederationSenderInboundPeeks: inboundPeeks,
|
||||||
FederationSenderOutboundPeeks: outboundPeeks,
|
FederationSenderOutboundPeeks: outboundPeeks,
|
||||||
|
NotaryServerKeysJSON: notaryJSON,
|
||||||
|
NotaryServerKeysMetadata: notaryMetadata,
|
||||||
}
|
}
|
||||||
if err = d.PartitionOffsetStatements.Prepare(d.db, d.writer, "federationsender"); err != nil {
|
if err = d.PartitionOffsetStatements.Prepare(d.db, d.writer, "federationsender"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
||||||
"github.com/matrix-org/dendrite/federationsender/types"
|
"github.com/matrix-org/dendrite/federationsender/types"
|
||||||
|
|
@ -37,6 +38,8 @@ type Database struct {
|
||||||
FederationSenderBlacklist tables.FederationSenderBlacklist
|
FederationSenderBlacklist tables.FederationSenderBlacklist
|
||||||
FederationSenderOutboundPeeks tables.FederationSenderOutboundPeeks
|
FederationSenderOutboundPeeks tables.FederationSenderOutboundPeeks
|
||||||
FederationSenderInboundPeeks tables.FederationSenderInboundPeeks
|
FederationSenderInboundPeeks tables.FederationSenderInboundPeeks
|
||||||
|
NotaryServerKeysJSON tables.FederationSenderNotaryServerKeysJSON
|
||||||
|
NotaryServerKeysMetadata tables.FederationSenderNotaryServerKeysMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// An Receipt contains the NIDs of a call to GetNextTransactionPDUs/EDUs.
|
// An Receipt contains the NIDs of a call to GetNextTransactionPDUs/EDUs.
|
||||||
|
|
@ -197,3 +200,47 @@ func (d *Database) GetInboundPeek(ctx context.Context, serverName gomatrixserver
|
||||||
func (d *Database) GetInboundPeeks(ctx context.Context, roomID string) ([]types.InboundPeek, error) {
|
func (d *Database) GetInboundPeeks(ctx context.Context, roomID string) ([]types.InboundPeek, error) {
|
||||||
return d.FederationSenderInboundPeeks.SelectInboundPeeks(ctx, nil, roomID)
|
return d.FederationSenderInboundPeeks.SelectInboundPeeks(ctx, nil, roomID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Database) UpdateNotaryKeys(ctx context.Context, serverName gomatrixserverlib.ServerName, serverKeys gomatrixserverlib.ServerKeys) error {
|
||||||
|
return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error {
|
||||||
|
validUntil := serverKeys.ValidUntilTS
|
||||||
|
// Servers MUST use the lesser of this field and 7 days into the future when determining if a key is valid.
|
||||||
|
// This is to avoid a situation where an attacker publishes a key which is valid for a significant amount of
|
||||||
|
// time without a way for the homeserver owner to revoke it.
|
||||||
|
// https://spec.matrix.org/unstable/server-server-api/#querying-keys-through-another-server
|
||||||
|
weekIntoFuture := time.Now().Add(7 * 24 * time.Hour)
|
||||||
|
if weekIntoFuture.Before(validUntil.Time()) {
|
||||||
|
validUntil = gomatrixserverlib.AsTimestamp(weekIntoFuture)
|
||||||
|
}
|
||||||
|
notaryID, err := d.NotaryServerKeysJSON.InsertJSONResponse(ctx, txn, serverKeys, serverName, validUntil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// update the metadata for the keys
|
||||||
|
for keyID := range serverKeys.OldVerifyKeys {
|
||||||
|
_, err = d.NotaryServerKeysMetadata.UpsertKey(ctx, txn, serverName, keyID, notaryID, validUntil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for keyID := range serverKeys.VerifyKeys {
|
||||||
|
_, err = d.NotaryServerKeysMetadata.UpsertKey(ctx, txn, serverName, keyID, notaryID, validUntil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clean up old responses
|
||||||
|
return d.NotaryServerKeysMetadata.DeleteOldJSONResponses(ctx, txn)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Database) GetNotaryKeys(
|
||||||
|
ctx context.Context, serverName gomatrixserverlib.ServerName, optKeyIDs []gomatrixserverlib.KeyID,
|
||||||
|
) (sks []gomatrixserverlib.ServerKeys, err error) {
|
||||||
|
err = d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error {
|
||||||
|
sks, err = d.NotaryServerKeysMetadata.SelectKeys(ctx, txn, serverName, optKeyIDs)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
return sks, err
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
// 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 sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const notaryServerKeysJSONSchema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS federationsender_notary_server_keys_json (
|
||||||
|
notary_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
response_json TEXT NOT NULL,
|
||||||
|
server_name TEXT NOT NULL,
|
||||||
|
valid_until BIGINT NOT NULL
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const insertServerKeysJSONSQL = "" +
|
||||||
|
"INSERT INTO federationsender_notary_server_keys_json (response_json, server_name, valid_until) VALUES ($1, $2, $3)" +
|
||||||
|
" RETURNING notary_id"
|
||||||
|
|
||||||
|
type notaryServerKeysStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
insertServerKeysJSONStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteNotaryServerKeysTable(db *sql.DB) (s *notaryServerKeysStatements, err error) {
|
||||||
|
s = ¬aryServerKeysStatements{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
_, err = db.Exec(notaryServerKeysJSONSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.insertServerKeysJSONStmt, err = db.Prepare(insertServerKeysJSONSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysStatements) InsertJSONResponse(
|
||||||
|
ctx context.Context, txn *sql.Tx, keyQueryResponseJSON gomatrixserverlib.ServerKeys, serverName gomatrixserverlib.ServerName, validUntil gomatrixserverlib.Timestamp,
|
||||||
|
) (tables.NotaryID, error) {
|
||||||
|
var notaryID tables.NotaryID
|
||||||
|
return notaryID, txn.Stmt(s.insertServerKeysJSONStmt).QueryRowContext(ctx, string(keyQueryResponseJSON.Raw), serverName, validUntil).Scan(¬aryID)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
// 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 sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/matrix-org/dendrite/federationsender/storage/tables"
|
||||||
|
"github.com/matrix-org/dendrite/internal"
|
||||||
|
"github.com/matrix-org/dendrite/internal/sqlutil"
|
||||||
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
|
)
|
||||||
|
|
||||||
|
const notaryServerKeysMetadataSchema = `
|
||||||
|
CREATE TABLE IF NOT EXISTS federationsender_notary_server_keys_metadata (
|
||||||
|
notary_id BIGINT NOT NULL,
|
||||||
|
server_name TEXT NOT NULL,
|
||||||
|
key_id TEXT NOT NULL,
|
||||||
|
UNIQUE (server_name, key_id)
|
||||||
|
);
|
||||||
|
`
|
||||||
|
|
||||||
|
const upsertServerKeysSQL = "" +
|
||||||
|
"INSERT INTO federationsender_notary_server_keys_metadata (notary_id, server_name, key_id) VALUES ($1, $2, $3)" +
|
||||||
|
" ON CONFLICT (server_name, key_id) DO UPDATE SET notary_id = $1"
|
||||||
|
|
||||||
|
// for a given (server_name, key_id), find the existing notary ID and valid until. Used to check if we will replace it
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyMetadataSQL = `
|
||||||
|
SELECT federationsender_notary_server_keys_metadata.notary_id, valid_until FROM federationsender_notary_server_keys_json
|
||||||
|
JOIN federationsender_notary_server_keys_metadata ON
|
||||||
|
federationsender_notary_server_keys_metadata.notary_id = federationsender_notary_server_keys_json.notary_id
|
||||||
|
WHERE federationsender_notary_server_keys_metadata.server_name = $1 AND federationsender_notary_server_keys_metadata.key_id = $2
|
||||||
|
`
|
||||||
|
|
||||||
|
// select the response which has the highest valid_until value
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyResponsesSQL = `
|
||||||
|
SELECT response_json FROM federationsender_notary_server_keys_json
|
||||||
|
WHERE server_name = $1 AND valid_until = (
|
||||||
|
SELECT MAX(valid_until) FROM federationsender_notary_server_keys_json WHERE server_name = $1
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
// select the responses which have the given key IDs
|
||||||
|
// JOINs with the json table
|
||||||
|
const selectNotaryKeyResponsesWithKeyIDsSQL = `
|
||||||
|
SELECT response_json FROM federationsender_notary_server_keys_json
|
||||||
|
JOIN federationsender_notary_server_keys_metadata ON
|
||||||
|
federationsender_notary_server_keys_metadata.notary_id = federationsender_notary_server_keys_json.notary_id
|
||||||
|
WHERE federationsender_notary_server_keys_json.server_name = $1 AND federationsender_notary_server_keys_metadata.key_id IN ($2)
|
||||||
|
GROUP BY federationsender_notary_server_keys_json.notary_id
|
||||||
|
`
|
||||||
|
|
||||||
|
// JOINs with the metadata table
|
||||||
|
const deleteUnusedServerKeysJSONSQL = `
|
||||||
|
DELETE FROM federationsender_notary_server_keys_json WHERE federationsender_notary_server_keys_json.notary_id NOT IN (
|
||||||
|
SELECT DISTINCT notary_id FROM federationsender_notary_server_keys_metadata
|
||||||
|
)
|
||||||
|
`
|
||||||
|
|
||||||
|
type notaryServerKeysMetadataStatements struct {
|
||||||
|
db *sql.DB
|
||||||
|
upsertServerKeysStmt *sql.Stmt
|
||||||
|
selectNotaryKeyResponsesStmt *sql.Stmt
|
||||||
|
selectNotaryKeyMetadataStmt *sql.Stmt
|
||||||
|
deleteUnusedServerKeysJSONStmt *sql.Stmt
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSQLiteNotaryServerKeysMetadataTable(db *sql.DB) (s *notaryServerKeysMetadataStatements, err error) {
|
||||||
|
s = ¬aryServerKeysMetadataStatements{
|
||||||
|
db: db,
|
||||||
|
}
|
||||||
|
_, err = db.Exec(notaryServerKeysMetadataSchema)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.upsertServerKeysStmt, err = db.Prepare(upsertServerKeysSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.selectNotaryKeyResponsesStmt, err = db.Prepare(selectNotaryKeyResponsesSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.selectNotaryKeyMetadataStmt, err = db.Prepare(selectNotaryKeyMetadataSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if s.deleteUnusedServerKeysJSONStmt, err = db.Prepare(deleteUnusedServerKeysJSONSQL); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) UpsertKey(
|
||||||
|
ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyID gomatrixserverlib.KeyID, newNotaryID tables.NotaryID, newValidUntil gomatrixserverlib.Timestamp,
|
||||||
|
) (tables.NotaryID, error) {
|
||||||
|
notaryID := newNotaryID
|
||||||
|
// see if the existing notary ID a) exists, b) has a longer valid_until
|
||||||
|
var existingNotaryID tables.NotaryID
|
||||||
|
var existingValidUntil gomatrixserverlib.Timestamp
|
||||||
|
if err := txn.Stmt(s.selectNotaryKeyMetadataStmt).QueryRowContext(ctx, serverName, keyID).Scan(&existingNotaryID, &existingValidUntil); err != nil {
|
||||||
|
if err != sql.ErrNoRows {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if existingValidUntil.Time().After(newValidUntil.Time()) {
|
||||||
|
// the existing valid_until is valid longer, so use that.
|
||||||
|
return existingNotaryID, nil
|
||||||
|
}
|
||||||
|
// overwrite the notary_id for this (server_name, key_id) tuple
|
||||||
|
_, err := txn.Stmt(s.upsertServerKeysStmt).ExecContext(ctx, notaryID, serverName, keyID)
|
||||||
|
return notaryID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) SelectKeys(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyIDs []gomatrixserverlib.KeyID) ([]gomatrixserverlib.ServerKeys, error) {
|
||||||
|
var rows *sql.Rows
|
||||||
|
var err error
|
||||||
|
if len(keyIDs) == 0 {
|
||||||
|
rows, err = txn.Stmt(s.selectNotaryKeyResponsesStmt).QueryContext(ctx, string(serverName))
|
||||||
|
} else {
|
||||||
|
iKeyIDs := make([]interface{}, len(keyIDs)+1)
|
||||||
|
iKeyIDs[0] = serverName
|
||||||
|
for i := range keyIDs {
|
||||||
|
iKeyIDs[i+1] = string(keyIDs[i])
|
||||||
|
}
|
||||||
|
sql := strings.Replace(selectNotaryKeyResponsesWithKeyIDsSQL, "($2)", sqlutil.QueryVariadicOffset(len(keyIDs), 1), 1)
|
||||||
|
fmt.Println(sql)
|
||||||
|
fmt.Println(iKeyIDs...)
|
||||||
|
rows, err = s.db.QueryContext(ctx, sql, iKeyIDs...)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer internal.CloseAndLogIfError(ctx, rows, "selectNotaryKeyResponsesStmt close failed")
|
||||||
|
var results []gomatrixserverlib.ServerKeys
|
||||||
|
for rows.Next() {
|
||||||
|
var sk gomatrixserverlib.ServerKeys
|
||||||
|
var raw string
|
||||||
|
if err = rows.Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err = json.Unmarshal([]byte(raw), &sk); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
results = append(results, sk)
|
||||||
|
}
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notaryServerKeysMetadataStatements) DeleteOldJSONResponses(ctx context.Context, txn *sql.Tx) error {
|
||||||
|
_, err := txn.Stmt(s.deleteUnusedServerKeysJSONStmt).ExecContext(ctx)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
@ -18,8 +18,6 @@ package sqlite3
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
|
||||||
_ "github.com/mattn/go-sqlite3"
|
|
||||||
|
|
||||||
"github.com/matrix-org/dendrite/federationsender/storage/shared"
|
"github.com/matrix-org/dendrite/federationsender/storage/shared"
|
||||||
"github.com/matrix-org/dendrite/federationsender/storage/sqlite3/deltas"
|
"github.com/matrix-org/dendrite/federationsender/storage/sqlite3/deltas"
|
||||||
"github.com/matrix-org/dendrite/internal/caching"
|
"github.com/matrix-org/dendrite/internal/caching"
|
||||||
|
|
@ -71,6 +69,14 @@ func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationS
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
notaryKeys, err := NewSQLiteNotaryServerKeysTable(d.db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
notaryKeysMetadata, err := NewSQLiteNotaryServerKeysMetadataTable(d.db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
m := sqlutil.NewMigrations()
|
m := sqlutil.NewMigrations()
|
||||||
deltas.LoadRemoveRoomsTable(m)
|
deltas.LoadRemoveRoomsTable(m)
|
||||||
if err = m.RunDeltas(d.db, dbProperties); err != nil {
|
if err = m.RunDeltas(d.db, dbProperties); err != nil {
|
||||||
|
|
@ -87,6 +93,8 @@ func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationS
|
||||||
FederationSenderBlacklist: blacklist,
|
FederationSenderBlacklist: blacklist,
|
||||||
FederationSenderOutboundPeeks: outboundPeeks,
|
FederationSenderOutboundPeeks: outboundPeeks,
|
||||||
FederationSenderInboundPeeks: inboundPeeks,
|
FederationSenderInboundPeeks: inboundPeeks,
|
||||||
|
NotaryServerKeysJSON: notaryKeys,
|
||||||
|
NotaryServerKeysMetadata: notaryKeysMetadata,
|
||||||
}
|
}
|
||||||
if err = d.PartitionOffsetStatements.Prepare(d.db, d.writer, "federationsender"); err != nil {
|
if err = d.PartitionOffsetStatements.Prepare(d.db, d.writer, "federationsender"); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import (
|
||||||
"github.com/matrix-org/gomatrixserverlib"
|
"github.com/matrix-org/gomatrixserverlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type NotaryID int64
|
||||||
|
|
||||||
type FederationSenderQueuePDUs interface {
|
type FederationSenderQueuePDUs interface {
|
||||||
InsertQueuePDU(ctx context.Context, txn *sql.Tx, transactionID gomatrixserverlib.TransactionID, serverName gomatrixserverlib.ServerName, nid int64) error
|
InsertQueuePDU(ctx context.Context, txn *sql.Tx, transactionID gomatrixserverlib.TransactionID, serverName gomatrixserverlib.ServerName, nid int64) error
|
||||||
DeleteQueuePDUs(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, jsonNIDs []int64) error
|
DeleteQueuePDUs(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, jsonNIDs []int64) error
|
||||||
|
|
@ -80,3 +82,25 @@ type FederationSenderInboundPeeks interface {
|
||||||
DeleteInboundPeek(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, roomID, peekID string) (err error)
|
DeleteInboundPeek(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, roomID, peekID string) (err error)
|
||||||
DeleteInboundPeeks(ctx context.Context, txn *sql.Tx, roomID string) (err error)
|
DeleteInboundPeeks(ctx context.Context, txn *sql.Tx, roomID string) (err error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FederationSenderNotaryServerKeysJSON contains the byte-for-byte responses from servers which contain their keys and is signed by them.
|
||||||
|
type FederationSenderNotaryServerKeysJSON interface {
|
||||||
|
// InsertJSONResponse inserts a new response JSON. Useless on its own, needs querying via FederationSenderNotaryServerKeysMetadata
|
||||||
|
// `validUntil` should be the value of `valid_until_ts` with the 7-day check applied from:
|
||||||
|
// "Servers MUST use the lesser of this field and 7 days into the future when determining if a key is valid.
|
||||||
|
// This is to avoid a situation where an attacker publishes a key which is valid for a significant amount of time
|
||||||
|
// without a way for the homeserver owner to revoke it.""
|
||||||
|
InsertJSONResponse(ctx context.Context, txn *sql.Tx, keyQueryResponseJSON gomatrixserverlib.ServerKeys, serverName gomatrixserverlib.ServerName, validUntil gomatrixserverlib.Timestamp) (NotaryID, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FederationSenderNotaryServerKeysMetadata persists the metadata for FederationSenderNotaryServerKeysJSON
|
||||||
|
type FederationSenderNotaryServerKeysMetadata interface {
|
||||||
|
// UpsertKey updates or inserts a (server_name, key_id) tuple, pointing it via NotaryID at the the response which has the longest valid_until_ts
|
||||||
|
// `newNotaryID` and `newValidUntil` should be the notary ID / valid_until which has this (server_name, key_id) tuple already, e.g one you just inserted.
|
||||||
|
UpsertKey(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyID gomatrixserverlib.KeyID, newNotaryID NotaryID, newValidUntil gomatrixserverlib.Timestamp) (NotaryID, error)
|
||||||
|
// SelectKeys returns the signed JSON objects which contain the given key IDs. This will be at most the length of `keyIDs` and at least 1 (assuming
|
||||||
|
// the keys exist in the first place). If `keyIDs` is empty, the signed JSON object with the longest valid_until_ts will be returned.
|
||||||
|
SelectKeys(ctx context.Context, txn *sql.Tx, serverName gomatrixserverlib.ServerName, keyIDs []gomatrixserverlib.KeyID) ([]gomatrixserverlib.ServerKeys, error)
|
||||||
|
// DeleteOldJSONResponses removes all responses which are not referenced in FederationSenderNotaryServerKeysMetadata
|
||||||
|
DeleteOldJSONResponses(ctx context.Context, txn *sql.Tx) error
|
||||||
|
}
|
||||||
|
|
|
||||||
61
go.mod
61
go.mod
|
|
@ -1,15 +1,23 @@
|
||||||
module github.com/matrix-org/dendrite
|
module github.com/matrix-org/dendrite
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/Arceliar/ironwood v0.0.0-20210619124114-6ad55cae5031
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0
|
github.com/DATA-DOG/go-sqlmock v1.5.0
|
||||||
github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect
|
github.com/HdrHistogram/hdrhistogram-go v1.0.1 // indirect
|
||||||
github.com/Shopify/sarama v1.28.0
|
github.com/Masterminds/semver/v3 v3.1.1
|
||||||
github.com/getsentry/sentry-go v0.10.0
|
github.com/Shopify/sarama v1.29.1
|
||||||
|
github.com/codeclysm/extract v2.2.0+incompatible
|
||||||
|
github.com/containerd/containerd v1.5.5 // indirect
|
||||||
|
github.com/docker/docker v20.10.7+incompatible
|
||||||
|
github.com/docker/go-connections v0.4.0
|
||||||
|
github.com/getsentry/sentry-go v0.11.0
|
||||||
github.com/gologme/log v1.2.0
|
github.com/gologme/log v1.2.0
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/gorilla/websocket v1.4.2
|
github.com/gorilla/websocket v1.4.2
|
||||||
|
github.com/h2non/filetype v1.1.1 // indirect
|
||||||
github.com/hashicorp/golang-lru v0.5.4
|
github.com/hashicorp/golang-lru v0.5.4
|
||||||
github.com/lib/pq v1.9.0
|
github.com/juju/testing v0.0.0-20210324180055-18c50b0c2098 // indirect
|
||||||
|
github.com/lib/pq v1.10.2
|
||||||
github.com/libp2p/go-libp2p v0.13.0
|
github.com/libp2p/go-libp2p v0.13.0
|
||||||
github.com/libp2p/go-libp2p-circuit v0.4.0
|
github.com/libp2p/go-libp2p-circuit v0.4.0
|
||||||
github.com/libp2p/go-libp2p-core v0.8.3
|
github.com/libp2p/go-libp2p-core v0.8.3
|
||||||
|
|
@ -18,35 +26,42 @@ require (
|
||||||
github.com/libp2p/go-libp2p-kad-dht v0.11.1
|
github.com/libp2p/go-libp2p-kad-dht v0.11.1
|
||||||
github.com/libp2p/go-libp2p-pubsub v0.4.1
|
github.com/libp2p/go-libp2p-pubsub v0.4.1
|
||||||
github.com/libp2p/go-libp2p-record v0.1.3
|
github.com/libp2p/go-libp2p-record v0.1.3
|
||||||
github.com/lucas-clemente/quic-go v0.19.3
|
github.com/lucas-clemente/quic-go v0.22.0
|
||||||
github.com/matrix-org/dugong v0.0.0-20180820122854-51a565b5666b
|
github.com/matrix-org/dugong v0.0.0-20210603171012-8379174dca81
|
||||||
github.com/matrix-org/go-http-js-libp2p v0.0.0-20200518170932-783164aeeda4
|
github.com/matrix-org/go-http-js-libp2p v0.0.0-20200518170932-783164aeeda4
|
||||||
github.com/matrix-org/go-sqlite3-js v0.0.0-20200522092705-bc8506ccbcf3
|
github.com/matrix-org/go-sqlite3-js v0.0.0-20210709140738-b0d1ba599a6d
|
||||||
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd
|
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
|
||||||
github.com/matrix-org/gomatrixserverlib v0.0.0-20210525110027-8cb7699aa64a
|
github.com/matrix-org/gomatrixserverlib v0.0.0-20210809130922-d9c3f400582b
|
||||||
github.com/matrix-org/naffka v0.0.0-20201009174903-d26a3b9cb161
|
github.com/matrix-org/naffka v0.0.0-20210623111924-14ff508b58e0
|
||||||
github.com/matrix-org/pinecone v0.0.0-20210602111459-5cb0e6aa1a6a
|
github.com/matrix-org/pinecone v0.0.0-20210623102758-74f885644c1b
|
||||||
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
|
github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4
|
||||||
github.com/matryer/is v1.4.0
|
github.com/matryer/is v1.4.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.7-0.20210414154423-1157a4212dcb
|
github.com/mattn/go-sqlite3 v1.14.8
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646
|
||||||
github.com/ngrok/sqlmw v0.0.0-20200129213757-d5c93a81bec6
|
github.com/ngrok/sqlmw v0.0.0-20200129213757-d5c93a81bec6
|
||||||
github.com/opentracing/opentracing-go v1.2.0
|
github.com/opentracing/opentracing-go v1.2.0
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/pressly/goose v2.7.0+incompatible
|
github.com/pressly/goose v2.7.0+incompatible
|
||||||
github.com/prometheus/client_golang v1.9.0
|
github.com/prometheus/client_golang v1.11.0
|
||||||
github.com/sirupsen/logrus v1.8.0
|
github.com/sirupsen/logrus v1.8.1
|
||||||
github.com/tidwall/gjson v1.6.8
|
github.com/tidwall/gjson v1.8.1
|
||||||
github.com/tidwall/sjson v1.1.5
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/uber/jaeger-client-go v2.25.0+incompatible
|
github.com/tidwall/sjson v1.1.7
|
||||||
github.com/uber/jaeger-lib v2.4.0+incompatible
|
github.com/uber/jaeger-client-go v2.29.1+incompatible
|
||||||
github.com/yggdrasil-network/yggdrasil-go v0.3.15-0.20210218094457-e77ca8019daa
|
github.com/uber/jaeger-lib v2.4.1+incompatible
|
||||||
go.uber.org/atomic v1.7.0
|
github.com/yggdrasil-network/yggdrasil-go v0.4.1-0.20210715083903-52309d094c00
|
||||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
go.uber.org/atomic v1.9.0
|
||||||
golang.org/x/mobile v0.0.0-20210220033013-bdb1ca9a1e08
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
|
golang.org/x/mobile v0.0.0-20210716004757-34ab1303b554
|
||||||
|
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c // indirect
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b
|
||||||
gopkg.in/h2non/bimg.v1 v1.1.5
|
gopkg.in/h2non/bimg.v1 v1.1.5
|
||||||
gopkg.in/yaml.v2 v2.4.0
|
gopkg.in/yaml.v2 v2.4.0
|
||||||
|
nhooyr.io/websocket v1.8.7
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.14
|
go 1.15
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue