diff --git a/.github/workflows/dendrite.yml b/.github/workflows/dendrite.yml index 4f337a866..5d60301c7 100644 --- a/.github/workflows/dendrite.yml +++ b/.github/workflows/dendrite.yml @@ -250,6 +250,7 @@ jobs: env: POSTGRES: ${{ matrix.postgres && 1}} API: ${{ matrix.api && 1 }} + SYTEST_BRANCH: ${{ github.head_ref }} steps: - uses: actions/checkout@v2 - name: Run Sytest diff --git a/.gitignore b/.gitignore index 2a8c2cf55..e4f0112c4 100644 --- a/.gitignore +++ b/.gitignore @@ -41,6 +41,10 @@ _testmain.go *.test *.prof *.wasm +*.aar +*.jar +*.framework +*.xcframework # Generated keys *.pem @@ -65,4 +69,7 @@ test/wasm/node_modules # Ignore complement folder when running locally complement/ +# Stuff from GitHub Pages +docs/_site + media_store/ diff --git a/CHANGES.md b/CHANGES.md index 831a8969d..3deebd8a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,93 @@ # Changelog +## Dendrite 0.8.5 (2022-05-13) + +### Features + +* New living documentation available at , including new installation instructions +* The built-in NATS Server has been updated to version 2.8.2 + +### Fixes + +* Monolith deployments will no longer panic at startup if given a config file that does not include the `internal_api` and `external_api` options +* State resolution v2 now correctly identifies other events related to power events, which should fix some event auth issues +* The latest events updater will no longer implicitly trust the new forward extremities when calculating the current room state, which may help to avoid some state resets +* The one-time key count is now correctly returned in `/sync` even if the request otherwise timed out, which should reduce the chance that unnecessary one-time keys will be uploaded by clients +* The `create-account` tool should now work properly when the database is configured using the global connection pool + +## Dendrite 0.8.4 (2022-05-10) + +### Fixes + +* Fixes a regression introduced in the previous version where appservices, push and phone-home statistics would not work over plain HTTP +* Adds missing indexes to the sync API output events table, which should significantly improve `/sync` performance and reduce database CPU usage +* Building Dendrite with the `bimg` thumbnailer should now work again (contributed by [database64128](https://github.com/database64128)) + +## Dendrite 0.8.3 (2022-05-09) + +### Features + +* Open registration is now harder to enable, which should reduce the chance that Dendrite servers will be used to conduct spam or abuse attacks + * Dendrite will only enable open registration if you pass the `--really-enable-open-registration` command line flag at startup + * If open registration is enabled but this command line flag is not passed, Dendrite will fail to start up +* Dendrite now supports phone-home statistic reporting + * These statistics include things like the number of registered and active users, some configuration options and platform/environment details, to help us to understand how Dendrite is used + * This is not enabled by default — it must be enabled in the `global.report_stats` section of the config file +* Monolith installations can now be configured with a single global database connection pool (in `global.database` in the config) rather than having to configure each component separately + * This also means that you no longer need to balance connection counts between different components, as they will share the same larger pool + * Specific components can override the global database settings by specifying their own `database` block + * To use only the global pool, you must configure `global.database` and then remove the `database` block from all of the component sections of the config file +* A new admin API endpoint `/_dendrite/admin/evacuateRoom/{roomID}` has been added, allowing server admins to forcefully part all local users from a given room +* The sync notifier now only loads members for the relevant rooms, which should reduce CPU usage and load on the database +* A number of component interfaces have been refactored for cleanliness and developer ease +* Event auth errors in the log should now be much more useful, including the reason for the event failures +* The forward extremity calculation in the roomserver has been simplified +* A new index has been added to the one-time keys table in the keyserver which should speed up key count lookups + +### Fixes + +* Dendrite will no longer process events for rooms where there are no local users joined, which should help to reduce CPU and RAM usage +* A bug has been fixed in event auth when changing the user levels in `m.room.power_levels` events +* Usernames should no longer be duplicated when no room name is set +* Device display names should now be correctly propagated over federation +* A panic when uploading cross-signing signatures has been fixed +* Presence is now correctly limited in `/sync` based on the filters +* The presence stream position returned by `/sync` will now be correct if no presence events were returned +* The media `/config` endpoint will no longer return a maximum upload size field if it is configured to be unlimited in the Dendrite config +* The server notices room will no longer produce "User is already joined to the room" errors +* Consumer errors will no longer flood the logs during a graceful shutdown +* Sync API and federation API consumers will no longer unnecessarily query added state events matching the one in the output event +* The Sync API will no longer unnecessarily track invites for remote users + +## Dendrite 0.8.2 (2022-04-27) + +### Features + +* Lazy-loading has been added to the `/sync` endpoint, which should speed up syncs considerably +* Filtering has been added to the `/messages` endpoint +* The room summary now contains "heroes" (up to 5 users in the room) for clients to display when no room name is set +* The existing lazy-loading caches will now be used by `/messages` and `/context` so that member events will not be sent to clients more times than necessary +* The account data stream now uses the provided filters +* The built-in NATS Server has been updated to version 2.8.0 +* The `/state` and `/state_ids` endpoints will now return `M_NOT_FOUND` for rejected events +* Repeated calls to the `/redact` endpoint will now be idempotent when a transaction ID is given +* Dendrite should now be able to run as a Windows service under Service Control Manager + +### Fixes + +* Fictitious presence updates will no longer be created for users which have not sent us presence updates, which should speed up complete syncs considerably +* Uploading cross-signing device signatures should now be more reliable, fixing a number of bugs with cross-signing +* All account data should now be sent properly on a complete sync, which should eliminate problems with client settings or key backups appearing to be missing +* Account data will now be limited correctly on incremental syncs, returning the stream position of the most recent update rather than the latest stream position +* Account data will not be sent for parted rooms, which should reduce the number of left/forgotten rooms reappearing in clients as empty rooms +* The TURN username hash has been fixed which should help to resolve some problems when using TURN for voice calls (contributed by [fcwoknhenuxdfiyv](https://github.com/fcwoknhenuxdfiyv)) +* Push rules can no longer be modified using the account data endpoints +* Querying account availability should now work properly in polylith deployments +* A number of bugs with sync filters have been fixed +* A default sync filter will now be used if the request contains a filter ID that does not exist +* The `pushkey_ts` field is now using seconds instead of milliseconds +* A race condition when gracefully shutting down has been fixed, so JetStream should no longer cause the process to exit before other Dendrite components are finished shutting down + ## Dendrite 0.8.1 (2022-04-07) ### Fixes diff --git a/README.md b/README.md index cbb35ad59..e8b7bd0e2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Dendrite + [![Build status](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml/badge.svg?event=push)](https://github.com/matrix-org/dendrite/actions/workflows/dendrite.yml) [![Dendrite](https://img.shields.io/matrix/dendrite:matrix.org.svg?label=%23dendrite%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite:matrix.org) [![Dendrite Dev](https://img.shields.io/matrix/dendrite-dev:matrix.org.svg?label=%23dendrite-dev%3Amatrix.org&logo=matrix&server_fqdn=matrix.org)](https://matrix.to/#/#dendrite-dev:matrix.org) Dendrite is a second-generation Matrix homeserver written in Go. @@ -6,11 +7,11 @@ It intends to provide an **efficient**, **reliable** and **scalable** alternativ - Efficient: A small memory footprint with better baseline performance than an out-of-the-box Synapse. - Reliable: Implements the Matrix specification as written, using the - [same test suite](https://github.com/matrix-org/sytest) as Synapse as well as - a [brand new Go test suite](https://github.com/matrix-org/complement). + [same test suite](https://github.com/matrix-org/sytest) as Synapse as well as + a [brand new Go test suite](https://github.com/matrix-org/complement). - Scalable: can run on multiple machines and eventually scale to massive homeserver deployments. -As of October 2020, Dendrite has now entered **beta** which means: +As of October 2020 (current [progress below](#progress)), Dendrite has now entered **beta** which means: - Dendrite is ready for early adopters. We recommend running in Monolith mode with a PostgreSQL database. - Dendrite has periodic semver releases. We intend to release new versions as we land significant features. @@ -21,7 +22,7 @@ This does not mean: - Dendrite is bug-free. It has not yet been battle-tested in the real world and so will be error prone initially. - All of the CS/Federation APIs are implemented. We are tracking progress via a script called 'Are We Synapse Yet?'. In particular, - presence and push notifications are entirely missing from Dendrite. See [CHANGES.md](CHANGES.md) for updates. + presence and push notifications are entirely missing from Dendrite. See [CHANGES.md](CHANGES.md) for updates. - Dendrite is ready for massive homeserver deployments. You cannot shard each microservice, only run each one on a different machine. Currently, we expect Dendrite to function well for small (10s/100s of users) homeserver deployments as well as P2P Matrix nodes in-browser or on mobile devices. @@ -52,7 +53,7 @@ The [Federation Tester](https://federationtester.matrix.org) can be used to veri ## Get started -If you wish to build a fully-federating Dendrite instance, see [INSTALL.md](docs/INSTALL.md). For running in Docker, see [build/docker](build/docker). +If you wish to build a fully-federating Dendrite instance, see [the Installation documentation](https://matrix-org.github.io/dendrite/installation). For running in Docker, see [build/docker](build/docker). The following instructions are enough to get Dendrite started as a non-federating test deployment using self-signed certificates and SQLite databases: @@ -70,15 +71,19 @@ $ ./bin/generate-keys --tls-cert server.crt --tls-key server.key # Copy and modify the config file - you'll need to set a server name and paths to the keys # at the very least, along with setting up the database connection strings. -$ cp dendrite-config.yaml dendrite.yaml +$ cp dendrite-sample.monolith.yaml dendrite.yaml # Build and run the server: $ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml + +# Create an user account (add -admin for an admin user). +# Specify the localpart only, e.g. 'alice' for '@alice:domain.com' +$ ./bin/create-account --config dendrite.yaml -username alice ``` Then point your favourite Matrix client at `http://localhost:8008` or `https://localhost:8448`. -## Progress +## Progress We use a script called Are We Synapse Yet which checks Sytest compliance rates. Sytest is a black-box homeserver test rig with around 900 tests. The script works out how many of these tests are passing on Dendrite and it diff --git a/appservice/api/query.go b/appservice/api/query.go index cf25a9616..4d1cf9474 100644 --- a/appservice/api/query.go +++ b/appservice/api/query.go @@ -26,6 +26,23 @@ import ( "github.com/matrix-org/gomatrixserverlib" ) +// AppServiceInternalAPI is used to query user and room alias data from application +// services +type AppServiceInternalAPI interface { + // Check whether a room alias exists within any application service namespaces + RoomAliasExists( + ctx context.Context, + req *RoomAliasExistsRequest, + resp *RoomAliasExistsResponse, + ) error + // Check whether a user ID exists within any application service namespaces + UserIDExists( + ctx context.Context, + req *UserIDExistsRequest, + resp *UserIDExistsResponse, + ) error +} + // RoomAliasExistsRequest is a request to an application service // about whether a room alias exists type RoomAliasExistsRequest struct { @@ -60,31 +77,14 @@ type UserIDExistsResponse struct { UserIDExists bool `json:"exists"` } -// AppServiceQueryAPI is used to query user and room alias data from application -// services -type AppServiceQueryAPI interface { - // Check whether a room alias exists within any application service namespaces - RoomAliasExists( - ctx context.Context, - req *RoomAliasExistsRequest, - resp *RoomAliasExistsResponse, - ) error - // Check whether a user ID exists within any application service namespaces - UserIDExists( - ctx context.Context, - req *UserIDExistsRequest, - resp *UserIDExistsResponse, - ) error -} - // RetrieveUserProfile is a wrapper that queries both the local database and // application services for a given user's profile // TODO: Remove this, it's called from federationapi and clientapi but is a pure function func RetrieveUserProfile( ctx context.Context, userID string, - asAPI AppServiceQueryAPI, - profileAPI userapi.UserProfileAPI, + asAPI AppServiceInternalAPI, + profileAPI userapi.ClientUserAPI, ) (*authtypes.Profile, error) { localpart, _, err := gomatrixserverlib.SplitID('@', userID) if err != nil { diff --git a/appservice/appservice.go b/appservice/appservice.go index b99091866..8fe1b2fc4 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -34,12 +34,11 @@ import ( roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/dendrite/setup/jetstream" userapi "github.com/matrix-org/dendrite/userapi/api" ) // AddInternalRoutes registers HTTP handlers for internal API calls -func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceQueryAPI) { +func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceInternalAPI) { inthttp.AddRoutes(queryAPI, router) } @@ -49,7 +48,7 @@ func NewInternalAPI( base *base.BaseDendrite, userAPI userapi.UserInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI, -) appserviceAPI.AppServiceQueryAPI { +) appserviceAPI.AppServiceInternalAPI { client := &http.Client{ Timeout: time.Second * 30, Transport: &http.Transport{ @@ -57,12 +56,13 @@ func NewInternalAPI( TLSClientConfig: &tls.Config{ InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation, }, + Proxy: http.ProxyFromEnvironment, }, } - js, _ := jetstream.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) // Create a connection to the appservice postgres DB - appserviceDB, err := storage.NewDatabase(&base.Cfg.AppServiceAPI.Database) + appserviceDB, err := storage.NewDatabase(base, &base.Cfg.AppServiceAPI.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to appservice db") } @@ -117,7 +117,7 @@ func NewInternalAPI( // `sender_localpart` field of each application service if it doesn't // exist already func generateAppServiceAccount( - userAPI userapi.UserInternalAPI, + userAPI userapi.AppserviceUserAPI, as config.ApplicationService, ) error { var accRes userapi.PerformAccountCreationResponse diff --git a/appservice/consumers/roomserver.go b/appservice/consumers/roomserver.go index 31e05caa0..e406e88a7 100644 --- a/appservice/consumers/roomserver.go +++ b/appservice/consumers/roomserver.go @@ -37,7 +37,7 @@ type OutputRoomEventConsumer struct { durable string topic string asDB storage.Database - rsAPI api.RoomserverInternalAPI + rsAPI api.AppserviceRoomserverAPI serverName string workerStates []types.ApplicationServiceWorkerState } @@ -49,7 +49,7 @@ func NewOutputRoomEventConsumer( cfg *config.Dendrite, js nats.JetStreamContext, appserviceDB storage.Database, - rsAPI api.RoomserverInternalAPI, + rsAPI api.AppserviceRoomserverAPI, workerStates []types.ApplicationServiceWorkerState, ) *OutputRoomEventConsumer { return &OutputRoomEventConsumer{ diff --git a/appservice/inthttp/client.go b/appservice/inthttp/client.go index 7e3cb208f..0a8baea99 100644 --- a/appservice/inthttp/client.go +++ b/appservice/inthttp/client.go @@ -29,7 +29,7 @@ type httpAppServiceQueryAPI struct { func NewAppserviceClient( appserviceURL string, httpClient *http.Client, -) (api.AppServiceQueryAPI, error) { +) (api.AppServiceInternalAPI, error) { if httpClient == nil { return nil, errors.New("NewRoomserverAliasAPIHTTP: httpClient is ") } diff --git a/appservice/inthttp/server.go b/appservice/inthttp/server.go index 009b7b5db..645b43871 100644 --- a/appservice/inthttp/server.go +++ b/appservice/inthttp/server.go @@ -11,7 +11,7 @@ import ( ) // AddRoutes adds the AppServiceQueryAPI handlers to the http.ServeMux. -func AddRoutes(a api.AppServiceQueryAPI, internalAPIMux *mux.Router) { +func AddRoutes(a api.AppServiceInternalAPI, internalAPIMux *mux.Router) { internalAPIMux.Handle( AppServiceRoomAliasExistsPath, httputil.MakeInternalAPI("appserviceRoomAliasExists", func(req *http.Request) util.JSONResponse { diff --git a/appservice/storage/postgres/storage.go b/appservice/storage/postgres/storage.go index eaf947ff3..a4c04b2cc 100644 --- a/appservice/storage/postgres/storage.go +++ b/appservice/storage/postgres/storage.go @@ -22,6 +22,7 @@ import ( // Import postgres database driver _ "github.com/lib/pq" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) @@ -35,13 +36,12 @@ type Database struct { } // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*Database, error) { var result Database var err error - if result.db, err = sqlutil.Open(dbProperties); err != nil { + if result.db, result.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil { return nil, err } - result.writer = sqlutil.NewDummyWriter() if err = result.prepare(); err != nil { return nil, err } diff --git a/appservice/storage/sqlite3/storage.go b/appservice/storage/sqlite3/storage.go index 9260c7fe7..ad62b3628 100644 --- a/appservice/storage/sqlite3/storage.go +++ b/appservice/storage/sqlite3/storage.go @@ -21,6 +21,7 @@ import ( // Import SQLite database driver "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) @@ -34,13 +35,12 @@ type Database struct { } // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions) (*Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*Database, error) { var result Database var err error - if result.db, err = sqlutil.Open(dbProperties); err != nil { + if result.db, result.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil { return nil, err } - result.writer = sqlutil.NewExclusiveWriter() if err = result.prepare(); err != nil { return nil, err } diff --git a/appservice/storage/storage.go b/appservice/storage/storage.go index 97b8501e2..89d5e0cc2 100644 --- a/appservice/storage/storage.go +++ b/appservice/storage/storage.go @@ -22,17 +22,18 @@ import ( "github.com/matrix-org/dendrite/appservice/storage/postgres" "github.com/matrix-org/dendrite/appservice/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme) // and sets DB connection parameters -func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties) + return postgres.NewDatabase(base, dbProperties) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/appservice/storage/storage_wasm.go b/appservice/storage/storage_wasm.go index 07d0e9ee1..230254598 100644 --- a/appservice/storage/storage_wasm.go +++ b/appservice/storage/storage_wasm.go @@ -18,13 +18,14 @@ import ( "fmt" "github.com/matrix-org/dendrite/appservice/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) -func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/build/docker/Dockerfile.monolith b/build/docker/Dockerfile.monolith index 0d2a141ad..891a3a9e0 100644 --- a/build/docker/Dockerfile.monolith +++ b/build/docker/Dockerfile.monolith @@ -1,4 +1,4 @@ -FROM docker.io/golang:1.17-alpine AS base +FROM docker.io/golang:1.18-alpine AS base RUN apk --update --no-cache add bash build-base @@ -23,4 +23,4 @@ COPY --from=base /build/bin/* /usr/bin/ VOLUME /etc/dendrite WORKDIR /etc/dendrite -ENTRYPOINT ["/usr/bin/dendrite-monolith-server"] \ No newline at end of file +ENTRYPOINT ["/usr/bin/dendrite-monolith-server"] diff --git a/build/docker/Dockerfile.polylith b/build/docker/Dockerfile.polylith index c266fd480..ffdc35586 100644 --- a/build/docker/Dockerfile.polylith +++ b/build/docker/Dockerfile.polylith @@ -1,4 +1,4 @@ -FROM docker.io/golang:1.17-alpine AS base +FROM docker.io/golang:1.18-alpine AS base RUN apk --update --no-cache add bash build-base @@ -23,4 +23,4 @@ COPY --from=base /build/bin/* /usr/bin/ VOLUME /etc/dendrite WORKDIR /etc/dendrite -ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"] \ No newline at end of file +ENTRYPOINT ["/usr/bin/dendrite-polylith-multi"] diff --git a/build/docker/README.md b/build/docker/README.md index 7425d96cb..261519fde 100644 --- a/build/docker/README.md +++ b/build/docker/README.md @@ -27,8 +27,7 @@ There are three sample `docker-compose` files: The `docker-compose` files refer to the `/etc/dendrite` volume as where the runtime config should come from. The mounted folder must contain: -- `dendrite.yaml` configuration file (from the [Docker config folder](https://github.com/matrix-org/dendrite/tree/master/build/docker/config) - sample in the `build/docker/config` folder of this repository.) +- `dendrite.yaml` configuration file (based on one of the sample config files) - `matrix_key.pem` server key, as generated using `cmd/generate-keys` - `server.crt` certificate file - `server.key` private key file for the above certificate @@ -49,7 +48,7 @@ The key files will now exist in your current working directory, and can be mount ## Starting Dendrite as a monolith deployment -Create your config based on the [`dendrite.yaml`](https://github.com/matrix-org/dendrite/tree/master/build/docker/config) configuration file in the `build/docker/config` folder of this repository. +Create your config based on the [`dendrite-sample.monolith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.monolith.yaml) sample configuration file. Then start the deployment: @@ -59,7 +58,7 @@ docker-compose -f docker-compose.monolith.yml up ## Starting Dendrite as a polylith deployment -Create your config based on the [`dendrite-config.yaml`](https://github.com/matrix-org/dendrite/tree/master/build/docker/config) configuration file in the `build/docker/config` folder of this repository. +Create your config based on the [`dendrite-sample.polylith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.polylith.yaml) sample configuration file. Then start the deployment: diff --git a/build/docker/config/dendrite.yaml b/build/docker/config/dendrite.yaml deleted file mode 100644 index e3a0316dc..000000000 --- a/build/docker/config/dendrite.yaml +++ /dev/null @@ -1,348 +0,0 @@ -# This is the Dendrite configuration file. -# -# The configuration is split up into sections - each Dendrite component has a -# configuration section, in addition to the "global" section which applies to -# all components. -# -# At a minimum, to get started, you will need to update the settings in the -# "global" section for your deployment, and you will need to check that the -# database "connection_string" line in each component section is correct. -# -# Each component with a "database" section can accept the following formats -# for "connection_string": -# SQLite: file:filename.db -# file:///path/to/filename.db -# PostgreSQL: postgresql://user:pass@hostname/database?params=... -# -# SQLite is embedded into Dendrite and therefore no further prerequisites are -# needed for the database when using SQLite mode. However, performance with -# PostgreSQL is significantly better and recommended for multi-user deployments. -# SQLite is typically around 20-30% slower than PostgreSQL when tested with a -# small number of users and likely will perform worse still with a higher volume -# of users. -# -# The "max_open_conns" and "max_idle_conns" settings configure the maximum -# number of open/idle database connections. The value 0 will use the database -# engine default, and a negative value will use unlimited connections. The -# "conn_max_lifetime" option controls the maximum length of time a database -# connection can be idle in seconds - a negative value is unlimited. - -# The version of the configuration file. -version: 2 - -# Global Matrix configuration. This configuration applies to all components. -global: - # The domain name of this homeserver. - server_name: example.com - - # The path to the signing private key file, used to sign requests and events. - private_key: matrix_key.pem - - # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) - # to old signing private keys that were formerly in use on this domain. These - # keys will not be used for federation request or event signing, but will be - # provided to any other homeserver that asks when trying to verify old events. - # old_private_keys: - # - private_key: old_matrix_key.pem - # expired_at: 1601024554498 - - # How long a remote server can cache our server signing key before requesting it - # again. Increasing this number will reduce the number of requests made by other - # servers for our key but increases the period that a compromised key will be - # considered valid by other homeservers. - key_validity_period: 168h0m0s - - # The server name to delegate server-server communications to, with optional port - # e.g. localhost:443 - well_known_server_name: "" - - # Lists of domains that the server will trust as identity servers to verify third - # party identifiers such as phone numbers and email addresses. - trusted_third_party_id_servers: - - matrix.org - - vector.im - - # Disables federation. Dendrite will not be able to make any outbound HTTP requests - # to other servers and the federation API will not be exposed. - disable_federation: false - - # Configures the handling of presence events. - presence: - # Whether inbound presence events are allowed, e.g. receiving presence events from other servers - enable_inbound: false - # Whether outbound presence events are allowed, e.g. sending presence events to other servers - enable_outbound: false - - # Configuration for NATS JetStream - jetstream: - # A list of NATS Server addresses to connect to. If none are specified, an - # internal NATS server will be started automatically when running Dendrite - # in monolith mode. It is required to specify the address of at least one - # NATS Server node if running in polylith mode. - addresses: - - jetstream:4222 - - # Keep all NATS streams in memory, rather than persisting it to the storage - # path below. This option is present primarily for integration testing and - # should not be used on a real world Dendrite deployment. - in_memory: false - - # Persistent directory to store JetStream streams in. This directory - # should be preserved across Dendrite restarts. - storage_path: ./ - - # The prefix to use for stream names for this homeserver - really only - # useful if running more than one Dendrite on the same NATS deployment. - topic_prefix: Dendrite - - # Configuration for Prometheus metric collection. - metrics: - # Whether or not Prometheus metrics are enabled. - enabled: false - - # HTTP basic authentication to protect access to monitoring. - basic_auth: - username: metrics - password: metrics - - # DNS cache options. The DNS cache may reduce the load on DNS servers - # if there is no local caching resolver available for use. - dns_cache: - # Whether or not the DNS cache is enabled. - enabled: false - - # Maximum number of entries to hold in the DNS cache, and - # for how long those items should be considered valid in seconds. - cache_size: 256 - cache_lifetime: 300 - -# Configuration for the Appservice API. -app_service_api: - internal_api: - listen: http://0.0.0.0:7777 - connect: http://appservice_api:7777 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_appservice?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - - # Appservice configuration files to load into this homeserver. - config_files: [] - -# Configuration for the Client API. -client_api: - internal_api: - listen: http://0.0.0.0:7771 - connect: http://client_api:7771 - external_api: - listen: http://0.0.0.0:8071 - - # Prevents new users from being able to register on this homeserver, except when - # using the registration shared secret below. - registration_disabled: false - - # If set, allows registration by anyone who knows the shared secret, regardless of - # whether registration is otherwise disabled. - registration_shared_secret: "" - - # Whether to require reCAPTCHA for registration. - enable_registration_captcha: false - - # Settings for ReCAPTCHA. - recaptcha_public_key: "" - recaptcha_private_key: "" - recaptcha_bypass_secret: "" - recaptcha_siteverify_api: "" - - # TURN server information that this homeserver should send to clients. - turn: - turn_user_lifetime: "" - turn_uris: [] - turn_shared_secret: "" - turn_username: "" - turn_password: "" - - # Settings for rate-limited endpoints. Rate limiting will kick in after the - # threshold number of "slots" have been taken by requests from a specific - # host. Each "slot" will be released after the cooloff time in milliseconds. - rate_limiting: - enabled: true - threshold: 5 - cooloff_ms: 500 - -# Configuration for the Federation API. -federation_api: - internal_api: - listen: http://0.0.0.0:7772 - connect: http://federation_api:7772 - external_api: - listen: http://0.0.0.0:8072 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_federationapi?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - - # How many times we will try to resend a failed transaction to a specific server. The - # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. - send_max_retries: 16 - - # Disable the validation of TLS certificates of remote federated homeservers. Do not - # enable this option in production as it presents a security risk! - disable_tls_validation: false - - # Use the following proxy server for outbound federation traffic. - proxy_outbound: - enabled: false - protocol: http - host: localhost - port: 8080 - - # Perspective keyservers to use as a backup when direct key fetches fail. This may - # be required to satisfy key requests for servers that are no longer online when - # joining some rooms. - key_perspectives: - - server_name: matrix.org - keys: - - key_id: ed25519:auto - public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw - - key_id: ed25519:a_RXGa - public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ - - # This option will control whether Dendrite will prefer to look up keys directly - # or whether it should try perspective servers first, using direct fetches as a - # last resort. - prefer_direct_fetch: false - -# Configuration for the Key Server (for end-to-end encryption). -key_server: - internal_api: - listen: http://0.0.0.0:7779 - connect: http://key_server:7779 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_keyserver?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for the Media API. -media_api: - internal_api: - listen: http://0.0.0.0:7774 - connect: http://media_api:7774 - external_api: - listen: http://0.0.0.0:8074 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_mediaapi?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - - # Storage path for uploaded media. May be relative or absolute. - base_path: /var/dendrite/media - - # The maximum allowed file size (in bytes) for media uploads to this homeserver - # (0 = unlimited). - max_file_size_bytes: 10485760 - - # Whether to dynamically generate thumbnails if needed. - dynamic_thumbnails: false - - # The maximum number of simultaneous thumbnail generators to run. - max_thumbnail_generators: 10 - - # A list of thumbnail sizes to be generated for media content. - thumbnail_sizes: - - width: 32 - height: 32 - method: crop - - width: 96 - height: 96 - method: crop - - width: 640 - height: 480 - method: scale - -# Configuration for experimental MSC's -mscs: - # A list of enabled MSC's - # Currently valid values are: - # - msc2836 (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) - # - msc2946 (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) - mscs: [] - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_mscs?sslmode=disable - max_open_conns: 5 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for the Room Server. -room_server: - internal_api: - listen: http://0.0.0.0:7770 - connect: http://room_server:7770 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_roomserver?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for the Sync API. -sync_api: - internal_api: - listen: http://0.0.0.0:7773 - connect: http://sync_api:7773 - external_api: - listen: http://0.0.0.0:8073 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_syncapi?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for the User API. -user_api: - internal_api: - listen: http://0.0.0.0:7781 - connect: http://user_api:7781 - account_database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_userapi_accounts?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for the Push Server API. -push_server: - internal_api: - listen: http://localhost:7782 - connect: http://localhost:7782 - database: - connection_string: postgresql://dendrite:itsasecret@postgres/dendrite_pushserver?sslmode=disable - max_open_conns: 10 - max_idle_conns: 2 - conn_max_lifetime: -1 - -# Configuration for Opentracing. -# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on -# how this works and how to set it up. -tracing: - enabled: false - jaeger: - serviceName: "" - disabled: false - rpc_metrics: false - tags: [] - sampler: null - reporter: null - headers: null - baggage_restrictions: null - throttler: null - -# Logging configuration, in addition to the standard logging that is sent to -# stdout by Dendrite. -logging: - - type: file - level: info - params: - path: /var/log/dendrite diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index 9cc94d650..664ca85d9 100644 --- a/build/gobind-pinecone/monolith.go +++ b/build/gobind-pinecone/monolith.go @@ -225,7 +225,7 @@ func (m *DendriteMonolith) Start() { pk = sk.Public().(ed25519.PublicKey) } - m.listener, err = net.Listen("tcp", ":65432") + m.listener, err = net.Listen("tcp", "localhost:65432") if err != nil { panic(err) } @@ -259,6 +259,8 @@ func (m *DendriteMonolith) Start() { cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory)) cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/media", m.CacheDirectory)) cfg.MSCs.MSCs = []string{"msc2836", "msc2946"} + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true if err := cfg.Derive(); err != nil { panic(err) } @@ -266,7 +268,6 @@ func (m *DendriteMonolith) Start() { base := base.NewBaseDendrite(cfg, "Monolith") defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := conn.CreateFederationClient(base, m.PineconeQUIC) serverKeyAPI := &signing.YggdrasilKeys{} @@ -279,7 +280,7 @@ func (m *DendriteMonolith) Start() { ) keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI) - m.userAPI = userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient()) + m.userAPI = userapi.NewInternalAPI(base, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient()) keyAPI.SetUserAPI(m.userAPI) asAPI := appservice.NewInternalAPI(base, m.userAPI, rsAPI) @@ -293,7 +294,6 @@ func (m *DendriteMonolith) Start() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: conn.CreateClient(base, m.PineconeQUIC), FedClient: federation, KeyRing: keyRing, @@ -306,15 +306,7 @@ func (m *DendriteMonolith) Start() { ExtPublicRoomsProvider: roomProvider, ExtUserDirectoryProvider: userProvider, } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath() httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux) diff --git a/build/gobind-yggdrasil/monolith.go b/build/gobind-yggdrasil/monolith.go index 87dcad2e8..991bc462f 100644 --- a/build/gobind-yggdrasil/monolith.go +++ b/build/gobind-yggdrasil/monolith.go @@ -97,6 +97,8 @@ func (m *DendriteMonolith) Start() { cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s/dendrite-p2p-appservice.db", m.StorageDirectory)) cfg.MediaAPI.BasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory)) cfg.MediaAPI.AbsBasePath = config.Path(fmt.Sprintf("%s/tmp", m.StorageDirectory)) + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true if err = cfg.Derive(); err != nil { panic(err) } @@ -105,7 +107,6 @@ func (m *DendriteMonolith) Start() { m.processContext = base.ProcessContext defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := ygg.CreateFederationClient(base) serverKeyAPI := &signing.YggdrasilKeys{} @@ -118,7 +119,7 @@ func (m *DendriteMonolith) Start() { ) keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation) - userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient()) + userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, base.PushGatewayHTTPClient()) keyAPI.SetUserAPI(userAPI) asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI) @@ -130,7 +131,6 @@ func (m *DendriteMonolith) Start() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: ygg.CreateClient(base), FedClient: federation, KeyRing: keyRing, @@ -144,15 +144,7 @@ func (m *DendriteMonolith) Start() { ygg, fsAPI, federation, ), } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) httpRouter := mux.NewRouter() httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux) diff --git a/build/scripts/Complement.Dockerfile b/build/scripts/Complement.Dockerfile index 6b2942d97..63e3890ee 100644 --- a/build/scripts/Complement.Dockerfile +++ b/build/scripts/Complement.Dockerfile @@ -29,4 +29,4 @@ EXPOSE 8008 8448 CMD ./generate-keys --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key && \ ./generate-config -server $SERVER_NAME --ci > dendrite.yaml && \ cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \ - ./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0} + ./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0} diff --git a/build/scripts/ComplementLocal.Dockerfile b/build/scripts/ComplementLocal.Dockerfile index 60b4d983a..a9feb4cd1 100644 --- a/build/scripts/ComplementLocal.Dockerfile +++ b/build/scripts/ComplementLocal.Dockerfile @@ -32,7 +32,7 @@ RUN echo '\ ./generate-keys --server $SERVER_NAME --tls-cert server.crt --tls-key server.key --tls-authority-cert /complement/ca/ca.crt --tls-authority-key /complement/ca/ca.key \n\ ./generate-config -server $SERVER_NAME --ci > dendrite.yaml \n\ cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates \n\ -./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\ +./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml \n\ ' > run.sh && chmod +x run.sh diff --git a/build/scripts/ComplementPostgres.Dockerfile b/build/scripts/ComplementPostgres.Dockerfile index b98f4671c..4e26faa58 100644 --- a/build/scripts/ComplementPostgres.Dockerfile +++ b/build/scripts/ComplementPostgres.Dockerfile @@ -51,4 +51,4 @@ CMD /build/run_postgres.sh && ./generate-keys --server $SERVER_NAME --tls-cert s sed -i "s%connection_string:.*$%connection_string: postgresql://postgres@localhost/postgres?sslmode=disable%g" dendrite.yaml && \ sed -i 's/max_open_conns:.*$/max_open_conns: 100/g' dendrite.yaml && \ cp /complement/ca/ca.crt /usr/local/share/ca-certificates/ && update-ca-certificates && \ - ./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0} \ No newline at end of file + ./dendrite-monolith-server --really-enable-open-registration --tls-cert server.crt --tls-key server.key --config dendrite.yaml -api=${API:-0} \ No newline at end of file diff --git a/build/scripts/find-lint.sh b/build/scripts/find-lint.sh index e3564ae38..820b8cc46 100755 --- a/build/scripts/find-lint.sh +++ b/build/scripts/find-lint.sh @@ -25,7 +25,7 @@ echo "Installing golangci-lint..." # Make a backup of go.{mod,sum} first cp go.mod go.mod.bak && cp go.sum go.sum.bak -go get github.com/golangci/golangci-lint/cmd/golangci-lint@v1.41.1 +go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.45.2 # Run linting echo "Looking for lint..." @@ -33,7 +33,7 @@ echo "Looking for lint..." # Capture exit code to ensure go.{mod,sum} is restored before exiting exit_code=0 -PATH="$PATH:${GOPATH:-~/go}/bin" golangci-lint run $args || exit_code=1 +PATH="$PATH:$(go env GOPATH)/bin" golangci-lint run $args || exit_code=1 # Restore go.{mod,sum} mv go.mod.bak go.mod && mv go.sum.bak go.sum diff --git a/clientapi/auth/auth.go b/clientapi/auth/auth.go index 575c5377f..93345f4b9 100644 --- a/clientapi/auth/auth.go +++ b/clientapi/auth/auth.go @@ -51,7 +51,7 @@ type AccountDatabase interface { // Note: For an AS user, AS dummy device is returned. // On failure returns an JSON error response which can be sent to the client. func VerifyUserFromRequest( - req *http.Request, userAPI api.UserInternalAPI, + req *http.Request, userAPI api.QueryAcccessTokenAPI, ) (*api.Device, *util.JSONResponse) { // Try to find the Application Service user token, err := ExtractAccessToken(req) diff --git a/clientapi/auth/login.go b/clientapi/auth/login.go index 020731c9f..5f51c662a 100644 --- a/clientapi/auth/login.go +++ b/clientapi/auth/login.go @@ -33,7 +33,7 @@ import ( // called after authorization has completed, with the result of the authorization. // If the final return value is non-nil, an error occurred and the cleanup function // is nil. -func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserAccountAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) { +func LoginFromJSONReader(ctx context.Context, r io.Reader, useraccountAPI uapi.UserLoginAPI, userAPI UserInternalAPIForLogin, cfg *config.ClientAPI) (*Login, LoginCleanupFunc, *util.JSONResponse) { reqBytes, err := ioutil.ReadAll(r) if err != nil { err := &util.JSONResponse{ diff --git a/clientapi/auth/login_test.go b/clientapi/auth/login_test.go index d401469c1..5085f0170 100644 --- a/clientapi/auth/login_test.go +++ b/clientapi/auth/login_test.go @@ -160,7 +160,6 @@ func TestBadLoginFromJSONReader(t *testing.T) { type fakeUserInternalAPI struct { UserInternalAPIForLogin - uapi.UserAccountAPI DeletedTokens []string } @@ -179,6 +178,10 @@ func (ua *fakeUserInternalAPI) PerformLoginTokenDeletion(ctx context.Context, re return nil } +func (ua *fakeUserInternalAPI) PerformLoginTokenCreation(ctx context.Context, req *uapi.PerformLoginTokenCreationRequest, res *uapi.PerformLoginTokenCreationResponse) error { + return nil +} + func (*fakeUserInternalAPI) QueryLoginToken(ctx context.Context, req *uapi.QueryLoginTokenRequest, res *uapi.QueryLoginTokenResponse) error { if req.Token == "invalidtoken" { return nil diff --git a/clientapi/auth/user_interactive.go b/clientapi/auth/user_interactive.go index 22c430f97..6caf7dcdc 100644 --- a/clientapi/auth/user_interactive.go +++ b/clientapi/auth/user_interactive.go @@ -110,7 +110,7 @@ type UserInteractive struct { Sessions map[string][]string } -func NewUserInteractive(userAccountAPI api.UserAccountAPI, cfg *config.ClientAPI) *UserInteractive { +func NewUserInteractive(userAccountAPI api.UserLoginAPI, cfg *config.ClientAPI) *UserInteractive { typePassword := &LoginTypePassword{ GetAccountByPassword: userAccountAPI.QueryAccountByPassword, Config: cfg, diff --git a/clientapi/auth/user_interactive_test.go b/clientapi/auth/user_interactive_test.go index a4b4587a3..262e48103 100644 --- a/clientapi/auth/user_interactive_test.go +++ b/clientapi/auth/user_interactive_test.go @@ -24,9 +24,7 @@ var ( } ) -type fakeAccountDatabase struct { - api.UserAccountAPI -} +type fakeAccountDatabase struct{} func (d *fakeAccountDatabase) PerformPasswordUpdate(ctx context.Context, req *api.PerformPasswordUpdateRequest, res *api.PerformPasswordUpdateResponse) error { return nil diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index e2f8d3f32..f550c29bb 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -15,7 +15,6 @@ package clientapi import ( - "github.com/gorilla/mux" appserviceAPI "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/clientapi/api" "github.com/matrix-org/dendrite/clientapi/producers" @@ -24,31 +23,28 @@ import ( "github.com/matrix-org/dendrite/internal/transactions" keyserverAPI "github.com/matrix-org/dendrite/keyserver/api" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" - "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/jetstream" - "github.com/matrix-org/dendrite/setup/process" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" ) // AddPublicRoutes sets up and registers HTTP handlers for the ClientAPI component. func AddPublicRoutes( - process *process.ProcessContext, - router *mux.Router, - synapseAdminRouter *mux.Router, - cfg *config.ClientAPI, + base *base.BaseDendrite, federation *gomatrixserverlib.FederationClient, - rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, transactionsCache *transactions.Cache, - fsAPI federationAPI.FederationInternalAPI, - userAPI userapi.UserInternalAPI, - userDirectoryProvider userapi.UserDirectoryProvider, - keyAPI keyserverAPI.KeyInternalAPI, + fsAPI federationAPI.ClientFederationAPI, + userAPI userapi.ClientUserAPI, + userDirectoryProvider userapi.QuerySearchProfilesAPI, + keyAPI keyserverAPI.ClientKeyAPI, extRoomsProvider api.ExtraPublicRoomsProvider, - mscCfg *config.MSCs, ) { - js, natsClient := jetstream.Prepare(process, &cfg.Matrix.JetStream) + cfg := &base.Cfg.ClientAPI + mscCfg := &base.Cfg.MSCs + js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) syncProducer := &producers.SyncAPIProducer{ JetStream: js, @@ -62,7 +58,10 @@ func AddPublicRoutes( } routing.Setup( - router, synapseAdminRouter, cfg, rsAPI, asAPI, + base.PublicClientAPIMux, + base.SynapseAdminMux, + base.DendriteAdminMux, + cfg, rsAPI, asAPI, userAPI, userDirectoryProvider, federation, syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg, natsClient, diff --git a/clientapi/producers/syncapi.go b/clientapi/producers/syncapi.go index 187e3412d..48b1ae88d 100644 --- a/clientapi/producers/syncapi.go +++ b/clientapi/producers/syncapi.go @@ -38,7 +38,7 @@ type SyncAPIProducer struct { TopicPresenceEvent string JetStream nats.JetStreamContext ServerName gomatrixserverlib.ServerName - UserAPI userapi.UserInternalAPI + UserAPI userapi.ClientUserAPI } // SendData sends account data to the sync API server diff --git a/clientapi/routing/account_data.go b/clientapi/routing/account_data.go index d0dd3ab8d..a5a3014ab 100644 --- a/clientapi/routing/account_data.go +++ b/clientapi/routing/account_data.go @@ -33,7 +33,7 @@ import ( // GetAccountData implements GET /user/{userId}/[rooms/{roomid}/]account_data/{type} func GetAccountData( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, userID string, roomID string, dataType string, ) util.JSONResponse { if userID != device.UserID { @@ -76,7 +76,7 @@ func GetAccountData( // SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type} func SaveAccountData( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer, ) util.JSONResponse { if userID != device.UserID { @@ -152,7 +152,7 @@ type fullyReadEvent struct { // SaveReadMarker implements POST /rooms/{roomId}/read_markers func SaveReadMarker( req *http.Request, - userAPI api.UserInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI, + userAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, syncProducer *producers.SyncAPIProducer, device *api.Device, roomID string, ) util.JSONResponse { // Verify that the user is a member of this room diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go new file mode 100644 index 000000000..125b3847d --- /dev/null +++ b/clientapi/routing/admin.go @@ -0,0 +1,49 @@ +package routing + +import ( + "net/http" + + "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/dendrite/internal/httputil" + roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" + userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/util" +) + +func AdminEvacuateRoom(req *http.Request, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse { + if device.AccountType != userapi.AccountTypeAdmin { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("This API can only be used by admin users."), + } + } + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + roomID, ok := vars["roomID"] + if !ok { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.MissingArgument("Expecting room ID."), + } + } + res := &roomserverAPI.PerformAdminEvacuateRoomResponse{} + rsAPI.PerformAdminEvacuateRoom( + req.Context(), + &roomserverAPI.PerformAdminEvacuateRoomRequest{ + RoomID: roomID, + }, + res, + ) + if err := res.Error; err != nil { + return err.JSONResponse() + } + return util.JSONResponse{ + Code: 200, + JSON: map[string]interface{}{ + "affected": res.Affected, + }, + } +} diff --git a/clientapi/routing/admin_whois.go b/clientapi/routing/admin_whois.go index 87bb79366..f1cbd3467 100644 --- a/clientapi/routing/admin_whois.go +++ b/clientapi/routing/admin_whois.go @@ -44,7 +44,7 @@ type connectionInfo struct { // GetAdminWhois implements GET /admin/whois/{userId} func GetAdminWhois( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, userID string, ) util.JSONResponse { allowed := device.AccountType == api.AccountTypeAdmin || userID == device.UserID diff --git a/clientapi/routing/aliases.go b/clientapi/routing/aliases.go index 8c4830532..504d60265 100644 --- a/clientapi/routing/aliases.go +++ b/clientapi/routing/aliases.go @@ -28,7 +28,7 @@ import ( // GetAliases implements GET /_matrix/client/r0/rooms/{roomId}/aliases func GetAliases( - req *http.Request, rsAPI api.RoomserverInternalAPI, device *userapi.Device, roomID string, + req *http.Request, rsAPI api.ClientRoomserverAPI, device *userapi.Device, roomID string, ) util.JSONResponse { stateTuple := gomatrixserverlib.StateKeyTuple{ EventType: gomatrixserverlib.MRoomHistoryVisibility, diff --git a/clientapi/routing/capabilities.go b/clientapi/routing/capabilities.go index 72668fa5a..b7d47e916 100644 --- a/clientapi/routing/capabilities.go +++ b/clientapi/routing/capabilities.go @@ -26,7 +26,7 @@ import ( // GetCapabilities returns information about the server's supported feature set // and other relevant capabilities to an authenticated user. func GetCapabilities( - req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI, + req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, ) util.JSONResponse { roomVersionsQueryReq := roomserverAPI.QueryRoomVersionCapabilitiesRequest{} roomVersionsQueryRes := roomserverAPI.QueryRoomVersionCapabilitiesResponse{} diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index 4976b3e50..d40d84a79 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -137,8 +137,8 @@ type fledglingEvent struct { func CreateRoom( req *http.Request, device *api.Device, cfg *config.ClientAPI, - profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { var r createRoomRequest resErr := httputil.UnmarshalJSONRequest(req, &r) @@ -164,8 +164,8 @@ func createRoom( ctx context.Context, r createRoomRequest, device *api.Device, cfg *config.ClientAPI, - profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time, ) util.JSONResponse { // TODO (#267): Check room ID doesn't clash with an existing one, and we @@ -531,25 +531,23 @@ func createRoom( gomatrixserverlib.NewInviteV2StrippedState(inviteEvent.Event), ) // Send the invite event to the roomserver. - err = roomserverAPI.SendInvite( - ctx, - rsAPI, - inviteEvent.Headered(roomVersion), - inviteStrippedState, // invite room state - cfg.Matrix.ServerName, // send as server - nil, // transaction ID - ) - switch e := err.(type) { - case *roomserverAPI.PerformError: - return e.JSONResponse() - case nil: - default: - util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed") + var inviteRes roomserverAPI.PerformInviteResponse + event := inviteEvent.Headered(roomVersion) + if err := rsAPI.PerformInvite(ctx, &roomserverAPI.PerformInviteRequest{ + Event: event, + InviteRoomState: inviteStrippedState, + RoomVersion: event.RoomVersion, + SendAsServer: string(cfg.Matrix.ServerName), + }, &inviteRes); err != nil { + util.GetLogger(ctx).WithError(err).Error("PerformInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError(), } } + if inviteRes.Error != nil { + return inviteRes.Error.JSONResponse() + } } } diff --git a/clientapi/routing/deactivate.go b/clientapi/routing/deactivate.go index da1b6dcf9..c8aa6a3bc 100644 --- a/clientapi/routing/deactivate.go +++ b/clientapi/routing/deactivate.go @@ -15,7 +15,7 @@ import ( func Deactivate( req *http.Request, userInteractiveAuth *auth.UserInteractive, - accountAPI api.UserAccountAPI, + accountAPI api.ClientUserAPI, deviceAPI *api.Device, ) util.JSONResponse { ctx := req.Context() diff --git a/clientapi/routing/device.go b/clientapi/routing/device.go index 161bc2731..bb1cf47bd 100644 --- a/clientapi/routing/device.go +++ b/clientapi/routing/device.go @@ -50,7 +50,7 @@ type devicesDeleteJSON struct { // GetDeviceByID handles /devices/{deviceID} func GetDeviceByID( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, deviceID string, ) util.JSONResponse { var queryRes api.QueryDevicesResponse @@ -88,7 +88,7 @@ func GetDeviceByID( // GetDevicesByLocalpart handles /devices func GetDevicesByLocalpart( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, ) util.JSONResponse { var queryRes api.QueryDevicesResponse err := userAPI.QueryDevices(req.Context(), &api.QueryDevicesRequest{ @@ -118,7 +118,7 @@ func GetDevicesByLocalpart( // UpdateDeviceByID handles PUT on /devices/{deviceID} func UpdateDeviceByID( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, deviceID string, ) util.JSONResponse { @@ -161,7 +161,7 @@ func UpdateDeviceByID( // DeleteDeviceById handles DELETE requests to /devices/{deviceId} func DeleteDeviceById( - req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userInteractiveAuth *auth.UserInteractive, userAPI api.ClientUserAPI, device *api.Device, deviceID string, ) util.JSONResponse { var ( @@ -242,7 +242,7 @@ func DeleteDeviceById( // DeleteDevices handles POST requests to /delete_devices func DeleteDevices( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, ) util.JSONResponse { ctx := req.Context() payload := devicesDeleteJSON{} diff --git a/clientapi/routing/directory.go b/clientapi/routing/directory.go index ac355b5d4..53ba3f190 100644 --- a/clientapi/routing/directory.go +++ b/clientapi/routing/directory.go @@ -46,8 +46,8 @@ func DirectoryRoom( roomAlias string, federation *gomatrixserverlib.FederationClient, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - fedSenderAPI federationAPI.FederationInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + fedSenderAPI federationAPI.ClientFederationAPI, ) util.JSONResponse { _, domain, err := gomatrixserverlib.SplitID('#', roomAlias) if err != nil { @@ -117,7 +117,7 @@ func SetLocalAlias( device *userapi.Device, alias string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, ) util.JSONResponse { _, domain, err := gomatrixserverlib.SplitID('#', alias) if err != nil { @@ -199,7 +199,7 @@ func RemoveLocalAlias( req *http.Request, device *userapi.Device, alias string, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, ) util.JSONResponse { queryReq := roomserverAPI.RemoveRoomAliasRequest{ Alias: alias, @@ -237,7 +237,7 @@ type roomVisibility struct { // GetVisibility implements GET /directory/list/room/{roomID} func GetVisibility( - req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI, + req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, roomID string, ) util.JSONResponse { var res roomserverAPI.QueryPublishedRoomsResponse @@ -265,7 +265,7 @@ func GetVisibility( // SetVisibility implements PUT /directory/list/room/{roomID} // TODO: Allow admin users to edit the room visibility func SetVisibility( - req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI, dev *userapi.Device, + req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, dev *userapi.Device, roomID string, ) util.JSONResponse { resErr := checkMemberInRoom(req.Context(), rsAPI, dev.UserID, roomID) diff --git a/clientapi/routing/directory_public.go b/clientapi/routing/directory_public.go index 0dacfced5..c3e6141b2 100644 --- a/clientapi/routing/directory_public.go +++ b/clientapi/routing/directory_public.go @@ -50,7 +50,7 @@ type filter struct { // GetPostPublicRooms implements GET and POST /publicRooms func GetPostPublicRooms( - req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI, + req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider, federation *gomatrixserverlib.FederationClient, cfg *config.ClientAPI, @@ -91,7 +91,7 @@ func GetPostPublicRooms( } func publicRooms( - ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.RoomserverInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider, + ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider, ) (*gomatrixserverlib.RespPublicRooms, error) { response := gomatrixserverlib.RespPublicRooms{ @@ -229,7 +229,7 @@ func sliceInto(slice []gomatrixserverlib.PublicRoom, since int64, limit int16) ( } func refreshPublicRoomCache( - ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider, + ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, extRoomsProvider api.ExtraPublicRoomsProvider, ) []gomatrixserverlib.PublicRoom { cacheMu.Lock() defer cacheMu.Unlock() diff --git a/clientapi/routing/getevent.go b/clientapi/routing/getevent.go index 36f3ee9e3..7f5842800 100644 --- a/clientapi/routing/getevent.go +++ b/clientapi/routing/getevent.go @@ -31,7 +31,6 @@ type getEventRequest struct { roomID string eventID string cfg *config.ClientAPI - federation *gomatrixserverlib.FederationClient requestedEvent *gomatrixserverlib.Event } @@ -43,8 +42,7 @@ func GetEvent( roomID string, eventID string, cfg *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, - federation *gomatrixserverlib.FederationClient, + rsAPI api.ClientRoomserverAPI, ) util.JSONResponse { eventsReq := api.QueryEventsByIDRequest{ EventIDs: []string{eventID}, @@ -72,7 +70,6 @@ func GetEvent( roomID: roomID, eventID: eventID, cfg: cfg, - federation: federation, requestedEvent: requestedEvent, } diff --git a/clientapi/routing/joinroom.go b/clientapi/routing/joinroom.go index dc15f4bda..4e6acebc3 100644 --- a/clientapi/routing/joinroom.go +++ b/clientapi/routing/joinroom.go @@ -29,8 +29,8 @@ import ( func JoinRoomByIDOrAlias( req *http.Request, device *api.Device, - rsAPI roomserverAPI.RoomserverInternalAPI, - profileAPI api.UserProfileAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + profileAPI api.ClientUserAPI, roomIDOrAlias string, ) util.JSONResponse { // Prepare to ask the roomserver to perform the room join. diff --git a/clientapi/routing/key_backup.go b/clientapi/routing/key_backup.go index 9d2ff87fd..28c80415b 100644 --- a/clientapi/routing/key_backup.go +++ b/clientapi/routing/key_backup.go @@ -55,7 +55,7 @@ type keyBackupSessionResponse struct { // 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 { +func CreateKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device) util.JSONResponse { var kb keyBackupVersion resErr := httputil.UnmarshalJSONRequest(req, &kb) if resErr != nil { @@ -89,7 +89,7 @@ func CreateKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, // 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 { +func KeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse { var queryResp userapi.QueryKeyBackupResponse userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{ UserID: device.UserID, @@ -118,7 +118,7 @@ func KeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, device // 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 { +func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse { var kb keyBackupVersion resErr := httputil.UnmarshalJSONRequest(req, &kb) if resErr != nil { @@ -159,7 +159,7 @@ func ModifyKeyBackupVersionAuthData(req *http.Request, userAPI userapi.UserInter // 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 { +func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string) util.JSONResponse { var performKeyBackupResp userapi.PerformKeyBackupResponse if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{ UserID: device.UserID, @@ -194,7 +194,7 @@ func DeleteKeyBackupVersion(req *http.Request, userAPI userapi.UserInternalAPI, // 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, + req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version string, keys *keyBackupSessionRequest, ) util.JSONResponse { var performKeyBackupResp userapi.PerformKeyBackupResponse if err := userAPI.PerformKeyBackup(req.Context(), &userapi.PerformKeyBackupRequest{ @@ -230,7 +230,7 @@ func UploadBackupKeys( // 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, + req *http.Request, userAPI userapi.ClientUserAPI, device *userapi.Device, version, roomID, sessionID string, ) util.JSONResponse { var queryResp userapi.QueryKeyBackupResponse userAPI.QueryKeyBackup(req.Context(), &userapi.QueryKeyBackupRequest{ diff --git a/clientapi/routing/key_crosssigning.go b/clientapi/routing/key_crosssigning.go index c73e0a10d..8fbb86f7a 100644 --- a/clientapi/routing/key_crosssigning.go +++ b/clientapi/routing/key_crosssigning.go @@ -34,8 +34,8 @@ type crossSigningRequest struct { func UploadCrossSigningDeviceKeys( req *http.Request, userInteractiveAuth *auth.UserInteractive, - keyserverAPI api.KeyInternalAPI, device *userapi.Device, - accountAPI userapi.UserAccountAPI, cfg *config.ClientAPI, + keyserverAPI api.ClientKeyAPI, device *userapi.Device, + accountAPI userapi.ClientUserAPI, cfg *config.ClientAPI, ) util.JSONResponse { uploadReq := &crossSigningRequest{} uploadRes := &api.PerformUploadDeviceKeysResponse{} @@ -105,7 +105,7 @@ func UploadCrossSigningDeviceKeys( } } -func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse { +func UploadCrossSigningDeviceSignatures(req *http.Request, keyserverAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse { uploadReq := &api.PerformUploadDeviceSignaturesRequest{} uploadRes := &api.PerformUploadDeviceSignaturesResponse{} diff --git a/clientapi/routing/keys.go b/clientapi/routing/keys.go index 2d65ac353..fdda34a53 100644 --- a/clientapi/routing/keys.go +++ b/clientapi/routing/keys.go @@ -31,7 +31,7 @@ type uploadKeysRequest struct { OneTimeKeys map[string]json.RawMessage `json:"one_time_keys"` } -func UploadKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse { +func UploadKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse { var r uploadKeysRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { @@ -100,7 +100,7 @@ func (r *queryKeysRequest) GetTimeout() time.Duration { return time.Duration(r.Timeout) * time.Millisecond } -func QueryKeys(req *http.Request, keyAPI api.KeyInternalAPI, device *userapi.Device) util.JSONResponse { +func QueryKeys(req *http.Request, keyAPI api.ClientKeyAPI, device *userapi.Device) util.JSONResponse { var r queryKeysRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { @@ -138,7 +138,7 @@ func (r *claimKeysRequest) GetTimeout() time.Duration { return time.Duration(r.TimeoutMS) * time.Millisecond } -func ClaimKeys(req *http.Request, keyAPI api.KeyInternalAPI) util.JSONResponse { +func ClaimKeys(req *http.Request, keyAPI api.ClientKeyAPI) util.JSONResponse { var r claimKeysRequest resErr := httputil.UnmarshalJSONRequest(req, &r) if resErr != nil { diff --git a/clientapi/routing/leaveroom.go b/clientapi/routing/leaveroom.go index a34dd02d3..a71661851 100644 --- a/clientapi/routing/leaveroom.go +++ b/clientapi/routing/leaveroom.go @@ -26,7 +26,7 @@ import ( func LeaveRoomByID( req *http.Request, device *api.Device, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, roomID string, ) util.JSONResponse { // Prepare to ask the roomserver to perform the room join. diff --git a/clientapi/routing/login.go b/clientapi/routing/login.go index 2329df504..6017b5840 100644 --- a/clientapi/routing/login.go +++ b/clientapi/routing/login.go @@ -53,7 +53,7 @@ func passwordLogin() flows { // Login implements GET and POST /login func Login( - req *http.Request, userAPI userapi.UserInternalAPI, + req *http.Request, userAPI userapi.ClientUserAPI, cfg *config.ClientAPI, ) util.JSONResponse { if req.Method == http.MethodGet { @@ -79,7 +79,7 @@ func Login( } func completeAuth( - ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI userapi.UserInternalAPI, login *auth.Login, + ctx context.Context, serverName gomatrixserverlib.ServerName, userAPI userapi.ClientUserAPI, login *auth.Login, ipAddr, userAgent string, ) util.JSONResponse { token, err := auth.GenerateAccessToken() diff --git a/clientapi/routing/logout.go b/clientapi/routing/logout.go index cfbb6f9f2..73bae7af7 100644 --- a/clientapi/routing/logout.go +++ b/clientapi/routing/logout.go @@ -24,7 +24,7 @@ import ( // Logout handles POST /logout func Logout( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, ) util.JSONResponse { var performRes api.PerformDeviceDeletionResponse err := userAPI.PerformDeviceDeletion(req.Context(), &api.PerformDeviceDeletionRequest{ @@ -44,7 +44,7 @@ func Logout( // LogoutAll handles POST /logout/all func LogoutAll( - req *http.Request, userAPI api.UserInternalAPI, device *api.Device, + req *http.Request, userAPI api.ClientUserAPI, device *api.Device, ) util.JSONResponse { var performRes api.PerformDeviceDeletionResponse err := userAPI.PerformDeviceDeletion(req.Context(), &api.PerformDeviceDeletionRequest{ diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go index df8447b14..77f627eb2 100644 --- a/clientapi/routing/membership.go +++ b/clientapi/routing/membership.go @@ -27,6 +27,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/clientapi/threepid" "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/roomserver/api" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" @@ -38,9 +39,9 @@ import ( var errMissingUserID = errors.New("'user_id' must be supplied") func SendBan( - req *http.Request, profileAPI userapi.UserProfileAPI, device *userapi.Device, + req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -80,10 +81,10 @@ func SendBan( return sendMembership(req.Context(), profileAPI, device, roomID, "ban", body.Reason, cfg, body.UserID, evTime, roomVer, rsAPI, asAPI) } -func sendMembership(ctx context.Context, profileAPI userapi.UserProfileAPI, device *userapi.Device, +func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI) util.JSONResponse { + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse { event, err := buildMembershipEvent( ctx, targetUserID, reason, profileAPI, device, membership, @@ -124,9 +125,9 @@ func sendMembership(ctx context.Context, profileAPI userapi.UserProfileAPI, devi } func SendKick( - req *http.Request, profileAPI userapi.UserProfileAPI, device *userapi.Device, + req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -164,9 +165,9 @@ func SendKick( } func SendUnban( - req *http.Request, profileAPI userapi.UserProfileAPI, device *userapi.Device, + req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -187,6 +188,12 @@ func SendUnban( if err != nil { return util.ErrorResponse(err) } + if !queryRes.RoomExists { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("room does not exist"), + } + } // unban is only valid if the user is currently banned if queryRes.Membership != "ban" { return util.JSONResponse{ @@ -199,9 +206,9 @@ func SendUnban( } func SendInvite( - req *http.Request, profileAPI userapi.UserProfileAPI, device *userapi.Device, + req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -233,12 +240,12 @@ func SendInvite( // sendInvite sends an invitation to a user. Returns a JSONResponse and an error func sendInvite( ctx context.Context, - profileAPI userapi.UserProfileAPI, + profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID, userID, reason string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, evTime time.Time, + rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time, ) (util.JSONResponse, error) { event, err := buildMembershipEvent( ctx, userID, reason, profileAPI, device, "invite", @@ -259,37 +266,36 @@ func sendInvite( return jsonerror.InternalServerError(), err } - err = roomserverAPI.SendInvite( - ctx, rsAPI, - event, - nil, // ask the roomserver to draw up invite room state for us - cfg.Matrix.ServerName, - nil, - ) - switch e := err.(type) { - case *roomserverAPI.PerformError: - return e.JSONResponse(), err - case nil: - return util.JSONResponse{ - Code: http.StatusOK, - JSON: struct{}{}, - }, nil - default: - util.GetLogger(ctx).WithError(err).Error("roomserverAPI.SendInvite failed") + var inviteRes api.PerformInviteResponse + if err := rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{ + Event: event, + InviteRoomState: nil, // ask the roomserver to draw up invite room state for us + RoomVersion: event.RoomVersion, + SendAsServer: string(cfg.Matrix.ServerName), + }, &inviteRes); err != nil { + util.GetLogger(ctx).WithError(err).Error("PerformInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError(), }, err } + if inviteRes.Error != nil { + return inviteRes.Error.JSONResponse(), inviteRes.Error + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: struct{}{}, + }, nil } func buildMembershipEvent( ctx context.Context, - targetUserID, reason string, profileAPI userapi.UserProfileAPI, + targetUserID, reason string, profileAPI userapi.ClientUserAPI, device *userapi.Device, membership, roomID string, isDirect bool, cfg *config.ClientAPI, evTime time.Time, - rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) (*gomatrixserverlib.HeaderedEvent, error) { profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI) if err != nil { @@ -326,8 +332,8 @@ func loadProfile( ctx context.Context, userID string, cfg *config.ClientAPI, - profileAPI userapi.UserProfileAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + profileAPI userapi.ClientUserAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) (*authtypes.Profile, error) { _, serverName, err := gomatrixserverlib.SplitID('@', userID) if err != nil { @@ -344,7 +350,7 @@ func loadProfile( return profile, err } -func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.RoomserverInternalAPI) ( +func extractRequestData(req *http.Request, roomID string, rsAPI roomserverAPI.ClientRoomserverAPI) ( body *threepid.MembershipRequest, evTime time.Time, roomVer gomatrixserverlib.RoomVersion, resErr *util.JSONResponse, ) { verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID} @@ -379,8 +385,8 @@ func checkAndProcessThreepid( device *userapi.Device, body *threepid.MembershipRequest, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - profileAPI userapi.UserProfileAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + profileAPI userapi.ClientUserAPI, roomID string, evTime time.Time, ) (inviteStored bool, errRes *util.JSONResponse) { @@ -418,7 +424,7 @@ func checkAndProcessThreepid( return } -func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, userID, roomID string) *util.JSONResponse { +func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse { tuple := gomatrixserverlib.StateKeyTuple{ EventType: gomatrixserverlib.MRoomMember, StateKey: userID, @@ -457,7 +463,7 @@ func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.RoomserverIntern func SendForget( req *http.Request, device *userapi.Device, - roomID string, rsAPI roomserverAPI.RoomserverInternalAPI, + roomID string, rsAPI roomserverAPI.ClientRoomserverAPI, ) util.JSONResponse { ctx := req.Context() logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID) @@ -471,6 +477,12 @@ func SendForget( logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user") return jsonerror.InternalServerError() } + if !membershipRes.RoomExists { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("room does not exist"), + } + } if membershipRes.IsInRoom { return util.JSONResponse{ Code: http.StatusBadRequest, diff --git a/clientapi/routing/memberships.go b/clientapi/routing/memberships.go index 6ddcf1be3..9bdd8a4f4 100644 --- a/clientapi/routing/memberships.go +++ b/clientapi/routing/memberships.go @@ -55,7 +55,7 @@ type databaseJoinedMember struct { func GetMemberships( req *http.Request, device *userapi.Device, roomID string, joinedOnly bool, _ *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.ClientRoomserverAPI, ) util.JSONResponse { queryReq := api.QueryMembershipsForRoomRequest{ JoinedOnly: joinedOnly, @@ -100,7 +100,7 @@ func GetMemberships( func GetJoinedRooms( req *http.Request, device *userapi.Device, - rsAPI api.RoomserverInternalAPI, + rsAPI api.ClientRoomserverAPI, ) util.JSONResponse { var res api.QueryRoomsForUserResponse err := rsAPI.QueryRoomsForUser(req.Context(), &api.QueryRoomsForUserRequest{ diff --git a/clientapi/routing/notification.go b/clientapi/routing/notification.go index ee715d323..8a424a141 100644 --- a/clientapi/routing/notification.go +++ b/clientapi/routing/notification.go @@ -27,7 +27,7 @@ import ( // GetNotifications handles /_matrix/client/r0/notifications func GetNotifications( req *http.Request, device *userapi.Device, - userAPI userapi.UserInternalAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { var limit int64 if limitStr := req.URL.Query().Get("limit"); limitStr != "" { diff --git a/clientapi/routing/openid.go b/clientapi/routing/openid.go index 13656e288..cfb440bea 100644 --- a/clientapi/routing/openid.go +++ b/clientapi/routing/openid.go @@ -34,7 +34,7 @@ type openIDTokenResponse struct { // can supply to an OpenID Relying Party to verify their identity func CreateOpenIDToken( req *http.Request, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, device *api.Device, userID string, cfg *config.ClientAPI, diff --git a/clientapi/routing/password.go b/clientapi/routing/password.go index 08ce1ffa1..6dc9af508 100644 --- a/clientapi/routing/password.go +++ b/clientapi/routing/password.go @@ -28,7 +28,7 @@ type newPasswordAuth struct { func Password( req *http.Request, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, device *api.Device, cfg *config.ClientAPI, ) util.JSONResponse { diff --git a/clientapi/routing/peekroom.go b/clientapi/routing/peekroom.go index 41d1ff004..d0eeccf17 100644 --- a/clientapi/routing/peekroom.go +++ b/clientapi/routing/peekroom.go @@ -26,7 +26,7 @@ import ( func PeekRoomByIDOrAlias( req *http.Request, device *api.Device, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, roomIDOrAlias string, ) util.JSONResponse { // if this is a remote roomIDOrAlias, we have to ask the roomserver (or federation sender?) to @@ -79,7 +79,7 @@ func PeekRoomByIDOrAlias( func UnpeekRoomByID( req *http.Request, device *api.Device, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, roomID string, ) util.JSONResponse { unpeekReq := roomserverAPI.PerformUnpeekRequest{ diff --git a/clientapi/routing/profile.go b/clientapi/routing/profile.go index 3f91b4c93..0685c7352 100644 --- a/clientapi/routing/profile.go +++ b/clientapi/routing/profile.go @@ -35,9 +35,9 @@ import ( // GetProfile implements GET /profile/{userID} func GetProfile( - req *http.Request, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, userID string, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation) @@ -64,8 +64,8 @@ func GetProfile( // GetAvatarURL implements GET /profile/{userID}/avatar_url func GetAvatarURL( - req *http.Request, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, - userID string, asAPI appserviceAPI.AppServiceQueryAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, + userID string, asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation) @@ -91,8 +91,8 @@ func GetAvatarURL( // SetAvatarURL implements PUT /profile/{userID}/avatar_url func SetAvatarURL( - req *http.Request, profileAPI userapi.UserProfileAPI, - device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.RoomserverInternalAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, + device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI, ) util.JSONResponse { if userID != device.UserID { return util.JSONResponse{ @@ -193,8 +193,8 @@ func SetAvatarURL( // GetDisplayName implements GET /profile/{userID}/displayname func GetDisplayName( - req *http.Request, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, - userID string, asAPI appserviceAPI.AppServiceQueryAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, + userID string, asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation) @@ -220,8 +220,8 @@ func GetDisplayName( // SetDisplayName implements PUT /profile/{userID}/displayname func SetDisplayName( - req *http.Request, profileAPI userapi.UserProfileAPI, - device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.RoomserverInternalAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, + device *userapi.Device, userID string, cfg *config.ClientAPI, rsAPI api.ClientRoomserverAPI, ) util.JSONResponse { if userID != device.UserID { return util.JSONResponse{ @@ -325,9 +325,9 @@ func SetDisplayName( // Returns an error when something goes wrong or specifically // eventutil.ErrProfileNoExists when the profile doesn't exist. func getProfile( - ctx context.Context, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, + ctx context.Context, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, userID string, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) (*authtypes.Profile, error) { localpart, domain, err := gomatrixserverlib.SplitID('@', userID) @@ -366,7 +366,7 @@ func buildMembershipEvents( ctx context.Context, roomIDs []string, newProfile authtypes.Profile, userID string, cfg *config.ClientAPI, - evTime time.Time, rsAPI api.RoomserverInternalAPI, + evTime time.Time, rsAPI api.ClientRoomserverAPI, ) ([]*gomatrixserverlib.HeaderedEvent, error) { evs := []*gomatrixserverlib.HeaderedEvent{} diff --git a/clientapi/routing/pusher.go b/clientapi/routing/pusher.go index 9d6bef8bd..d6a6eb936 100644 --- a/clientapi/routing/pusher.go +++ b/clientapi/routing/pusher.go @@ -28,7 +28,7 @@ import ( // GetPushers handles /_matrix/client/r0/pushers func GetPushers( req *http.Request, device *userapi.Device, - userAPI userapi.UserInternalAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { var queryRes userapi.QueryPushersResponse localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID) @@ -57,7 +57,7 @@ func GetPushers( // The behaviour of this endpoint varies depending on the values in the JSON body. func SetPusher( req *http.Request, device *userapi.Device, - userAPI userapi.UserInternalAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID) if err != nil { diff --git a/clientapi/routing/pushrules.go b/clientapi/routing/pushrules.go index 81a33b25a..856f52c75 100644 --- a/clientapi/routing/pushrules.go +++ b/clientapi/routing/pushrules.go @@ -30,7 +30,7 @@ func errorResponse(ctx context.Context, err error, msg string, args ...interface return jsonerror.InternalServerError() } -func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { ruleSets, err := queryPushRules(ctx, device.UserID, userAPI) if err != nil { return errorResponse(ctx, err, "queryPushRulesJSON failed") @@ -41,7 +41,7 @@ func GetAllPushRules(ctx context.Context, device *userapi.Device, userAPI userap } } -func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { ruleSets, err := queryPushRules(ctx, device.UserID, userAPI) if err != nil { return errorResponse(ctx, err, "queryPushRulesJSON failed") @@ -56,7 +56,7 @@ func GetPushRulesByScope(ctx context.Context, scope string, device *userapi.Devi } } -func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { ruleSets, err := queryPushRules(ctx, device.UserID, userAPI) if err != nil { return errorResponse(ctx, err, "queryPushRules failed") @@ -75,7 +75,7 @@ func GetPushRulesByKind(ctx context.Context, scope, kind string, device *userapi } } -func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { ruleSets, err := queryPushRules(ctx, device.UserID, userAPI) if err != nil { return errorResponse(ctx, err, "queryPushRules failed") @@ -98,7 +98,7 @@ func GetPushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device } } -func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, beforeRuleID string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { var newRule pushrules.Rule if err := json.NewDecoder(body).Decode(&newRule); err != nil { return errorResponse(ctx, err, "JSON Decode failed") @@ -160,7 +160,7 @@ func PutPushRuleByRuleID(ctx context.Context, scope, kind, ruleID, afterRuleID, return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}} } -func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { ruleSets, err := queryPushRules(ctx, device.UserID, userAPI) if err != nil { return errorResponse(ctx, err, "queryPushRules failed") @@ -187,7 +187,7 @@ func DeletePushRuleByRuleID(ctx context.Context, scope, kind, ruleID string, dev return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}} } -func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { attrGet, err := pushRuleAttrGetter(attr) if err != nil { return errorResponse(ctx, err, "pushRuleAttrGetter failed") @@ -216,7 +216,7 @@ func GetPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri } } -func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, body io.Reader, device *userapi.Device, userAPI userapi.UserInternalAPI) util.JSONResponse { +func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr string, body io.Reader, device *userapi.Device, userAPI userapi.ClientUserAPI) util.JSONResponse { var newPartialRule pushrules.Rule if err := json.NewDecoder(body).Decode(&newPartialRule); err != nil { return util.JSONResponse{ @@ -266,7 +266,7 @@ func PutPushRuleAttrByRuleID(ctx context.Context, scope, kind, ruleID, attr stri return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}} } -func queryPushRules(ctx context.Context, userID string, userAPI userapi.UserInternalAPI) (*pushrules.AccountRuleSets, error) { +func queryPushRules(ctx context.Context, userID string, userAPI userapi.ClientUserAPI) (*pushrules.AccountRuleSets, error) { var res userapi.QueryPushRulesResponse if err := userAPI.QueryPushRules(ctx, &userapi.QueryPushRulesRequest{UserID: userID}, &res); err != nil { util.GetLogger(ctx).WithError(err).Error("userAPI.QueryPushRules failed") @@ -275,7 +275,7 @@ func queryPushRules(ctx context.Context, userID string, userAPI userapi.UserInte return res.RuleSets, nil } -func putPushRules(ctx context.Context, userID string, ruleSets *pushrules.AccountRuleSets, userAPI userapi.UserInternalAPI) error { +func putPushRules(ctx context.Context, userID string, ruleSets *pushrules.AccountRuleSets, userAPI userapi.ClientUserAPI) error { req := userapi.PerformPushRulesPutRequest{ UserID: userID, RuleSets: ruleSets, diff --git a/clientapi/routing/redaction.go b/clientapi/routing/redaction.go index 01ea818ab..27f0ba5d0 100644 --- a/clientapi/routing/redaction.go +++ b/clientapi/routing/redaction.go @@ -22,6 +22,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/internal/transactions" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" @@ -39,13 +40,22 @@ type redactionResponse struct { func SendRedaction( req *http.Request, device *userapi.Device, roomID, eventID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + txnID *string, + txnCache *transactions.Cache, ) util.JSONResponse { resErr := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID) if resErr != nil { return *resErr } + if txnID != nil { + // Try to fetch response from transactionsCache + if res, ok := txnCache.FetchTransaction(device.AccessToken, *txnID); ok { + return *res + } + } + ev := roomserverAPI.GetEvent(req.Context(), rsAPI, eventID) if ev == nil { return util.JSONResponse{ @@ -124,10 +134,18 @@ func SendRedaction( util.GetLogger(req.Context()).WithError(err).Errorf("failed to SendEvents") return jsonerror.InternalServerError() } - return util.JSONResponse{ + + res := util.JSONResponse{ Code: 200, JSON: redactionResponse{ EventID: e.EventID(), }, } + + // Add response to transactionsCache + if txnID != nil { + txnCache.AddTransaction(device.AccessToken, *txnID, &res) + } + + return res } diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go index 8253f3155..eba4920c6 100644 --- a/clientapi/routing/register.go +++ b/clientapi/routing/register.go @@ -518,7 +518,7 @@ func validateApplicationService( // http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register func Register( req *http.Request, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, cfg *config.ClientAPI, ) util.JSONResponse { defer req.Body.Close() // nolint: errcheck @@ -614,7 +614,7 @@ func handleGuestRegistration( req *http.Request, r registerRequest, cfg *config.ClientAPI, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { if cfg.RegistrationDisabled || cfg.GuestsDisabled { return util.JSONResponse{ @@ -679,7 +679,7 @@ func handleRegistrationFlow( r registerRequest, sessionID string, cfg *config.ClientAPI, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, accessToken string, accessTokenErr error, ) util.JSONResponse { @@ -768,7 +768,7 @@ func handleApplicationServiceRegistration( req *http.Request, r registerRequest, cfg *config.ClientAPI, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { // Check if we previously had issues extracting the access token from the // request. @@ -806,7 +806,7 @@ func checkAndCompleteFlow( r registerRequest, sessionID string, cfg *config.ClientAPI, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, ) util.JSONResponse { if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) { // This flow was completed, registration can continue @@ -833,7 +833,7 @@ func checkAndCompleteFlow( // not all func completeRegistration( ctx context.Context, - userAPI userapi.UserRegisterAPI, + userAPI userapi.ClientUserAPI, username, password, appserviceID, ipAddr, userAgent, sessionID string, inhibitLogin eventutil.WeakBoolean, displayName, deviceID *string, @@ -992,7 +992,7 @@ type availableResponse struct { func RegisterAvailable( req *http.Request, cfg *config.ClientAPI, - registerAPI userapi.UserRegisterAPI, + registerAPI userapi.ClientUserAPI, ) util.JSONResponse { username := req.URL.Query().Get("username") @@ -1040,7 +1040,7 @@ func RegisterAvailable( } } -func handleSharedSecretRegistration(userAPI userapi.UserInternalAPI, sr *SharedSecretRegistration, req *http.Request) util.JSONResponse { +func handleSharedSecretRegistration(userAPI userapi.ClientUserAPI, sr *SharedSecretRegistration, req *http.Request) util.JSONResponse { ssrr, err := NewSharedSecretRegistrationRequest(req.Body) if err != nil { return util.JSONResponse{ diff --git a/clientapi/routing/room_tagging.go b/clientapi/routing/room_tagging.go index ce173613e..039289569 100644 --- a/clientapi/routing/room_tagging.go +++ b/clientapi/routing/room_tagging.go @@ -31,7 +31,7 @@ import ( // GetTags implements GET /_matrix/client/r0/user/{userID}/rooms/{roomID}/tags func GetTags( req *http.Request, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, device *api.Device, userID string, roomID string, @@ -62,7 +62,7 @@ func GetTags( // the tag to the "map" and saving the new "map" to the DB func PutTag( req *http.Request, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, device *api.Device, userID string, roomID string, @@ -113,7 +113,7 @@ func PutTag( // the "map" and then saving the new "map" in the DB func DeleteTag( req *http.Request, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, device *api.Device, userID string, roomID string, @@ -167,7 +167,7 @@ func obtainSavedTags( req *http.Request, userID string, roomID string, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, ) (tags gomatrix.TagContent, err error) { dataReq := api.QueryAccountDataRequest{ UserID: userID, @@ -194,7 +194,7 @@ func saveTagData( req *http.Request, userID string, roomID string, - userAPI api.UserInternalAPI, + userAPI api.ClientUserAPI, Tag gomatrix.TagContent, ) error { newTagData, err := json.Marshal(Tag) diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 37d825b80..94becf465 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -48,16 +48,17 @@ import ( // applied: // nolint: gocyclo func Setup( - publicAPIMux, synapseAdminRouter *mux.Router, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, - userAPI userapi.UserInternalAPI, - userDirectoryProvider userapi.UserDirectoryProvider, + publicAPIMux, synapseAdminRouter, dendriteAdminRouter *mux.Router, + cfg *config.ClientAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, + userAPI userapi.ClientUserAPI, + userDirectoryProvider userapi.QuerySearchProfilesAPI, federation *gomatrixserverlib.FederationClient, syncProducer *producers.SyncAPIProducer, transactionsCache *transactions.Cache, - federationSender federationAPI.FederationInternalAPI, - keyAPI keyserverAPI.KeyInternalAPI, + federationSender federationAPI.ClientFederationAPI, + keyAPI keyserverAPI.ClientKeyAPI, extRoomsProvider api.ExtraPublicRoomsProvider, mscCfg *config.MSCs, natsClient *nats.Conn, ) { @@ -119,6 +120,12 @@ func Setup( ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions) } + dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}", + httputil.MakeAuthAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + return AdminEvacuateRoom(req, device, rsAPI) + }), + ).Methods(http.MethodGet, http.MethodOptions) + // server notifications if cfg.Matrix.ServerNotices.Enabled { logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") @@ -318,7 +325,7 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return GetEvent(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI, federation) + return GetEvent(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI) }), ).Methods(http.MethodGet, http.MethodOptions) @@ -479,7 +486,7 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI) + return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI, nil, nil) }), ).Methods(http.MethodPost, http.MethodOptions) v3mux.Handle("/rooms/{roomID}/redact/{eventID}/{txnId}", @@ -488,7 +495,8 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI) + txnID := vars["txnId"] + return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI, &txnID, transactionsCache) }), ).Methods(http.MethodPut, http.MethodOptions) @@ -889,7 +897,7 @@ func Setup( if resErr := clientutil.UnmarshalJSONRequest(req, &postContent); resErr != nil { return *resErr } - return *SearchUserDirectory( + return SearchUserDirectory( req.Context(), device, userAPI, diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go index 1211fa72d..5f84739d0 100644 --- a/clientapi/routing/sendevent.go +++ b/clientapi/routing/sendevent.go @@ -70,7 +70,7 @@ func SendEvent( device *userapi.Device, roomID, eventType string, txnID, stateKey *string, cfg *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.ClientRoomserverAPI, txnCache *transactions.Cache, ) util.JSONResponse { verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID} @@ -207,7 +207,7 @@ func generateSendEvent( device *userapi.Device, roomID, eventType string, stateKey *string, cfg *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.ClientRoomserverAPI, evTime time.Time, ) (*gomatrixserverlib.Event, *util.JSONResponse) { // parse the incoming http request diff --git a/clientapi/routing/sendtyping.go b/clientapi/routing/sendtyping.go index 6a27ee615..3f92e4227 100644 --- a/clientapi/routing/sendtyping.go +++ b/clientapi/routing/sendtyping.go @@ -32,7 +32,7 @@ type typingContentJSON struct { // sends the typing events to client API typingProducer func SendTyping( req *http.Request, device *userapi.Device, roomID string, - userID string, rsAPI roomserverAPI.RoomserverInternalAPI, + userID string, rsAPI roomserverAPI.ClientRoomserverAPI, syncProducer *producers.SyncAPIProducer, ) util.JSONResponse { if device.UserID != userID { diff --git a/clientapi/routing/server_notices.go b/clientapi/routing/server_notices.go index eec3d7e38..9edeed2f7 100644 --- a/clientapi/routing/server_notices.go +++ b/clientapi/routing/server_notices.go @@ -21,6 +21,7 @@ import ( "net/http" "time" + "github.com/matrix-org/dendrite/roomserver/version" "github.com/matrix-org/gomatrix" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/tokens" @@ -55,9 +56,9 @@ func SendServerNotice( req *http.Request, cfgNotices *config.ServerNotices, cfgClient *config.ClientAPI, - userAPI userapi.UserInternalAPI, - rsAPI api.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + userAPI userapi.ClientUserAPI, + rsAPI api.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, device *userapi.Device, senderDevice *userapi.Device, txnID *string, @@ -95,29 +96,16 @@ func SendServerNotice( // get rooms for specified user allUserRooms := []string{} userRooms := api.QueryRoomsForUserResponse{} - if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ - UserID: r.UserID, - WantMembership: "join", - }, &userRooms); err != nil { - return util.ErrorResponse(err) + // Get rooms the user is either joined, invited or has left. + for _, membership := range []string{"join", "invite", "leave"} { + if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ + UserID: r.UserID, + WantMembership: membership, + }, &userRooms); err != nil { + return util.ErrorResponse(err) + } + allUserRooms = append(allUserRooms, userRooms.RoomIDs...) } - allUserRooms = append(allUserRooms, userRooms.RoomIDs...) - // get invites for specified user - if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ - UserID: r.UserID, - WantMembership: "invite", - }, &userRooms); err != nil { - return util.ErrorResponse(err) - } - allUserRooms = append(allUserRooms, userRooms.RoomIDs...) - // get left rooms for specified user - if err := rsAPI.QueryRoomsForUser(ctx, &api.QueryRoomsForUserRequest{ - UserID: r.UserID, - WantMembership: "leave", - }, &userRooms); err != nil { - return util.ErrorResponse(err) - } - allUserRooms = append(allUserRooms, userRooms.RoomIDs...) // get rooms of the sender senderUserID := fmt.Sprintf("@%s:%s", cfgNotices.LocalPart, cfgClient.Matrix.ServerName) @@ -145,7 +133,7 @@ func SendServerNotice( var ( roomID string - roomVersion = gomatrixserverlib.RoomVersionV6 + roomVersion = version.DefaultRoomVersion() ) // create a new room for the user @@ -194,14 +182,21 @@ func SendServerNotice( // if we didn't get a createRoomResponse, we probably received an error, so return that. return roomRes } - } else { // we've found a room in common, check the membership roomID = commonRooms[0] - // re-invite the user - res, err := sendInvite(ctx, userAPI, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now()) + membershipRes := api.QueryMembershipForUserResponse{} + err := rsAPI.QueryMembershipForUser(ctx, &api.QueryMembershipForUserRequest{UserID: r.UserID, RoomID: roomID}, &membershipRes) if err != nil { - return res + util.GetLogger(ctx).WithError(err).Error("unable to query membership for user") + return jsonerror.InternalServerError() + } + if !membershipRes.IsInRoom { + // re-invite the user + res, err := sendInvite(ctx, userAPI, senderDevice, roomID, r.UserID, "Server notice room", cfgClient, rsAPI, asAPI, time.Now()) + if err != nil { + return res + } } } @@ -281,7 +276,7 @@ func (r sendServerNoticeRequest) valid() (ok bool) { // It returns an userapi.Device, which is used for building the event func getSenderDevice( ctx context.Context, - userAPI userapi.UserInternalAPI, + userAPI userapi.ClientUserAPI, cfg *config.ClientAPI, ) (*userapi.Device, error) { var accRes userapi.PerformAccountCreationResponse diff --git a/clientapi/routing/state.go b/clientapi/routing/state.go index d25ee8237..12984c39a 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -41,7 +41,7 @@ type stateEventInStateResp struct { // TODO: Check if the user is in the room. If not, check if the room's history // is publicly visible. Current behaviour is returning an empty array if the // user cannot see the room's history. -func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, roomID string) util.JSONResponse { +func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI api.ClientRoomserverAPI, roomID string) util.JSONResponse { var worldReadable bool var wantLatestState bool @@ -56,6 +56,12 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a util.GetLogger(ctx).WithError(err).Error("queryAPI.QueryLatestEventsAndState failed") return jsonerror.InternalServerError() } + if !stateRes.RoomExists { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("room does not exist"), + } + } // Look at the room state and see if we have a history visibility event // that marks the room as world-readable. If we don't then we assume that @@ -162,7 +168,7 @@ func OnIncomingStateRequest(ctx context.Context, device *userapi.Device, rsAPI a // is then (by default) we return the content, otherwise a 404. // If eventFormat=true, sends the whole event else just the content. func OnIncomingStateTypeRequest( - ctx context.Context, device *userapi.Device, rsAPI api.RoomserverInternalAPI, + ctx context.Context, device *userapi.Device, rsAPI api.ClientRoomserverAPI, roomID, evType, stateKey string, eventFormat bool, ) util.JSONResponse { var worldReadable bool diff --git a/clientapi/routing/threepid.go b/clientapi/routing/threepid.go index a4898ca46..94b658ee3 100644 --- a/clientapi/routing/threepid.go +++ b/clientapi/routing/threepid.go @@ -40,7 +40,7 @@ type threePIDsResponse struct { // RequestEmailToken implements: // POST /account/3pid/email/requestToken // POST /register/email/requestToken -func RequestEmailToken(req *http.Request, threePIDAPI api.UserThreePIDAPI, cfg *config.ClientAPI) util.JSONResponse { +func RequestEmailToken(req *http.Request, threePIDAPI api.ClientUserAPI, cfg *config.ClientAPI) util.JSONResponse { var body threepid.EmailAssociationRequest if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { return *reqErr @@ -90,7 +90,7 @@ func RequestEmailToken(req *http.Request, threePIDAPI api.UserThreePIDAPI, cfg * // CheckAndSave3PIDAssociation implements POST /account/3pid func CheckAndSave3PIDAssociation( - req *http.Request, threePIDAPI api.UserThreePIDAPI, device *api.Device, + req *http.Request, threePIDAPI api.ClientUserAPI, device *api.Device, cfg *config.ClientAPI, ) util.JSONResponse { var body threepid.EmailAssociationCheckRequest @@ -158,7 +158,7 @@ func CheckAndSave3PIDAssociation( // GetAssociated3PIDs implements GET /account/3pid func GetAssociated3PIDs( - req *http.Request, threepidAPI api.UserThreePIDAPI, device *api.Device, + req *http.Request, threepidAPI api.ClientUserAPI, device *api.Device, ) util.JSONResponse { localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID) if err != nil { @@ -182,7 +182,7 @@ func GetAssociated3PIDs( } // Forget3PID implements POST /account/3pid/delete -func Forget3PID(req *http.Request, threepidAPI api.UserThreePIDAPI) util.JSONResponse { +func Forget3PID(req *http.Request, threepidAPI api.ClientUserAPI) util.JSONResponse { var body authtypes.ThreePID if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { return *reqErr diff --git a/clientapi/routing/upgrade_room.go b/clientapi/routing/upgrade_room.go index 00bde36b3..744e2d889 100644 --- a/clientapi/routing/upgrade_room.go +++ b/clientapi/routing/upgrade_room.go @@ -40,9 +40,9 @@ type upgradeRoomResponse struct { func UpgradeRoom( req *http.Request, device *userapi.Device, cfg *config.ClientAPI, - roomID string, profileAPI userapi.UserProfileAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + roomID string, profileAPI userapi.ClientUserAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { var r upgradeRoomRequest if rErr := httputil.UnmarshalJSONRequest(req, &r); rErr != nil { diff --git a/clientapi/routing/userdirectory.go b/clientapi/routing/userdirectory.go index ab73cf430..f311457a0 100644 --- a/clientapi/routing/userdirectory.go +++ b/clientapi/routing/userdirectory.go @@ -34,13 +34,13 @@ type UserDirectoryResponse struct { func SearchUserDirectory( ctx context.Context, device *userapi.Device, - userAPI userapi.UserInternalAPI, - rsAPI api.RoomserverInternalAPI, - provider userapi.UserDirectoryProvider, + userAPI userapi.ClientUserAPI, + rsAPI api.ClientRoomserverAPI, + provider userapi.QuerySearchProfilesAPI, serverName gomatrixserverlib.ServerName, searchString string, limit int, -) *util.JSONResponse { +) util.JSONResponse { if limit < 10 { limit = 10 } @@ -58,8 +58,7 @@ func SearchUserDirectory( } userRes := &userapi.QuerySearchProfilesResponse{} if err := provider.QuerySearchProfiles(ctx, userReq, userRes); err != nil { - errRes := util.ErrorResponse(fmt.Errorf("userAPI.QuerySearchProfiles: %w", err)) - return &errRes + return util.ErrorResponse(fmt.Errorf("userAPI.QuerySearchProfiles: %w", err)) } for _, user := range userRes.Profiles { @@ -94,8 +93,7 @@ func SearchUserDirectory( } stateRes := &api.QueryKnownUsersResponse{} if err := rsAPI.QueryKnownUsers(ctx, stateReq, stateRes); err != nil && err != sql.ErrNoRows { - errRes := util.ErrorResponse(fmt.Errorf("rsAPI.QueryKnownUsers: %w", err)) - return &errRes + return util.ErrorResponse(fmt.Errorf("rsAPI.QueryKnownUsers: %w", err)) } for _, user := range stateRes.Users { @@ -114,7 +112,7 @@ func SearchUserDirectory( response.Results = append(response.Results, result) } - return &util.JSONResponse{ + return util.JSONResponse{ Code: 200, JSON: response, } diff --git a/clientapi/threepid/invites.go b/clientapi/threepid/invites.go index 6b750199b..9670fecad 100644 --- a/clientapi/threepid/invites.go +++ b/clientapi/threepid/invites.go @@ -86,7 +86,7 @@ var ( func CheckAndProcessInvite( ctx context.Context, device *userapi.Device, body *MembershipRequest, cfg *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, db userapi.UserProfileAPI, + rsAPI api.ClientRoomserverAPI, db userapi.ClientUserAPI, roomID string, evTime time.Time, ) (inviteStoredOnIDServer bool, err error) { @@ -136,7 +136,7 @@ func CheckAndProcessInvite( // Returns an error if a check or a request failed. func queryIDServer( ctx context.Context, - db userapi.UserProfileAPI, cfg *config.ClientAPI, device *userapi.Device, + userAPI userapi.ClientUserAPI, cfg *config.ClientAPI, device *userapi.Device, body *MembershipRequest, roomID string, ) (lookupRes *idServerLookupResponse, storeInviteRes *idServerStoreInviteResponse, err error) { if err = isTrusted(body.IDServer, cfg); err != nil { @@ -152,7 +152,7 @@ func queryIDServer( if lookupRes.MXID == "" { // No Matrix ID matches with the given 3PID, ask the server to store the // invite and return a token - storeInviteRes, err = queryIDServerStoreInvite(ctx, db, cfg, device, body, roomID) + storeInviteRes, err = queryIDServerStoreInvite(ctx, userAPI, cfg, device, body, roomID) return } @@ -163,7 +163,7 @@ func queryIDServer( if lookupRes.NotBefore > now || now > lookupRes.NotAfter { // If the current timestamp isn't in the time frame in which the association // is known to be valid, re-run the query - return queryIDServer(ctx, db, cfg, device, body, roomID) + return queryIDServer(ctx, userAPI, cfg, device, body, roomID) } // Check the request signatures and send an error if one isn't valid @@ -205,7 +205,7 @@ func queryIDServerLookup(ctx context.Context, body *MembershipRequest) (*idServe // Returns an error if the request failed to send or if the response couldn't be parsed. func queryIDServerStoreInvite( ctx context.Context, - db userapi.UserProfileAPI, cfg *config.ClientAPI, device *userapi.Device, + userAPI userapi.ClientUserAPI, cfg *config.ClientAPI, device *userapi.Device, body *MembershipRequest, roomID string, ) (*idServerStoreInviteResponse, error) { // Retrieve the sender's profile to get their display name @@ -217,7 +217,7 @@ func queryIDServerStoreInvite( var profile *authtypes.Profile if serverName == cfg.Matrix.ServerName { res := &userapi.QueryProfileResponse{} - err = db.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: device.UserID}, res) + err = userAPI.QueryProfile(ctx, &userapi.QueryProfileRequest{UserID: device.UserID}, res) if err != nil { return nil, err } @@ -337,7 +337,7 @@ func emit3PIDInviteEvent( ctx context.Context, body *MembershipRequest, res *idServerStoreInviteResponse, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.ClientRoomserverAPI, evTime time.Time, ) error { builder := &gomatrixserverlib.EventBuilder{ diff --git a/cmd/create-account/main.go b/cmd/create-account/main.go index 2719f8680..7f6d5105e 100644 --- a/cmd/create-account/main.go +++ b/cmd/create-account/main.go @@ -27,6 +27,7 @@ import ( "github.com/matrix-org/dendrite/setup" "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/storage" "github.com/sirupsen/logrus" "golang.org/x/term" ) @@ -99,8 +100,24 @@ func main() { } } - b := base.NewBaseDendrite(cfg, "Monolith") - accountDB := b.CreateAccountsDB() + // avoid warning about open registration + cfg.ClientAPI.RegistrationDisabled = true + + b := base.NewBaseDendrite(cfg, "") + defer b.Close() // nolint: errcheck + + accountDB, err := storage.NewUserAPIDatabase( + b, + &cfg.UserAPI.AccountDatabase, + cfg.Global.ServerName, + cfg.UserAPI.BCryptCost, + cfg.UserAPI.OpenIDTokenLifetimeMS, + 0, // TODO + cfg.Global.ServerNotices.LocalPart, + ) + if err != nil { + logrus.WithError(err).Fatalln("Failed to connect to the database") + } accType := api.AccountTypeUser if *isAdmin { diff --git a/cmd/dendrite-demo-pinecone/main.go b/cmd/dendrite-demo-pinecone/main.go index dd1ab3697..703436051 100644 --- a/cmd/dendrite-demo-pinecone/main.go +++ b/cmd/dendrite-demo-pinecone/main.go @@ -140,6 +140,8 @@ func main() { cfg.FederationAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-federationapi.db", *instanceName)) cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-appservice.db", *instanceName)) cfg.MSCs.MSCs = []string{"msc2836", "msc2946"} + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true if err := cfg.Derive(); err != nil { panic(err) } @@ -147,7 +149,6 @@ func main() { base := base.NewBaseDendrite(cfg, "Monolith") defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := conn.CreateFederationClient(base, pQUIC) serverKeyAPI := &signing.YggdrasilKeys{} @@ -160,7 +161,7 @@ func main() { ) keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, fsAPI) - userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) + userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) keyAPI.SetUserAPI(userAPI) asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI) @@ -172,7 +173,6 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: conn.CreateClient(base, pQUIC), FedClient: federation, KeyRing: keyRing, @@ -185,15 +185,7 @@ func main() { ExtPublicRoomsProvider: roomProvider, ExtUserDirectoryProvider: userProvider, } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) wsUpgrader := websocket.Upgrader{ CheckOrigin: func(_ *http.Request) bool { diff --git a/cmd/dendrite-demo-pinecone/users/users.go b/cmd/dendrite-demo-pinecone/users/users.go index ebfb5cbe3..fc66bf299 100644 --- a/cmd/dendrite-demo-pinecone/users/users.go +++ b/cmd/dendrite-demo-pinecone/users/users.go @@ -37,7 +37,7 @@ import ( type PineconeUserProvider struct { r *pineconeRouter.Router s *pineconeSessions.Sessions - userAPI userapi.UserProfileAPI + userAPI userapi.QuerySearchProfilesAPI fedClient *gomatrixserverlib.FederationClient } @@ -46,7 +46,7 @@ const PublicURL = "/_matrix/p2p/profiles" func NewPineconeUserProvider( r *pineconeRouter.Router, s *pineconeSessions.Sessions, - userAPI userapi.UserProfileAPI, + userAPI userapi.QuerySearchProfilesAPI, fedClient *gomatrixserverlib.FederationClient, ) *PineconeUserProvider { p := &PineconeUserProvider{ diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go index b840eb2b8..619720d6c 100644 --- a/cmd/dendrite-demo-yggdrasil/main.go +++ b/cmd/dendrite-demo-yggdrasil/main.go @@ -89,6 +89,8 @@ func main() { cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-appservice.db", *instanceName)) cfg.MSCs.MSCs = []string{"msc2836"} cfg.MSCs.Database.ConnectionString = config.DataSource(fmt.Sprintf("file:%s-mscs.db", *instanceName)) + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true if err = cfg.Derive(); err != nil { panic(err) } @@ -102,7 +104,6 @@ func main() { base := base.NewBaseDendrite(cfg, "Monolith") defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := ygg.CreateFederationClient(base) serverKeyAPI := &signing.YggdrasilKeys{} @@ -115,7 +116,7 @@ func main() { ) rsAPI := rsComponent - userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) + userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) keyAPI.SetUserAPI(userAPI) asAPI := appservice.NewInternalAPI(base, userAPI, rsAPI) @@ -128,7 +129,6 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: ygg.CreateClient(base), FedClient: federation, KeyRing: keyRing, @@ -142,15 +142,7 @@ func main() { ygg, fsAPI, federation, ), } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) if err := mscs.Enable(base, &monolith); err != nil { logrus.WithError(err).Fatalf("Failed to enable MSCs") } diff --git a/cmd/dendrite-monolith-server/main.go b/cmd/dendrite-monolith-server/main.go index 1443ab5b1..845b9e465 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -71,7 +71,6 @@ func main() { base := basepkg.NewBaseDendrite(cfg, "Monolith", options...) defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := base.CreateFederationClient() rsImpl := roomserver.NewInternalAPI(base) @@ -90,6 +89,7 @@ func main() { fsAPI := federationapi.NewInternalAPI( base, federation, rsAPI, base.Caches, nil, false, ) + fsImplAPI := fsAPI if base.UseHTTPAPIs { federationapi.AddInternalRoutes(base.InternalAPIMux, fsAPI) fsAPI = base.FederationAPIHTTPClient() @@ -104,7 +104,7 @@ func main() { } pgClient := base.PushGatewayHTTPClient() - userImpl := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, pgClient) + userImpl := userapi.NewInternalAPI(base, &cfg.UserAPI, cfg.Derived.ApplicationServices, keyAPI, rsAPI, pgClient) userAPI := userImpl if base.UseHTTPAPIs { userapi.AddInternalRoutes(base.InternalAPIMux, userAPI) @@ -135,25 +135,19 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: base.CreateClient(), FedClient: federation, KeyRing: keyRing, - AppserviceAPI: asAPI, FederationAPI: fsAPI, + AppserviceAPI: asAPI, + // always use the concrete impl here even in -http mode because adding public routes + // must be done on the concrete impl not an HTTP client else fedapi will call itself + FederationAPI: fsImplAPI, RoomserverAPI: rsAPI, UserAPI: userAPI, KeyAPI: keyAPI, } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) if len(base.Cfg.MSCs.MSCs) > 0 { if err := mscs.Enable(base, &monolith); err != nil { diff --git a/cmd/dendrite-polylith-multi/main.go b/cmd/dendrite-polylith-multi/main.go index 6226cc328..e4845f649 100644 --- a/cmd/dendrite-polylith-multi/main.go +++ b/cmd/dendrite-polylith-multi/main.go @@ -31,7 +31,7 @@ import ( type entrypoint func(base *base.BaseDendrite, cfg *config.Dendrite) func main() { - cfg := setup.ParseFlags(true) + cfg := setup.ParseFlags(false) component := "" if flag.NFlag() > 0 { @@ -71,8 +71,8 @@ func main() { logrus.Infof("Starting %q component", component) - base := base.NewBaseDendrite(cfg, component) // TODO - defer base.Close() // nolint: errcheck + base := base.NewBaseDendrite(cfg, component, base.PolylithMode) // TODO + defer base.Close() // nolint: errcheck go start(base, cfg) base.WaitForShutdown() diff --git a/cmd/dendrite-polylith-multi/personalities/clientapi.go b/cmd/dendrite-polylith-multi/personalities/clientapi.go index 1e509f88a..a5d69d07c 100644 --- a/cmd/dendrite-polylith-multi/personalities/clientapi.go +++ b/cmd/dendrite-polylith-multi/personalities/clientapi.go @@ -31,9 +31,9 @@ func ClientAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { keyAPI := base.KeyServerHTTPClient() clientapi.AddPublicRoutes( - base.ProcessContext, base.PublicClientAPIMux, base.SynapseAdminMux, &base.Cfg.ClientAPI, - federation, rsAPI, asQuery, transactions.New(), fsAPI, userAPI, userAPI, - keyAPI, nil, &cfg.MSCs, + base, federation, rsAPI, asQuery, + transactions.New(), fsAPI, userAPI, userAPI, + keyAPI, nil, ) base.SetupAndServeHTTP( diff --git a/cmd/dendrite-polylith-multi/personalities/federationapi.go b/cmd/dendrite-polylith-multi/personalities/federationapi.go index b82577ce3..6377ce9e3 100644 --- a/cmd/dendrite-polylith-multi/personalities/federationapi.go +++ b/cmd/dendrite-polylith-multi/personalities/federationapi.go @@ -29,10 +29,9 @@ func FederationAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { keyRing := fsAPI.KeyRing() federationapi.AddPublicRoutes( - base.ProcessContext, base.PublicFederationAPIMux, base.PublicKeyAPIMux, base.PublicWellKnownAPIMux, - &base.Cfg.FederationAPI, userAPI, federation, keyRing, - rsAPI, fsAPI, keyAPI, - &base.Cfg.MSCs, nil, + base, + userAPI, federation, keyRing, + rsAPI, fsAPI, keyAPI, nil, ) federationapi.AddInternalRoutes(base.InternalAPIMux, fsAPI) diff --git a/cmd/dendrite-polylith-multi/personalities/mediaapi.go b/cmd/dendrite-polylith-multi/personalities/mediaapi.go index fa9d36a38..69d5fd5a8 100644 --- a/cmd/dendrite-polylith-multi/personalities/mediaapi.go +++ b/cmd/dendrite-polylith-multi/personalities/mediaapi.go @@ -24,7 +24,9 @@ func MediaAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { userAPI := base.UserAPIClient() client := base.CreateClient() - mediaapi.AddPublicRoutes(base.PublicMediaAPIMux, &base.Cfg.MediaAPI, &base.Cfg.ClientAPI.RateLimiting, userAPI, client) + mediaapi.AddPublicRoutes( + base, userAPI, client, + ) base.SetupAndServeHTTP( base.Cfg.MediaAPI.InternalAPI.Listen, diff --git a/cmd/dendrite-polylith-multi/personalities/syncapi.go b/cmd/dendrite-polylith-multi/personalities/syncapi.go index 6fee8419b..41637fe1d 100644 --- a/cmd/dendrite-polylith-multi/personalities/syncapi.go +++ b/cmd/dendrite-polylith-multi/personalities/syncapi.go @@ -22,15 +22,13 @@ import ( func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { userAPI := base.UserAPIClient() - federation := base.CreateFederationClient() rsAPI := base.RoomserverHTTPClient() syncapi.AddPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, userAPI, rsAPI, + base, + userAPI, rsAPI, base.KeyServerHTTPClient(), - federation, &cfg.SyncAPI, ) base.SetupAndServeHTTP( diff --git a/cmd/dendrite-polylith-multi/personalities/userapi.go b/cmd/dendrite-polylith-multi/personalities/userapi.go index f1fa379c7..3fe5a43d7 100644 --- a/cmd/dendrite-polylith-multi/personalities/userapi.go +++ b/cmd/dendrite-polylith-multi/personalities/userapi.go @@ -21,10 +21,8 @@ import ( ) func UserAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { - accountDB := base.CreateAccountsDB() - userAPI := userapi.NewInternalAPI( - base, accountDB, &cfg.UserAPI, cfg.Derived.ApplicationServices, + base, &cfg.UserAPI, cfg.Derived.ApplicationServices, base.KeyServerHTTPClient(), base.RoomserverHTTPClient(), base.PushGatewayHTTPClient(), ) diff --git a/cmd/dendrite-upgrade-tests/main.go b/cmd/dendrite-upgrade-tests/main.go index fbcede4ca..13c10fc26 100644 --- a/cmd/dendrite-upgrade-tests/main.go +++ b/cmd/dendrite-upgrade-tests/main.go @@ -84,7 +84,8 @@ do \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\ +PARAMS="--tls-cert server.crt --tls-key server.key --config dendrite.yaml" \n\ +./dendrite-monolith-server --really-enable-open-registration ${PARAMS} || ./dendrite-monolith-server ${PARAMS} \n\ ' > run_dendrite.sh && chmod +x run_dendrite.sh ENV SERVER_NAME=localhost diff --git a/cmd/dendritejs-pinecone/main.go b/cmd/dendritejs-pinecone/main.go index 211b3e131..e070173aa 100644 --- a/cmd/dendritejs-pinecone/main.go +++ b/cmd/dendritejs-pinecone/main.go @@ -171,6 +171,8 @@ func startup() { cfg.Global.KeyID = gomatrixserverlib.KeyID(signing.KeyID) cfg.Global.PrivateKey = sk cfg.Global.ServerName = gomatrixserverlib.ServerName(hex.EncodeToString(pk)) + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true if err := cfg.Derive(); err != nil { logrus.Fatalf("Failed to derive values from config: %s", err) @@ -178,7 +180,6 @@ func startup() { base := base.NewBaseDendrite(cfg, "Monolith") defer base.Close() // nolint: errcheck - accountDB := base.CreateAccountsDB() federation := conn.CreateFederationClient(base, pSessions) keyAPI := keyserver.NewInternalAPI(base, &base.Cfg.KeyServer, federation) @@ -187,7 +188,7 @@ func startup() { rsAPI := roomserver.NewInternalAPI(base) - userAPI := userapi.NewInternalAPI(base, accountDB, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) + userAPI := userapi.NewInternalAPI(base, &cfg.UserAPI, nil, keyAPI, rsAPI, base.PushGatewayHTTPClient()) keyAPI.SetUserAPI(userAPI) asQuery := appservice.NewInternalAPI( @@ -199,7 +200,6 @@ func startup() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: conn.CreateClient(base, pSessions), FedClient: federation, KeyRing: keyRing, @@ -212,15 +212,7 @@ func startup() { //ServerKeyAPI: serverKeyAPI, ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation), } - monolith.AddAllPublicRoutes( - base.ProcessContext, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - ) + monolith.AddAllPublicRoutes(base) httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath() httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux) diff --git a/cmd/generate-config/main.go b/cmd/generate-config/main.go index 24085afaa..1c585d916 100644 --- a/cmd/generate-config/main.go +++ b/cmd/generate-config/main.go @@ -90,6 +90,8 @@ func main() { cfg.Logging[0].Type = "std" cfg.UserAPI.BCryptCost = bcrypt.MinCost cfg.Global.JetStream.InMemory = true + cfg.ClientAPI.RegistrationDisabled = false + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true cfg.ClientAPI.RegistrationSharedSecret = "complement" cfg.Global.Presence = config.PresenceOptions{ EnableInbound: true, diff --git a/cmd/generate-keys/main.go b/cmd/generate-keys/main.go index bddf219dc..8acd28be0 100644 --- a/cmd/generate-keys/main.go +++ b/cmd/generate-keys/main.go @@ -20,7 +20,7 @@ import ( "log" "os" - "github.com/matrix-org/dendrite/internal/test" + "github.com/matrix-org/dendrite/test" ) const usage = `Usage: %s diff --git a/cmd/resolve-state/main.go b/cmd/resolve-state/main.go index 30331fbb3..c52fd6c42 100644 --- a/cmd/resolve-state/main.go +++ b/cmd/resolve-state/main.go @@ -45,7 +45,7 @@ func main() { panic(err) } - roomserverDB, err := storage.Open(&cfg.RoomServer.Database, cache) + roomserverDB, err := storage.Open(nil, &cfg.RoomServer.Database, cache) if err != nil { panic(err) } diff --git a/dendrite-sample.monolith.yaml b/dendrite-sample.monolith.yaml new file mode 100644 index 000000000..e974dbcba --- /dev/null +++ b/dendrite-sample.monolith.yaml @@ -0,0 +1,279 @@ +# This is the Dendrite configuration file. +# +# The configuration is split up into sections - each Dendrite component has a +# configuration section, in addition to the "global" section which applies to +# all components. + +# The version of the configuration file. +version: 2 + +# Global Matrix configuration. This configuration applies to all components. +global: + # The domain name of this homeserver. + server_name: localhost + + # The path to the signing private key file, used to sign requests and events. + # Note that this is NOT the same private key as used for TLS! To generate a + # signing key, use "./bin/generate-keys --private-key matrix_key.pem". + private_key: matrix_key.pem + + # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) + # to old signing private keys that were formerly in use on this domain. These + # keys will not be used for federation request or event signing, but will be + # provided to any other homeserver that asks when trying to verify old events. + old_private_keys: + # - private_key: old_matrix_key.pem + # expired_at: 1601024554498 + + # How long a remote server can cache our server signing key before requesting it + # again. Increasing this number will reduce the number of requests made by other + # servers for our key but increases the period that a compromised key will be + # considered valid by other homeservers. + key_validity_period: 168h0m0s + + # Global database connection pool, for PostgreSQL monolith deployments only. If + # this section is populated then you can omit the "database" blocks in all other + # sections. For polylith deployments, or monolith deployments using SQLite databases, + # you must configure the "database" block for each component instead. + database: + connection_string: postgresql://username:password@hostname/dendrite?sslmode=disable + max_open_conns: 100 + max_idle_conns: 5 + conn_max_lifetime: -1 + + # The server name to delegate server-server communications to, with optional port + # e.g. localhost:443 + well_known_server_name: "" + + # Lists of domains that the server will trust as identity servers to verify third + # party identifiers such as phone numbers and email addresses. + trusted_third_party_id_servers: + - matrix.org + - vector.im + + # Disables federation. Dendrite will not be able to communicate with other servers + # in the Matrix federation and the federation API will not be exposed. + disable_federation: false + + # Configures the handling of presence events. Inbound controls whether we receive + # presence events from other servers, outbound controls whether we send presence + # events for our local users to other servers. + presence: + enable_inbound: false + enable_outbound: false + + # Configures phone-home statistics reporting. These statistics contain the server + # name, number of active users and some information on your deployment config. + # We use this information to understand how Dendrite is being used in the wild. + report_stats: + enabled: false + endpoint: https://matrix.org/report-usage-stats/push + + # Server notices allows server admins to send messages to all users on the server. + server_notices: + enabled: false + # The local part, display name and avatar URL (as a mxc:// URL) for the user that + # will send the server notices. These are visible to all users on the deployment. + local_part: "_server" + display_name: "Server Alerts" + avatar_url: "" + # The room name to be used when sending server notices. This room name will + # appear in user clients. + room_name: "Server Alerts" + + # Configuration for NATS JetStream + jetstream: + # A list of NATS Server addresses to connect to. If none are specified, an + # internal NATS server will be started automatically when running Dendrite in + # monolith mode. For polylith deployments, it is required to specify the address + # of at least one NATS Server node. + addresses: + # - localhost:4222 + + # Persistent directory to store JetStream streams in. This directory should be + # preserved across Dendrite restarts. + storage_path: ./ + + # The prefix to use for stream names for this homeserver - really only useful + # if you are running more than one Dendrite server on the same NATS deployment. + topic_prefix: Dendrite + + # Configuration for Prometheus metric collection. + metrics: + enabled: false + basic_auth: + username: metrics + password: metrics + + # Optional DNS cache. The DNS cache may reduce the load on DNS servers if there + # is no local caching resolver available for use. + dns_cache: + enabled: false + cache_size: 256 + cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration + +# Configuration for the Appservice API. +app_service_api: + # Disable the validation of TLS certificates of appservices. This is + # not recommended in production since it may allow appservice traffic + # to be sent to an insecure endpoint. + disable_tls_validation: false + + # Appservice configuration files to load into this homeserver. + config_files: + # - /path/to/appservice_registration.yaml + +# Configuration for the Client API. +client_api: + # Prevents new users from being able to register on this homeserver, except when + # using the registration shared secret below. + registration_disabled: true + + # Prevents new guest accounts from being created. Guest registration is also + # disabled implicitly by setting 'registration_disabled' above. + guests_disabled: true + + # If set, allows registration by anyone who knows the shared secret, regardless + # of whether registration is otherwise disabled. + registration_shared_secret: "" + + # Whether to require reCAPTCHA for registration. If you have enabled registration + # then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used + # for coordinated spam attacks. + enable_registration_captcha: false + + # Settings for ReCAPTCHA. + recaptcha_public_key: "" + recaptcha_private_key: "" + recaptcha_bypass_secret: "" + recaptcha_siteverify_api: "" + + # TURN server information that this homeserver should send to clients. + turn: + turn_user_lifetime: "" + turn_uris: + # - turn:turn.server.org?transport=udp + # - turn:turn.server.org?transport=tcp + turn_shared_secret: "" + turn_username: "" + turn_password: "" + + # Settings for rate-limited endpoints. Rate limiting kicks in after the threshold + # number of "slots" have been taken by requests from a specific host. Each "slot" + # will be released after the cooloff time in milliseconds. + rate_limiting: + enabled: true + threshold: 5 + cooloff_ms: 500 + +# Configuration for the Federation API. +federation_api: + # How many times we will try to resend a failed transaction to a specific server. The + # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once + # the max retries are exceeded, Dendrite will no longer try to send transactions to + # that server until it comes back to life and connects to us again. + send_max_retries: 16 + + # Disable the validation of TLS certificates of remote federated homeservers. Do not + # enable this option in production as it presents a security risk! + disable_tls_validation: false + + # Perspective keyservers to use as a backup when direct key fetches fail. This may + # be required to satisfy key requests for servers that are no longer online when + # joining some rooms. + key_perspectives: + - server_name: matrix.org + keys: + - key_id: ed25519:auto + public_key: Noi6WqcDj0QmPxCNQqgezwTlBKrfqehY1u2FyWP9uYw + - key_id: ed25519:a_RXGa + public_key: l8Hft5qXKn1vfHrg3p4+W8gELQVo8N13JkluMfmn2sQ + + # This option will control whether Dendrite will prefer to look up keys directly + # or whether it should try perspective servers first, using direct fetches as a + # last resort. + prefer_direct_fetch: false + +# Configuration for the Media API. +media_api: + # Storage path for uploaded media. May be relative or absolute. + base_path: ./media_store + + # The maximum allowed file size (in bytes) for media uploads to this homeserver + # (0 = unlimited). If using a reverse proxy, ensure it allows requests at least + #this large (e.g. the client_max_body_size setting in nginx). + max_file_size_bytes: 10485760 + + # Whether to dynamically generate thumbnails if needed. + dynamic_thumbnails: false + + # The maximum number of simultaneous thumbnail generators to run. + max_thumbnail_generators: 10 + + # A list of thumbnail sizes to be generated for media content. + thumbnail_sizes: + - width: 32 + height: 32 + method: crop + - width: 96 + height: 96 + method: crop + - width: 640 + height: 480 + method: scale + +# Configuration for enabling experimental MSCs on this homeserver. +mscs: + mscs: + # - msc2836 # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) + # - msc2946 # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) + +# Configuration for the Sync API. +sync_api: + # This option controls which HTTP header to inspect to find the real remote IP + # address of the client. This is likely required if Dendrite is running behind + # a reverse proxy server. + # real_ip_header: X-Real-IP + +# Configuration for the User API. +user_api: + # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31 + # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information. + # Setting this lower makes registration/login consume less CPU resources at the cost + # of security should the database be compromised. Setting this higher makes registration/login + # consume more CPU resources but makes it harder to brute force password hashes. This value + # can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds). + bcrypt_cost: 10 + + # The length of time that a token issued for a relying party from + # /_matrix/client/r0/user/{userId}/openid/request_token endpoint + # is considered to be valid in milliseconds. + # The default lifetime is 3600000ms (60 minutes). + # openid_token_lifetime_ms: 3600000 + +# Configuration for Opentracing. +# See https://github.com/matrix-org/dendrite/tree/master/docs/tracing for information on +# how this works and how to set it up. +tracing: + enabled: false + jaeger: + serviceName: "" + disabled: false + rpc_metrics: false + tags: [] + sampler: null + reporter: null + headers: null + baggage_restrictions: null + throttler: null + +# Logging configuration. The "std" logging type controls the logs being sent to +# stdout. The "file" logging type controls logs being written to a log folder on +# the disk. Supported log levels are "debug", "info", "warn", "error". +logging: + - type: std + level: info + - type: file + level: info + params: + path: ./logs diff --git a/dendrite-config.yaml b/dendrite-sample.polylith.yaml similarity index 57% rename from dendrite-config.yaml rename to dendrite-sample.polylith.yaml index 47f08c4fd..4b67aaa94 100644 --- a/dendrite-config.yaml +++ b/dendrite-sample.polylith.yaml @@ -3,29 +3,6 @@ # The configuration is split up into sections - each Dendrite component has a # configuration section, in addition to the "global" section which applies to # all components. -# -# At a minimum, to get started, you will need to update the settings in the -# "global" section for your deployment, and you will need to check that the -# database "connection_string" line in each component section is correct. -# -# Each component with a "database" section can accept the following formats -# for "connection_string": -# SQLite: file:filename.db -# file:///path/to/filename.db -# PostgreSQL: postgresql://user:pass@hostname/database?params=... -# -# SQLite is embedded into Dendrite and therefore no further prerequisites are -# needed for the database when using SQLite mode. However, performance with -# PostgreSQL is significantly better and recommended for multi-user deployments. -# SQLite is typically around 20-30% slower than PostgreSQL when tested with a -# small number of users and likely will perform worse still with a higher volume -# of users. -# -# The "max_open_conns" and "max_idle_conns" settings configure the maximum -# number of open/idle database connections. The value 0 will use the database -# engine default, and a negative value will use unlimited connections. The -# "conn_max_lifetime" option controls the maximum length of time a database -# connection can be idle in seconds - a negative value is unlimited. # The version of the configuration file. version: 2 @@ -44,9 +21,9 @@ global: # to old signing private keys that were formerly in use on this domain. These # keys will not be used for federation request or event signing, but will be # provided to any other homeserver that asks when trying to verify old events. - # old_private_keys: - # - private_key: old_matrix_key.pem - # expired_at: 1601024554498 + old_private_keys: + # - private_key: old_matrix_key.pem + # expired_at: 1601024554498 # How long a remote server can cache our server signing key before requesting it # again. Increasing this number will reduce the number of requests made by other @@ -64,112 +41,108 @@ global: - matrix.org - vector.im - # Disables federation. Dendrite will not be able to make any outbound HTTP requests - # to other servers and the federation API will not be exposed. + # Disables federation. Dendrite will not be able to communicate with other servers + # in the Matrix federation and the federation API will not be exposed. disable_federation: false - # Configures the handling of presence events. + # Configures the handling of presence events. Inbound controls whether we receive + # presence events from other servers, outbound controls whether we send presence + # events for our local users to other servers. presence: - # Whether inbound presence events are allowed, e.g. receiving presence events from other servers enable_inbound: false - # Whether outbound presence events are allowed, e.g. sending presence events to other servers enable_outbound: false - # Server notices allows server admins to send messages to all users. + # Configures phone-home statistics reporting. These statistics contain the server + # name, number of active users and some information on your deployment config. + # We use this information to understand how Dendrite is being used in the wild. + report_stats: + enabled: false + endpoint: https://matrix.org/report-usage-stats/push + + # Server notices allows server admins to send messages to all users on the server. server_notices: enabled: false - # The server localpart to be used when sending notices, ensure this is not yet taken + # The local part, display name and avatar URL (as a mxc:// URL) for the user that + # will send the server notices. These are visible to all users on the deployment. local_part: "_server" - # The displayname to be used when sending notices - display_name: "Server alerts" - # The mxid of the avatar to use + display_name: "Server Alerts" avatar_url: "" - # The roomname to be used when creating messages + # The room name to be used when sending server notices. This room name will + # appear in user clients. room_name: "Server Alerts" # Configuration for NATS JetStream jetstream: # A list of NATS Server addresses to connect to. If none are specified, an - # internal NATS server will be started automatically when running Dendrite - # in monolith mode. It is required to specify the address of at least one - # NATS Server node if running in polylith mode. + # internal NATS server will be started automatically when running Dendrite in + # monolith mode. For polylith deployments, it is required to specify the address + # of at least one NATS Server node. addresses: - # - localhost:4222 + - hostname:4222 - # Keep all NATS streams in memory, rather than persisting it to the storage - # path below. This option is present primarily for integration testing and - # should not be used on a real world Dendrite deployment. - in_memory: false - - # Persistent directory to store JetStream streams in. This directory - # should be preserved across Dendrite restarts. - storage_path: ./ - - # The prefix to use for stream names for this homeserver - really only - # useful if running more than one Dendrite on the same NATS deployment. + # The prefix to use for stream names for this homeserver - really only useful + # if you are running more than one Dendrite server on the same NATS deployment. topic_prefix: Dendrite # Configuration for Prometheus metric collection. metrics: - # Whether or not Prometheus metrics are enabled. enabled: false - - # HTTP basic authentication to protect access to monitoring. basic_auth: username: metrics password: metrics - # DNS cache options. The DNS cache may reduce the load on DNS servers - # if there is no local caching resolver available for use. + # Optional DNS cache. The DNS cache may reduce the load on DNS servers if there + # is no local caching resolver available for use. dns_cache: - # Whether or not the DNS cache is enabled. enabled: false - - # Maximum number of entries to hold in the DNS cache, and - # for how long those items should be considered valid in seconds. cache_size: 256 - cache_lifetime: "5m" # 5minutes; see https://pkg.go.dev/time@master#ParseDuration for more + cache_lifetime: "5m" # 5 minutes; https://pkg.go.dev/time@master#ParseDuration # Configuration for the Appservice API. app_service_api: internal_api: - listen: http://localhost:7777 # Only used in polylith deployments - connect: http://localhost:7777 # Only used in polylith deployments + listen: http://[::]:7777 # The listen address for incoming API requests + connect: http://app_service_api:7777 # The connect address for other components to use + + # Database configuration for this component. database: - connection_string: file:appservice.db + connection_string: postgresql://username@password:hostname/dendrite_appservice?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 # Disable the validation of TLS certificates of appservices. This is # not recommended in production since it may allow appservice traffic - # to be sent to an unverified endpoint. + # to be sent to an insecure endpoint. disable_tls_validation: false # Appservice configuration files to load into this homeserver. - config_files: [] + config_files: + # - /path/to/appservice_registration.yaml # Configuration for the Client API. client_api: internal_api: - listen: http://localhost:7771 # Only used in polylith deployments - connect: http://localhost:7771 # Only used in polylith deployments + listen: http://[::]:7771 # The listen address for incoming API requests + connect: http://client_api:7771 # The connect address for other components to use external_api: listen: http://[::]:8071 # Prevents new users from being able to register on this homeserver, except when # using the registration shared secret below. - registration_disabled: false + registration_disabled: true # Prevents new guest accounts from being created. Guest registration is also # disabled implicitly by setting 'registration_disabled' above. guests_disabled: true - # If set, allows registration by anyone who knows the shared secret, regardless of - # whether registration is otherwise disabled. + # If set, allows registration by anyone who knows the shared secret, regardless + # of whether registration is otherwise disabled. registration_shared_secret: "" - # Whether to require reCAPTCHA for registration. + # Whether to require reCAPTCHA for registration. If you have enabled registration + # then this is HIGHLY RECOMMENDED to reduce the risk of your homeserver being used + # for coordinated spam attacks. enable_registration_captcha: false # Settings for ReCAPTCHA. @@ -181,14 +154,16 @@ client_api: # TURN server information that this homeserver should send to clients. turn: turn_user_lifetime: "" - turn_uris: [] + turn_uris: + # - turn:turn.server.org?transport=udp + # - turn:turn.server.org?transport=tcp turn_shared_secret: "" turn_username: "" turn_password: "" - # Settings for rate-limited endpoints. Rate limiting will kick in after the - # threshold number of "slots" have been taken by requests from a specific - # host. Each "slot" will be released after the cooloff time in milliseconds. + # Settings for rate-limited endpoints. Rate limiting kicks in after the threshold + # number of "slots" have been taken by requests from a specific host. Each "slot" + # will be released after the cooloff time in milliseconds. rate_limiting: enabled: true threshold: 5 @@ -197,18 +172,20 @@ client_api: # Configuration for the Federation API. federation_api: internal_api: - listen: http://localhost:7772 # Only used in polylith deployments - connect: http://localhost:7772 # Only used in polylith deployments + listen: http://[::]:7772 # The listen address for incoming API requests + connect: http://federation_api:7772 # The connect address for other components to use external_api: listen: http://[::]:8072 database: - connection_string: file:federationapi.db + connection_string: postgresql://username@password:hostname/dendrite_federationapi?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 # How many times we will try to resend a failed transaction to a specific server. The - # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. + # backoff is 2**x seconds, so 1 = 2 seconds, 2 = 4 seconds, 3 = 8 seconds etc. Once + # the max retries are exceeded, Dendrite will no longer try to send transactions to + # that server until it comes back to life and connects to us again. send_max_retries: 16 # Disable the validation of TLS certificates of remote federated homeservers. Do not @@ -234,10 +211,10 @@ federation_api: # Configuration for the Key Server (for end-to-end encryption). key_server: internal_api: - listen: http://localhost:7779 # Only used in polylith deployments - connect: http://localhost:7779 # Only used in polylith deployments + listen: http://[::]:7779 # The listen address for incoming API requests + connect: http://key_server:7779 # The connect address for other components to use database: - connection_string: file:keyserver.db + connection_string: postgresql://username@password:hostname/dendrite_keyserver?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 @@ -245,12 +222,12 @@ key_server: # Configuration for the Media API. media_api: internal_api: - listen: http://localhost:7774 # Only used in polylith deployments - connect: http://localhost:7774 # Only used in polylith deployments + listen: http://[::]:7774 # The listen address for incoming API requests + connect: http://media_api:7774 # The connect address for other components to use external_api: listen: http://[::]:8074 database: - connection_string: file:mediaapi.db + connection_string: postgresql://username@password:hostname/dendrite_mediaapi?sslmode=disable max_open_conns: 5 max_idle_conns: 2 conn_max_lifetime: -1 @@ -259,8 +236,8 @@ media_api: base_path: ./media_store # The maximum allowed file size (in bytes) for media uploads to this homeserver - # (0 = unlimited). If using a reverse proxy, ensure it allows requests at - # least this large (e.g. client_max_body_size in nginx.) + # (0 = unlimited). If using a reverse proxy, ensure it allows requests at least + #this large (e.g. the client_max_body_size setting in nginx). max_file_size_bytes: 10485760 # Whether to dynamically generate thumbnails if needed. @@ -281,15 +258,13 @@ media_api: height: 480 method: scale -# Configuration for experimental MSC's +# Configuration for enabling experimental MSCs on this homeserver. mscs: - # A list of enabled MSC's - # Currently valid values are: - # - msc2836 (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) - # - msc2946 (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) - mscs: [] + mscs: + # - msc2836 # (Threading, see https://github.com/matrix-org/matrix-doc/pull/2836) + # - msc2946 # (Spaces Summary, see https://github.com/matrix-org/matrix-doc/pull/2946) database: - connection_string: file:mscs.db + connection_string: postgresql://username@password:hostname/dendrite_mscs?sslmode=disable max_open_conns: 5 max_idle_conns: 2 conn_max_lifetime: -1 @@ -297,10 +272,10 @@ mscs: # Configuration for the Room Server. room_server: internal_api: - listen: http://localhost:7770 # Only used in polylith deployments - connect: http://localhost:7770 # Only used in polylith deployments + listen: http://[::]:7770 # The listen address for incoming API requests + connect: http://room_server:7770 # The connect address for other components to use database: - connection_string: file:roomserver.db + connection_string: postgresql://username@password:hostname/dendrite_roomserver?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 @@ -308,12 +283,12 @@ room_server: # Configuration for the Sync API. sync_api: internal_api: - listen: http://localhost:7773 # Only used in polylith deployments - connect: http://localhost:7773 # Only used in polylith deployments + listen: http://[::]:7773 # The listen address for incoming API requests + connect: http://sync_api:7773 # The connect address for other components to use external_api: listen: http://[::]:8073 database: - connection_string: file:syncapi.db + connection_string: postgresql://username@password:hostname/dendrite_syncapi?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 @@ -325,21 +300,23 @@ sync_api: # Configuration for the User API. user_api: - # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31 - # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information. - # Setting this lower makes registration/login consume less CPU resources at the cost of security - # should the database be compromised. Setting this higher makes registration/login consume more - # CPU resources but makes it harder to brute force password hashes. - # This value can be low if performing tests or on embedded Dendrite instances (e.g WASM builds) - # bcrypt_cost: 10 internal_api: - listen: http://localhost:7781 # Only used in polylith deployments - connect: http://localhost:7781 # Only used in polylith deployments + listen: http://[::]:7781 # The listen address for incoming API requests + connect: http://user_api:7781 # The connect address for other components to use account_database: - connection_string: file:userapi_accounts.db + connection_string: postgresql://username@password:hostname/dendrite_userapi?sslmode=disable max_open_conns: 10 max_idle_conns: 2 conn_max_lifetime: -1 + + # The cost when hashing passwords on registration/login. Default: 10. Min: 4, Max: 31 + # See https://pkg.go.dev/golang.org/x/crypto/bcrypt for more information. + # Setting this lower makes registration/login consume less CPU resources at the cost + # of security should the database be compromised. Setting this higher makes registration/login + # consume more CPU resources but makes it harder to brute force password hashes. This value + # can be lowered if performing tests or on embedded Dendrite instances (e.g WASM builds). + bcrypt_cost: 10 + # The length of time that a token issued for a relying party from # /_matrix/client/r0/user/{userId}/openid/request_token endpoint # is considered to be valid in milliseconds. @@ -362,12 +339,13 @@ tracing: baggage_restrictions: null throttler: null -# Logging configuration +# Logging configuration. The "std" logging type controls the logs being sent to +# stdout. The "file" logging type controls logs being written to a log folder on +# the disk. Supported log levels are "debug", "info", "warn", "error". logging: - type: std level: info - type: file - # The logging level, must be one of debug, info, warn, error, fatal, panic. level: info params: path: ./logs diff --git a/docs/CODE_STYLE.md b/docs/CODE_STYLE.md deleted file mode 100644 index 8096ae27c..000000000 --- a/docs/CODE_STYLE.md +++ /dev/null @@ -1,60 +0,0 @@ -# Code Style - -In addition to standard Go code style (`gofmt`, `goimports`), we use `golangci-lint` -to run a number of linters, the exact list can be found under linters in [.golangci.yml](.golangci.yml). -[Installation](https://github.com/golangci/golangci-lint#install-golangci-lint) and [Editor -Integration](https://golangci-lint.run/usage/integrations/#editor-integration) for -it can be found in the readme of golangci-lint. - -For rare cases where a linter is giving a spurious warning, it can be disabled -for that line or statement using a [comment -directive](https://golangci-lint.run/usage/false-positives/#nolint), e.g. `var -bad_name int //nolint:golint,unused`. This should be used sparingly and only -when its clear that the lint warning is spurious. - -The linters can be run using [build/scripts/find-lint.sh](/build/scripts/find-lint.sh) -(see file for docs) or as part of a build/test/lint cycle using -[build/scripts/build-test-lint.sh](/build/scripts/build-test-lint.sh). - - -## Labels - -In addition to `TODO` and `FIXME` we also use `NOTSPEC` to identify deviations -from the Matrix specification. - -## Logging - -We generally prefer to log with static log messages and include any dynamic -information in fields. - -```golang -logger := util.GetLogger(ctx) - -// Not recommended -logger.Infof("Finished processing keys for %s, number of keys %d", name, numKeys) - -// Recommended -logger.WithFields(logrus.Fields{ - "numberOfKeys": numKeys, - "entityName": name, -}).Info("Finished processing keys") -``` - -This is useful when logging to systems that natively understand log fields, as -it allows people to search and process the fields without having to parse the -log message. - - -## Visual Studio Code - -If you use VSCode then the following is an example of a workspace setting that -sets up linting correctly: - -```json -{ - "go.lintTool":"golangci-lint", - "go.lintFlags": [ - "--fast" - ] -} -``` diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 116adfae6..5a89e6841 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,55 +1,103 @@ +--- +title: Contributing +parent: Development +permalink: /development/contributing +--- + # Contributing to Dendrite Everyone is welcome to contribute to Dendrite! We aim to make it as easy as possible to get started. -Please ensure that you sign off your contributions! See [Sign Off](#sign-off) -section below. +## Sign off + +We ask that everyone who contributes to the project signs off their contributions +in accordance with the [DCO](https://github.com/matrix-org/matrix-spec/blob/main/CONTRIBUTING.rst#sign-off). +In effect, this means adding a statement to your pull requests or commit messages +along the lines of: + +``` +Signed-off-by: Full Name +``` + +Unfortunately we can't accept contributions without it. ## Getting up and running -See [INSTALL.md](INSTALL.md) for instructions on setting up a running dev -instance of dendrite, and [CODE_STYLE.md](CODE_STYLE.md) for the code style -guide. +See the [Installation](INSTALL.md) section for information on how to build an +instance of Dendrite. You will likely need this in order to test your changes. -We use [golangci-lint](https://github.com/golangci/golangci-lint) to lint -Dendrite which can be executed via: +## Code style +On the whole, the format as prescribed by `gofmt`, `goimports` etc. is exactly +what we use and expect. Please make sure that you run one of these formatters before +submitting your contribution. + +## Comments + +Please make sure that the comments adequately explain *why* your code does what it +does. If there are statements that are not obvious, please comment what they do. + +We also have some special tags which we use for searchability. These are: + +* `// TODO:` for places where a future review, rewrite or refactor is likely required; +* `// FIXME:` for places where we know there is an outstanding bug that needs a fix; +* `// NOTSPEC:` for places where the behaviour specifically does not match what the + [Matrix Specification](https://spec.matrix.org/) prescribes, along with a description + of *why* that is the case. + +## Linting + +We use [golangci-lint](https://github.com/golangci/golangci-lint) to lint Dendrite +which can be executed via: + +```bash +golangci-lint run ``` -$ golangci-lint run -``` + +If you are receiving linter warnings that you are certain are spurious and want to +silence them, you can annotate the relevant lines or methods with a `// nolint:` +comment. Please avoid doing this if you can. + +## Unit tests We also have unit tests which we run via: -``` -$ go test ./... +```bash +go test ./... ``` -## Continuous Integration +In general, we like submissions that come with tests. Anything that proves that the +code is functioning as intended is great, and to ensure that we will find out quickly +in the future if any regressions happen. -When a Pull Request is submitted, continuous integration jobs are run -automatically to ensure the code builds and is relatively well-written. The jobs -are run on [Buildkite](https://buildkite.com/matrix-dot-org/dendrite/), and the -Buildkite pipeline configuration can be found in Matrix.org's [pipelines -repository](https://github.com/matrix-org/pipelines). +We use the standard [Go testing package](https://gobyexample.com/testing) for this, +alongside some helper functions in our own [`test` package](https://pkg.go.dev/github.com/matrix-org/dendrite/test). -If a job fails, click the "details" button and you should be taken to the job's -logs. +## Continuous integration -![Click the details button on the failing build -step](https://raw.githubusercontent.com/matrix-org/dendrite/main/docs/images/details-button-location.jpg) +When a Pull Request is submitted, continuous integration jobs are run automatically +by GitHub actions to ensure that the code builds and works in a number of configurations, +such as different Go versions, using full HTTP APIs and both database engines. +CI will automatically run the unit tests (as above) as well as both of our integration +test suites ([Complement](https://github.com/matrix-org/complement) and +[SyTest](https://github.com/matrix-org/sytest)). -Scroll down to the failing step and you should see some log output. Scan the -logs until you find what it's complaining about, fix it, submit a new commit, -then rinse and repeat until CI passes. +You can see the progress of any CI jobs at the bottom of the Pull Request page, or by +looking at the [Actions](https://github.com/matrix-org/dendrite/actions) tab of the Dendrite +repository. -### Running CI Tests Locally +We generally won't accept a submission unless all of the CI jobs are passing. We +do understand though that sometimes the tests get things wrong — if that's the case, +please also raise a pull request to fix the relevant tests! + +### Running CI tests locally To save waiting for CI to finish after every commit, it is ideal to run the checks locally before pushing, fixing errors first. This also saves other people time as only so many PRs can be tested at a given time. -To execute what Buildkite tests, first run `./build/scripts/build-test-lint.sh`; this +To execute what CI tests, first run `./build/scripts/build-test-lint.sh`; this script will build the code, lint it, and run `go test ./...` with race condition checking enabled. If something needs to be changed, fix it and then run the script again until it no longer complains. Be warned that the linting can take a @@ -64,8 +112,7 @@ passing tests. If these two steps report no problems, the code should be able to pass the CI tests. - -## Picking Things To Do +## Picking things to do If you're new then feel free to pick up an issue labelled [good first issue](https://github.com/matrix-org/dendrite/labels/good%20first%20issue). @@ -81,17 +128,10 @@ We ask people who are familiar with Dendrite to leave the [good first issue](https://github.com/matrix-org/dendrite/labels/good%20first%20issue) issues so that there is always a way for new people to come and get involved. -## Getting Help +## Getting help For questions related to developing on Dendrite we have a dedicated room on Matrix [#dendrite-dev:matrix.org](https://matrix.to/#/#dendrite-dev:matrix.org) where we're happy to help. -For more general questions please use -[#dendrite:matrix.org](https://matrix.to/#/#dendrite:matrix.org). - -## Sign off - -We ask that everyone who contributes to the project signs off their -contributions, in accordance with the -[DCO](https://github.com/matrix-org/matrix-spec/blob/main/CONTRIBUTING.rst#sign-off). +For more general questions please use [#dendrite:matrix.org](https://matrix.to/#/#dendrite:matrix.org). diff --git a/docs/DESIGN.md b/docs/DESIGN.md deleted file mode 100644 index 80e251c5e..000000000 --- a/docs/DESIGN.md +++ /dev/null @@ -1,140 +0,0 @@ -# Design - -## Log Based Architecture - -### Decomposition and Decoupling - -A matrix homeserver can be built around append-only event logs built from the -messages, receipts, presence, typing notifications, device messages and other -events sent by users on the homeservers or by other homeservers. - -The server would then decompose into two categories: writers that add new -entries to the logs and readers that read those entries. - -The event logs then serve to decouple the two components, the writers and -readers need only agree on the format of the entries in the event log. -This format could be largely derived from the wire format of the events used -in the client and federation protocols: - - - C-S API +---------+ Event Log +---------+ C-S API - ---------> | |+ (e.g. kafka) | |+ ---------> - | Writers || =============> | Readers || - ---------> | || | || ---------> - S-S API +---------+| +---------+| S-S API - +---------+ +---------+ - -However the way matrix handles state events in a room creates a few -complications for this model. - - 1) Writers require the room state at an event to check if it is allowed. - 2) Readers require the room state at an event to determine the users and - servers that are allowed to see the event. - 3) A client can query the current state of the room from a reader. - -The writers and readers cannot extract the necessary information directly from -the event logs because it would take too long to extract the information as the -state is built up by collecting individual state events from the event history. - -The writers and readers therefore need access to something that stores copies -of the event state in a form that can be efficiently queried. One possibility -would be for the readers and writers to maintain copies of the current state -in local databases. A second possibility would be to add a dedicated component -that maintained the state of the room and exposed an API that the readers and -writers could query to get the state. The second has the advantage that the -state is calculated and stored in a single location. - - - C-S API +---------+ Log +--------+ Log +---------+ C-S API - ---------> | |+ ======> | | ======> | |+ ---------> - | Writers || | Room | | Readers || - ---------> | || <------ | Server | ------> | || ---------> - S-S API +---------+| Query | | Query +---------+| S-S API - +---------+ +--------+ +---------+ - - -The room server can annotate the events it logs to the readers with room state -so that the readers can avoid querying the room server unnecessarily. - -[This architecture can be extended to cover most of the APIs.](WIRING.md) - -## How things are supposed to work. - -### Local client sends an event in an existing room. - - 0) The client sends a PUT `/_matrix/client/r0/rooms/{roomId}/send` request - and an HTTP loadbalancer routes the request to a ClientAPI. - - 1) The ClientAPI: - - * Authenticates the local user using the `access_token` sent in the HTTP - request. - * Checks if it has already processed or is processing a request with the - same `txnID`. - * Calculates which state events are needed to auth the request. - * Queries the necessary state events and the latest events in the room - from the RoomServer. - * Confirms that the room exists and checks whether the event is allowed by - the auth checks. - * Builds and signs the events. - * Writes the event to a "InputRoomEvent" kafka topic. - * Send a `200 OK` response to the client. - - 2) The RoomServer reads the event from "InputRoomEvent" kafka topic: - - * Checks if it has already has a copy of the event. - * Checks if the event is allowed by the auth checks using the auth events - at the event. - * Calculates the room state at the event. - * Works out what the latest events in the room after processing this event - are. - * Calculate how the changes in the latest events affect the current state - of the room. - * TODO: Workout what events determine the visibility of this event to other - users - * Writes the event along with the changes in current state to an - "OutputRoomEvent" kafka topic. It writes all the events for a room to - the same kafka partition. - - 3a) The ClientSync reads the event from the "OutputRoomEvent" kafka topic: - - * Updates its copy of the current state for the room. - * Works out which users need to be notified about the event. - * Wakes up any pending `/_matrix/client/r0/sync` requests for those users. - * Adds the event to the recent timeline events for the room. - - 3b) The FederationSender reads the event from the "OutputRoomEvent" kafka topic: - - * Updates its copy of the current state for the room. - * Works out which remote servers need to be notified about the event. - * Sends a `/_matrix/federation/v1/send` request to those servers. - * Or if there is a request in progress then add the event to a queue to be - sent when the previous request finishes. - -### Remote server sends an event in an existing room. - - 0) The remote server sends a `PUT /_matrix/federation/v1/send` request and an - HTTP loadbalancer routes the request to a FederationReceiver. - - 1) The FederationReceiver: - - * Authenticates the remote server using the "X-Matrix" authorisation header. - * Checks if it has already processed or is processing a request with the - same `txnID`. - * Checks the signatures for the events. - Fetches the ed25519 keys for the event senders if necessary. - * Queries the RoomServer for a copy of the state of the room at each event. - * If the RoomServer doesn't know the state of the room at an event then - query the state of the room at the event from the remote server using - `GET /_matrix/federation/v1/state_ids` falling back to - `GET /_matrix/federation/v1/state` if necessary. - * Once the state at each event is known check whether the events are - allowed by the auth checks against the state at each event. - * For each event that is allowed write the event to the "InputRoomEvent" - kafka topic. - * Send a 200 OK response to the remote server listing which events were - successfully processed and which events failed - - 2) The RoomServer processes the event the same as it would a local event. - - 3a) The ClientSync processes the event the same as it would a local event. diff --git a/docs/FAQ.md b/docs/FAQ.md index 978212cce..47f39b9e6 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -1,26 +1,34 @@ -# Frequently Asked Questions +--- +title: FAQ +nav_order: 1 +permalink: /faq +--- -### Is Dendrite stable? +# FAQ + +## Is Dendrite stable? Mostly, although there are still bugs and missing features. If you are a confident power user and you are happy to spend some time debugging things when they go wrong, then please try out Dendrite. If you are a community, organisation or business that demands stability and uptime, then Dendrite is not for you yet - please install Synapse instead. -### Is Dendrite feature-complete? +## Is Dendrite feature-complete? No, although a good portion of the Matrix specification has been implemented. Mostly missing are client features - see the readme at the root of the repository for more information. -### Is there a migration path from Synapse to Dendrite? +## Is there a migration path from Synapse to Dendrite? -No, not at present. There will be in the future when Dendrite reaches version 1.0. +No, not at present. There will be in the future when Dendrite reaches version 1.0. For now it is not +possible to migrate an existing Synapse deployment to Dendrite. -### Can I use Dendrite with an existing Synapse database? +## Can I use Dendrite with an existing Synapse database? No, Dendrite has a very different database schema to Synapse and the two are not interchangeable. -### Should I run a monolith or a polylith deployment? +## Should I run a monolith or a polylith deployment? -Monolith deployments are always preferred where possible, and at this time, are far better tested than polylith deployments are. The only reason to consider a polylith deployment is if you wish to run different Dendrite components on separate physical machines. +Monolith deployments are always preferred where possible, and at this time, are far better tested than polylith deployments are. The only reason to consider a polylith deployment is if you wish to run different Dendrite components on separate physical machines, but this is an advanced configuration which we don't +recommend. -### I've installed Dendrite but federation isn't working +## I've installed Dendrite but federation isn't working Check the [Federation Tester](https://federationtester.matrix.org). You need at least: @@ -28,49 +36,95 @@ Check the [Federation Tester](https://federationtester.matrix.org). You need at * A valid TLS certificate for that DNS name * Either DNS SRV records or well-known files -### Does Dendrite work with my favourite client? +## Does Dendrite work with my favourite client? It should do, although we are aware of some minor issues: * **Element Android**: registration does not work, but logging in with an existing account does * **Hydrogen**: occasionally sync can fail due to gaps in the `since` parameter, but clearing the cache fixes this -### Does Dendrite support push notifications? +## Does Dendrite support push notifications? Yes, we have experimental support for push notifications. Configure them in the usual way in your Matrix client. -### Does Dendrite support application services/bridges? +## Does Dendrite support application services/bridges? Possibly - Dendrite does have some application service support but it is not well tested. Please let us know by raising a GitHub issue if you try it and run into problems. Bridges known to work (as of v0.5.1): -- [Telegram](https://docs.mau.fi/bridges/python/telegram/index.html) -- [WhatsApp](https://docs.mau.fi/bridges/go/whatsapp/index.html) -- [Signal](https://docs.mau.fi/bridges/python/signal/index.html) -- [probably all other mautrix bridges](https://docs.mau.fi/bridges/) -Remember to add the config file(s) to the `app_service_api` [config](https://github.com/matrix-org/dendrite/blob/de38be469a23813921d01bef3e14e95faab2a59e/dendrite-config.yaml#L130-L131). +* [Telegram](https://docs.mau.fi/bridges/python/telegram/index.html) +* [WhatsApp](https://docs.mau.fi/bridges/go/whatsapp/index.html) +* [Signal](https://docs.mau.fi/bridges/python/signal/index.html) +* [probably all other mautrix bridges](https://docs.mau.fi/bridges/) -### Is it possible to prevent communication with the outside world? +Remember to add the config file(s) to the `app_service_api` section of the config file. -Yes, you can do this by disabling federation - set `disable_federation` to `true` in the `global` section of the Dendrite configuration file. +## Is it possible to prevent communication with the outside world? -### Should I use PostgreSQL or SQLite for my databases? +Yes, you can do this by disabling federation - set `disable_federation` to `true` in the `global` section of the Dendrite configuration file. -Please use PostgreSQL wherever possible, especially if you are planning to run a homeserver that caters to more than a couple of users. +## Should I use PostgreSQL or SQLite for my databases? -### Dendrite is using a lot of CPU +Please use PostgreSQL wherever possible, especially if you are planning to run a homeserver that caters to more than a couple of users. -Generally speaking, you should expect to see some CPU spikes, particularly if you are joining or participating in large rooms. However, constant/sustained high CPU usage is not expected - if you are experiencing that, please join `#dendrite-dev:matrix.org` and let us know, or file a GitHub issue. +## Dendrite is using a lot of CPU -### Dendrite is using a lot of RAM +Generally speaking, you should expect to see some CPU spikes, particularly if you are joining or participating in large rooms. However, constant/sustained high CPU usage is not expected - if you are experiencing that, please join `#dendrite-dev:matrix.org` and let us know what you were doing when the +CPU usage shot up, or file a GitHub issue. If you can take a [CPU profile](PROFILING.md) then that would +be a huge help too, as that will help us to understand where the CPU time is going. -A lot of users report that Dendrite is using a lot of RAM, sometimes even gigabytes of it. This is usually due to Go's allocator behaviour, which tries to hold onto allocated memory until the operating system wants to reclaim it for something else. This can make the memory usage look significantly inflated in tools like `top`/`htop` when actually most of that memory is not really in use at all. +## Dendrite is using a lot of RAM -If you want to prevent this behaviour so that the Go runtime releases memory normally, start Dendrite using the `GODEBUG=madvdontneed=1` environment variable. It is also expected that the allocator behaviour will be changed again in Go 1.16 so that it does not hold onto memory unnecessarily in this way. +As above with CPU usage, some memory spikes are expected if Dendrite is doing particularly heavy work +at a given instant. However, if it is using more RAM than you expect for a long time, that's probably +not expected. Join `#dendrite-dev:matrix.org` and let us know what you were doing when the memory usage +ballooned, or file a GitHub issue if you can. If you can take a [memory profile](PROFILING.md) then that +would be a huge help too, as that will help us to understand where the memory usage is happening. -If you are running with `GODEBUG=madvdontneed=1` and still see hugely inflated memory usage then that's quite possibly a bug - please join `#dendrite-dev:matrix.org` and let us know, or file a GitHub issue. - -### Dendrite is running out of PostgreSQL database connections +## Dendrite is running out of PostgreSQL database connections You may need to revisit the connection limit of your PostgreSQL server and/or make changes to the `max_connections` lines in your Dendrite configuration. Be aware that each Dendrite component opens its own database connections and has its own connection limit, even in monolith mode! + +## What is being reported when enabling anonymous stats? + +If anonymous stats reporting is enabled, the following data is send to the defined endpoint. + +```json +{ + "cpu_average": 0, + "daily_active_users": 97, + "daily_e2ee_messages": 0, + "daily_messages": 0, + "daily_sent_e2ee_messages": 0, + "daily_sent_messages": 0, + "daily_user_type_bridged": 2, + "daily_user_type_native": 97, + "database_engine": "Postgres", + "database_server_version": "11.14 (Debian 11.14-0+deb10u1)", + "federation_disabled": false, + "go_arch": "amd64", + "go_os": "linux", + "go_version": "go1.16.13", + "homeserver": "localhost:8800", + "log_level": "trace", + "memory_rss": 93452, + "monolith": true, + "monthly_active_users": 97, + "nats_embedded": true, + "nats_in_memory": true, + "num_cpu": 8, + "num_go_routine": 203, + "r30v2_users_all": 0, + "r30v2_users_android": 0, + "r30v2_users_electron": 0, + "r30v2_users_ios": 0, + "r30v2_users_web": 0, + "timestamp": 1651741851, + "total_nonbridged_users": 97, + "total_room_count": 0, + "total_users": 99, + "uptime_seconds": 30, + "version": "0.8.2" +} +``` diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 000000000..a6aa152a2 --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,5 @@ +source "https://rubygems.org" +gem "github-pages", "~> 226", group: :jekyll_plugins +group :jekyll_plugins do + gem "jekyll-feed", "~> 0.15.1" +end diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 000000000..e62aa4ce3 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,283 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.0.5) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + colorator (1.1.0) + commonmarker (0.23.4) + concurrent-ruby (1.1.10) + dnsruby (1.61.9) + simpleidn (~> 0.1) + em-websocket (0.5.3) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0) + ethon (0.15.0) + ffi (>= 1.15.0) + eventmachine (1.2.7) + execjs (2.8.1) + faraday (1.10.0) + faraday-em_http (~> 1.0) + faraday-em_synchrony (~> 1.0) + faraday-excon (~> 1.1) + faraday-httpclient (~> 1.0) + faraday-multipart (~> 1.0) + faraday-net_http (~> 1.0) + faraday-net_http_persistent (~> 1.0) + faraday-patron (~> 1.0) + faraday-rack (~> 1.0) + faraday-retry (~> 1.0) + ruby2_keywords (>= 0.0.4) + faraday-em_http (1.0.0) + faraday-em_synchrony (1.0.0) + faraday-excon (1.1.0) + faraday-httpclient (1.0.1) + faraday-multipart (1.0.3) + multipart-post (>= 1.2, < 3) + faraday-net_http (1.0.1) + faraday-net_http_persistent (1.2.0) + faraday-patron (1.0.0) + faraday-rack (1.0.0) + faraday-retry (1.0.3) + ffi (1.15.5) + forwardable-extended (2.6.0) + gemoji (3.0.1) + github-pages (226) + github-pages-health-check (= 1.17.9) + jekyll (= 3.9.2) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.2.0) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.15.1) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-include-cache (= 0.2.1) + jekyll-mentions (= 1.6.0) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.16.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.3) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.8.0) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.2.0) + jekyll-theme-cayman (= 0.2.0) + jekyll-theme-dinky (= 0.2.0) + jekyll-theme-hacker (= 0.2.0) + jekyll-theme-leap-day (= 0.2.0) + jekyll-theme-merlot (= 0.2.0) + jekyll-theme-midnight (= 0.2.0) + jekyll-theme-minimal (= 0.2.0) + jekyll-theme-modernist (= 0.2.0) + jekyll-theme-primer (= 0.6.0) + jekyll-theme-slate (= 0.2.0) + jekyll-theme-tactile (= 0.2.0) + jekyll-theme-time-machine (= 0.2.0) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.12.0) + kramdown (= 2.3.2) + kramdown-parser-gfm (= 1.1.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.13.4, < 2.0) + rouge (= 3.26.0) + terminal-table (~> 1.4) + github-pages-health-check (1.17.9) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (>= 3.0, < 5.0) + typhoeus (~> 1.3) + html-pipeline (2.14.1) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.8.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jekyll (3.9.2) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (>= 1.17, < 3) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.4.0) + commonmarker (~> 0.22) + jekyll-commonmark-ghpages (0.2.0) + commonmarker (~> 0.23.4) + jekyll (~> 3.9.0) + jekyll-commonmark (~> 1.4.0) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.15.1) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-include-cache (0.2.1) + jekyll (>= 3.7, < 5.0) + jekyll-mentions (1.6.0) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.16.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.3) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) + rubyzip (>= 1.3.0, < 3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.8.0) + jekyll (>= 3.8, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.6.0) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.2.0) + jekyll (> 3.5, < 5.0) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.12.0) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + kramdown (2.3.2) + rexml + kramdown-parser-gfm (1.1.0) + kramdown (~> 2.0) + liquid (4.0.3) + listen (3.7.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.15.0) + multipart-post (2.1.1) + nokogiri (1.13.6-arm64-darwin) + racc (~> 1.4) + octokit (4.22.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (4.0.7) + racc (1.6.0) + rb-fsevent (0.11.1) + rb-inotify (0.10.1) + ffi (~> 1.0) + rexml (3.2.5) + rouge (3.26.0) + ruby2_keywords (0.0.5) + rubyzip (2.3.2) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + simpleidn (0.2.1) + unf (~> 0.1.4) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + typhoeus (1.4.0) + ethon (>= 0.9.0) + tzinfo (1.2.9) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.8.1) + unicode-display_width (1.8.0) + zeitwerk (2.5.4) + +PLATFORMS + arm64-darwin-21 + +DEPENDENCIES + github-pages (~> 226) + jekyll-feed (~> 0.15.1) + minima (~> 2.5.1) + +BUNDLED WITH + 2.3.7 diff --git a/docs/INSTALL.md b/docs/INSTALL.md index ca1316aca..add822108 100644 --- a/docs/INSTALL.md +++ b/docs/INSTALL.md @@ -1,283 +1,15 @@ -# Installing Dendrite - -Dendrite can be run in one of two configurations: - -* **Monolith mode**: All components run in the same process. In this mode, - it is possible to run an in-process [NATS Server](https://github.com/nats-io/nats-server) - instead of running a standalone deployment. This will usually be the preferred model for - low-to-mid volume deployments, providing the best balance between performance and resource usage. - -* **Polylith mode**: A cluster of individual components running in their own processes, dealing - with different aspects of the Matrix protocol (see [WIRING.md](WIRING-Current.md)). Components - communicate with each other using internal HTTP APIs and [NATS Server](https://github.com/nats-io/nats-server). - This will almost certainly be the preferred model for very large deployments but scalability - comes with a cost. API calls are expensive and therefore a polylith deployment may end up using - disproportionately more resources for a smaller number of users compared to a monolith deployment. - -In almost all cases, it is **recommended to run in monolith mode with PostgreSQL databases**. - -Regardless of whether you are running in polylith or monolith mode, each Dendrite component that -requires storage has its own database connections. Both Postgres and SQLite are supported and can -be mixed-and-matched across components as needed in the configuration file. - -Be advised that Dendrite is still in development and it's not recommended for -use in production environments just yet! - -## Requirements - -Dendrite requires: - -* Go 1.16 or higher -* PostgreSQL 12 or higher (if using PostgreSQL databases, not needed for SQLite) - -If you want to run a polylith deployment, you also need: - -* A standalone [NATS Server](https://github.com/nats-io/nats-server) deployment with JetStream enabled - -If you want to build it on Windows, you need `gcc` in the path: - -* [MinGW-w64](https://www.mingw-w64.org/) - -## Building Dendrite - -Start by cloning the code: - -```bash -git clone https://github.com/matrix-org/dendrite -cd dendrite -``` - -Then build it: - -* Linux or UNIX-like systems: - ```bash - ./build.sh - ``` - -* Windows: - ```dos - build.cmd - ``` - -## Install NATS Server - -Follow the [NATS Server installation instructions](https://docs.nats.io/running-a-nats-service/introduction/installation) and then [start your NATS deployment](https://docs.nats.io/running-a-nats-service/introduction/running). - -JetStream must be enabled, either by passing the `-js` flag to `nats-server`, -or by specifying the `store_dir` option in the the `jetstream` configuration. - -## Configuration - -### PostgreSQL database setup - -Assuming that PostgreSQL 12 (or later) is installed: - -* Create role, choosing a new password when prompted: - - ```bash - sudo -u postgres createuser -P dendrite - ``` - -At this point you have a choice on whether to run all of the Dendrite -components from a single database, or for each component to have its -own database. For most deployments, running from a single database will -be sufficient, although you may wish to separate them if you plan to -split out the databases across multiple machines in the future. - -On macOS, omit `sudo -u postgres` from the below commands. - -* If you want to run all Dendrite components from a single database: - - ```bash - sudo -u postgres createdb -O dendrite dendrite - ``` - - ... in which case your connection string will look like `postgres://user:pass@database/dendrite`. - -* If you want to run each Dendrite component with its own database: - - ```bash - for i in mediaapi syncapi roomserver federationapi appservice keyserver userapi_accounts; do - sudo -u postgres createdb -O dendrite dendrite_$i - done - ``` - - ... in which case your connection string will look like `postgres://user:pass@database/dendrite_componentname`. - -### SQLite database setup - -**WARNING:** SQLite is suitable for small experimental deployments only and should not be used in production - use PostgreSQL instead for any user-facing federating installation! - -Dendrite can use the built-in SQLite database engine for small setups. -The SQLite databases do not need to be pre-built - Dendrite will -create them automatically at startup. - -### Server key generation - -Each Dendrite installation requires: - -* A unique Matrix signing private key -* A valid and trusted TLS certificate and private key - -To generate a Matrix signing private key: - -```bash -./bin/generate-keys --private-key matrix_key.pem -``` - -**WARNING:** Make sure take a safe backup of this key! You will likely need it if you want to reinstall Dendrite, or -any other Matrix homeserver, on the same domain name in the future. If you lose this key, you may have trouble joining -federated rooms. - -For testing, you can generate a self-signed certificate and key, although this will not work for public federation: - -```bash -./bin/generate-keys --tls-cert server.crt --tls-key server.key -``` - -If you have server keys from an older Synapse instance, -[convert them](serverkeyformat.md#converting-synapse-keys) to Dendrite's PEM -format and configure them as `old_private_keys` in your config. - -### Configuration file - -Create config file, based on `dendrite-config.yaml`. Call it `dendrite.yaml`. Things that will need editing include *at least*: - -* The `server_name` entry to reflect the hostname of your Dendrite server -* The `database` lines with an updated connection string based on your - desired setup, e.g. replacing `database` with the name of the database: - * For Postgres: `postgres://dendrite:password@localhost/database`, e.g. - * `postgres://dendrite:password@localhost/dendrite_userapi_account` to connect to PostgreSQL with SSL/TLS - * `postgres://dendrite:password@localhost/dendrite_userapi_account?sslmode=disable` to connect to PostgreSQL without SSL/TLS - * For SQLite on disk: `file:component.db` or `file:///path/to/component.db`, e.g. `file:userapi_account.db` - * Postgres and SQLite can be mixed and matched on different components as desired. -* Either one of the following in the `jetstream` configuration section: - * The `addresses` option — a list of one or more addresses of an external standalone - NATS Server deployment - * The `storage_path` — where on the filesystem the built-in NATS server should - store durable queues, if using the built-in NATS server - -There are other options which may be useful so review them all. In particular, -if you are trying to federate from your Dendrite instance into public rooms -then configuring `key_perspectives` (like `matrix.org` in the sample) can -help to improve reliability considerably by allowing your homeserver to fetch -public keys for dead homeservers from somewhere else. - -**WARNING:** Dendrite supports running all components from the same database in -PostgreSQL mode, but this is **NOT** a supported configuration with SQLite. When -using SQLite, all components **MUST** use their own database file. - -## Starting a monolith server - -The monolith server can be started as shown below. By default it listens for -HTTP connections on port 8008, so you can configure your Matrix client to use -`http://servername:8008` as the server: - -```bash -./bin/dendrite-monolith-server -``` - -If you set `--tls-cert` and `--tls-key` as shown below, it will also listen -for HTTPS connections on port 8448: - -```bash -./bin/dendrite-monolith-server --tls-cert=server.crt --tls-key=server.key -``` - -If the `jetstream` section of the configuration contains no `addresses` but does -contain a `store_dir`, Dendrite will start up a built-in NATS JetStream node -automatically, eliminating the need to run a separate NATS server. - -## Starting a polylith deployment - -The following contains scripts which will run all the required processes in order to point a Matrix client at Dendrite. - -### nginx (or other reverse proxy) - -This is what your clients and federated hosts will talk to. It must forward -requests onto the correct API server based on URL: - -* `/_matrix/client` to the client API server -* `/_matrix/federation` to the federation API server -* `/_matrix/key` to the federation API server -* `/_matrix/media` to the media API server - -See `docs/nginx/polylith-sample.conf` for a sample configuration. - -### Client API server - -This is what implements CS API endpoints. Clients talk to this via the proxy in -order to send messages, create and join rooms, etc. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml clientapi -``` - -### Sync server - -This is what implements `/sync` requests. Clients talk to this via the proxy -in order to receive messages. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml syncapi -``` - -### Media server - -This implements `/media` requests. Clients talk to this via the proxy in -order to upload and retrieve media. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml mediaapi -``` - -### Federation API server - -This implements the federation API. Servers talk to this via the proxy in -order to send transactions. This is only required if you want to support -federation. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml federationapi -``` - -### Internal components - -This refers to components that are not directly spoken to by clients. They are only -contacted by other components. This includes the following components. - -#### Room server - -This is what implements the room DAG. Clients do not talk to this. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml roomserver -``` - -#### Appservice server - -This sends events from the network to [application -services](https://matrix.org/docs/spec/application_service/unstable.html) -running locally. This is only required if you want to support running -application services on your homeserver. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml appservice -``` - -#### Key server - -This manages end-to-end encryption keys for users. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml keyserver -``` - -#### User server - -This manages user accounts, device access tokens and user account data, -amongst other things. - -```bash -./bin/dendrite-polylith-multi --config=dendrite.yaml userapi -``` +# Installation + +Please note that new installation instructions can be found +on the [new documentation site](https://matrix-org.github.io/dendrite/), +or alternatively, in the [installation](installation/) folder: + +1. [Planning your deployment](installation/1_planning.md) +2. [Setting up the domain](installation/2_domainname.md) +3. [Preparing database storage](installation/3_database.md) +4. [Generating signing keys](installation/4_signingkey.md) +5. [Installing as a monolith](installation/5_install_monolith.md) +6. [Installing as a polylith](installation/6_install_polylith.md) +7. [Populate the configuration](installation/7_configuration.md) +8. [Starting the monolith](installation/8_starting_monolith.md) +9. [Starting the polylith](installation/9_starting_polylith.md) diff --git a/docs/PROFILING.md b/docs/PROFILING.md index b026a8aed..f3b573472 100644 --- a/docs/PROFILING.md +++ b/docs/PROFILING.md @@ -1,8 +1,14 @@ +--- +title: Profiling +parent: Development +permalink: /development/profiling +--- + # Profiling Dendrite If you are running into problems with Dendrite using excessive resources (e.g. CPU or RAM) then you can use the profiler to work out what is happening. -Dendrite contains an embedded profiler called `pprof`, which is a part of the standard Go toolchain. +Dendrite contains an embedded profiler called `pprof`, which is a part of the standard Go toolchain. ## Enable the profiler @@ -16,7 +22,7 @@ If pprof has been enabled successfully, a log line at startup will show that ppr ``` WARN[2020-12-03T13:32:33.669405000Z] [/Users/neilalexander/Desktop/dendrite/internal/log.go:87] SetupPprof - Starting pprof on localhost:65432 + Starting pprof on localhost:65432 ``` All examples from this point forward assume `PPROFLISTEN=localhost:65432` but you may need to adjust as necessary for your setup. diff --git a/docs/WIRING-Current.md b/docs/WIRING-Current.md deleted file mode 100644 index b74f341e5..000000000 --- a/docs/WIRING-Current.md +++ /dev/null @@ -1,71 +0,0 @@ -This document details how various components communicate with each other. There are two kinds of components: - - Public-facing: exposes CS/SS API endpoints and need to be routed to via client-api-proxy or equivalent. - - Internal-only: exposes internal APIs and produces Kafka events. - -## Internal HTTP APIs - -Not everything can be done using Kafka logs. For example, requesting the latest events in a room is much better suited to -a request/response model like HTTP or RPC. Therefore, components can expose "internal APIs" which sit outside of Kafka logs. -Note in Monolith mode these are actually direct function calls and are not serialised HTTP requests. - -``` - Tier 1 Sync FederationAPI ClientAPI MediaAPI -Public Facing | | | | | | | | | | - 2 .-------3-----------------` | | | `--------|-|-|-|--11--------------------. - | | .--------4----------------------------------` | | | | - | | | .---5-----------` | | | | | | - | | | | .---6----------------------------` | | | - | | | | | | .-----7----------` | | - | | | | | 8 | | 10 | - | | | | | | | `---9----. | | - V V V V V V V V V V - Tier 2 Roomserver EDUServer FedSender AppService KeyServer ServerKeyAPI -Internal only | `------------------------12----------^ ^ - `------------------------------------------------------------13----------` - - Client ---> Server -``` -- 2 (Sync -> Roomserver): When making backfill requests -- 3 (FedAPI -> Roomserver): Calculating (prev/auth events) and sending new events, processing backfill/state/state_ids requests -- 4 (ClientAPI -> Roomserver): Calculating (prev/auth events) and sending new events, processing /state requests -- 5 (FedAPI -> EDUServer): Sending typing/send-to-device events -- 6 (ClientAPI -> EDUServer): Sending typing/send-to-device events -- 7 (ClientAPI -> FedSender): Handling directory lookups -- 8 (FedAPI -> FedSender): Resetting backoffs when receiving traffic from a server. Querying joined hosts when handling alias lookup requests -- 9 (FedAPI -> AppService): Working out if the client is an appservice user -- 10 (ClientAPI -> AppService): Working out if the client is an appservice user -- 11 (FedAPI -> ServerKeyAPI): Verifying incoming event signatures -- 12 (FedSender -> ServerKeyAPI): Verifying event signatures of responses (e.g from send_join) -- 13 (Roomserver -> ServerKeyAPI): Verifying event signatures of backfilled events - -In addition to this, all public facing components (Tier 1) talk to the `UserAPI` to verify access tokens and extract profile information where needed. - -## Kafka logs - -``` - .----1--------------------------------------------. - V | - Tier 1 Sync FederationAPI ClientAPI MediaAPI -Public Facing ^ ^ ^ - | | | - 2 | | - | `-3------------. | - | | | - | | | - | | | - | .--------4-----|------------------------------` - | | | - Tier 2 Roomserver EDUServer FedSender AppService KeyServer ServerKeyAPI -Internal only | | ^ ^ - | `-----5----------` | - `--------------------6--------` - - -Producer ----> Consumer -``` -- 1 (ClientAPI -> Sync): For tracking account data -- 2 (Roomserver -> Sync): For all data to send to clients -- 3 (EDUServer -> Sync): For typing/send-to-device data to send to clients -- 4 (Roomserver -> ClientAPI): For tracking memberships for profile updates. -- 5 (EDUServer -> FedSender): For sending EDUs over federation -- 6 (Roomserver -> FedSender): For sending PDUs over federation, for tracking joined hosts. diff --git a/docs/WIRING.md b/docs/WIRING.md deleted file mode 100644 index 8ec5b0432..000000000 --- a/docs/WIRING.md +++ /dev/null @@ -1,229 +0,0 @@ -# Wiring - -The diagram is incomplete. The following things aren't shown on the diagram: - -* Device Messages -* User Profiles -* Notification Counts -* Sending federation. -* Querying federation. -* Other things that aren't shown on the diagram. - -Diagram: - - - W -> Writer - S -> Server/Store/Service/Something/Stuff - R -> Reader - - +---+ +---+ +---+ - +----------| W | +----------| S | +--------| R | - | +---+ | Receipts +---+ | Client +---+ - | Federation |>=========================================>| Server |>=====================>| Sync | - | Receiver | | | | | - | | +---+ | | | | - | | +--------| W | | | | | - | | | Client +---+ | | | | - | | | Receipt |>=====>| | | | - | | | Updater | | | | | - | | +----------+ | | | | - | | | | | | - | | +---+ +---+ | | +---+ | | - | | +------------| W | +------| S | | | +--------| R | | | - | | | Federation +---+ | Room +---+ | | | Client +---+ | | - | | | Backfill |>=====>| Server |>=====>| |>=====>| Push | | | - | | +--------------+ | | +------------+ | | | | - | | | | | | | | - | | | |>==========================>| | | | - | | | | +----------+ | | - | | | | +---+ | | - | | | | +-------------| R | | | - | | | |>=====>| Application +---+ | | - | | | | | Services | | | - | | | | +--------------+ | | - | | | | +---+ | | - | | | | +--------| R | | | - | | | | | Client +---+ | | - | |>========================>| |>==========================>| Search | | | - | | | | | | | | - | | | | +----------+ | | - | | | | | | - | | | |>==========================================>| | - | | | | | | - | | +---+ | | +---+ | | - | | +--------| W | | | +----------| S | | | - | | | Client +---+ | | | Presence +---+ | | - | | | API |>=====>| |>=====>| Server |>=====================>| | - | | | /send | +--------+ | | | | - | | | | | | | | - | | | |>======================>| |<=====================<| | - | | +----------+ | | | | - | | | | | | - | | +---+ | | | | - | | +--------| W | | | | | - | | | Client +---+ | | | | - | | | Presence |>=====>| | | | - | | | Setter | | | | | - | | +----------+ | | | | - | | | | | | - | | | | | | - | |>=========================================>| | | | - | | +------------+ | | - | | | | - | | +---+ | | - | | +----------| S | | | - | | | EDU +---+ | | - | |>=========================================>| Server |>=====================>| | - +------------+ | | +----------+ - +---+ | | - +--------| W | | | - | Client +---+ | | - | Typing |>=====>| | - | Setter | | | - +----------+ +------------+ - - -# Component Descriptions - -Many of the components are logical rather than physical. For example it is -possible that all of the client API writers will end up being glued together -and always deployed as a single unit. - -Outbound federation requests will probably need to be funnelled through a -choke-point to implement ratelimiting and backoff correctly. - -## Federation Send - - * Handles `/federation/v1/send/` requests. - * Fetches missing ``prev_events`` from the remote server if needed. - * Fetches missing room state from the remote server if needed. - * Checks signatures on remote events, downloading keys if needed. - * Queries information needed to process events from the Room Server. - * Writes room events to logs. - * Writes presence updates to logs. - * Writes receipt updates to logs. - * Writes typing updates to logs. - * Writes other updates to logs. - -## Client API /send - - * Handles puts to `/client/v1/rooms/` that create room events. - * Queries information needed to process events from the Room Server. - * Talks to remote servers if needed for joins and invites. - * Writes room event pdus. - * Writes presence updates to logs. - -## Client Presence Setter - - * Handles puts to the [client API presence paths](https://matrix.org/docs/spec/client_server/unstable.html#id41). - * Writes presence updates to logs. - -## Client Typing Setter - - * Handles puts to the [client API typing paths](https://matrix.org/docs/spec/client_server/unstable.html#id32). - * Writes typing updates to logs. - -## Client Receipt Updater - - * Handles puts to the [client API receipt paths](https://matrix.org/docs/spec/client_server/unstable.html#id36). - * Writes receipt updates to logs. - -## Federation Backfill - - * Backfills events from other servers - * Writes the resulting room events to logs. - * Is a different component from the room server itself cause it'll - be easier if the room server component isn't making outbound HTTP requests - to remote servers - -## Room Server - - * Reads new and backfilled room events from the logs written by FS, FB and CRS. - * Tracks the current state of the room and the state at each event. - * Probably does auth checks on the incoming events. - * Handles state resolution as part of working out the current state and the - state at each event. - * Writes updates to the current state and new events to logs. - * Shards by room ID. - -## Receipt Server - - * Reads new updates to receipts from the logs written by the FS and CRU. - * Somehow learns enough information from the room server to workout how the - current receipt markers move with each update. - * Writes the new marker positions to logs - * Shards by room ID? - * It may be impossible to implement without folding it into the Room Server - forever coupling the components together. - -## EDU Server - - * Reads new updates to typing from the logs written by the FS and CTS. - * Updates the current list of people typing in a room. - * Writes the current list of people typing in a room to the logs. - * Shards by room ID? - -## Presence Server - - * Reads the current state of the rooms from the logs to track the intersection - of room membership between users. - * Reads updates to presence from the logs written by the FS and the CPS. - * Reads when clients sync from the logs from the Client Sync. - * Tracks any timers for users. - * Writes the changes to presence state to the logs. - * Shards by user ID somehow? - -## Client Sync - - * Handle /client/v2/sync requests. - * Reads new events and the current state of the rooms from logs written by the Room Server. - * Reads new receipts positions from the logs written by the Receipts Server. - * Reads changes to presence from the logs written by the Presence Server. - * Reads changes to typing from the logs written by the EDU Server. - * Writes when a client starts and stops syncing to the logs. - -## Client Search - - * Handle whatever the client API path for event search is? - * Reads new events and the current state of the rooms from logs writeen by the Room Server. - * Maintains a full text search index of somekind. - -## Client Push - - * Pushes unread messages to remote push servers. - * Reads new events and the current state of the rooms from logs writeen by the Room Server. - * Reads the position of the read marker from the Receipts Server. - * Makes outbound HTTP hits to the push server for the client device. - -## Application Service - - * Receives events from the Room Server. - * Filters events and sends them to each registered application service. - * Runs a separate goroutine for each application service. - -# Internal Component API - -Some dendrite components use internal APIs to communicate information back -and forth between each other. There are two implementations of each API, one -that uses HTTP requests and one that does not. The HTTP implementation is -used in multi-process mode, so processes on separate computers may still -communicate, whereas in single-process or Monolith mode, the direct -implementation is used. HTTP is preferred here to kafka streams as it allows -for request responses. - -Running `dendrite-monolith-server` will set up direct connections between -components, whereas running each individual component (which are only run in -multi-process mode) will set up HTTP-based connections. - -The functions that make HTTP requests to internal APIs of a component are -located in `//api/.go`, named according to what -functionality they cover. Each of these requests are handled in `///.go`. - -As an example, the `appservices` component allows other Dendrite components -to query external application services via its internal API. A component -would call the desired function in `/appservices/api/query.go`. In -multi-process mode, this would send an internal HTTP request, which would -be handled by a function in `/appservices/query/query.go`. In single-process -mode, no internal HTTP request occurs, instead functions are simply called -directly, thus requiring no changes on the calling component's end. diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 000000000..ed93fd796 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,19 @@ +title: Dendrite +description: >- + Second-generation Matrix homeserver written in Go! +baseurl: "/dendrite" # the subpath of your site, e.g. /blog +url: "" +twitter_username: matrixdotorg +github_username: matrix-org +remote_theme: just-the-docs/just-the-docs +plugins: + - jekyll-feed +aux_links: + "GitHub": + - "//github.com/matrix-org/dendrite" +aux_links_new_tab: true +sass: + sass_dir: _sass + style: compressed +exclude: + - INSTALL.md diff --git a/docs/_sass/custom/custom.scss b/docs/_sass/custom/custom.scss new file mode 100644 index 000000000..8a5ed3d8d --- /dev/null +++ b/docs/_sass/custom/custom.scss @@ -0,0 +1,3 @@ +footer.site-footer { + opacity: 10%; +} \ No newline at end of file diff --git a/docs/administration.md b/docs/administration.md new file mode 100644 index 000000000..08ad7803e --- /dev/null +++ b/docs/administration.md @@ -0,0 +1,10 @@ +--- +title: Administration +has_children: yes +nav_order: 4 +permalink: /administration +--- + +# Administration + +This section contains documentation on managing your existing Dendrite deployment. diff --git a/docs/administration/1_createusers.md b/docs/administration/1_createusers.md new file mode 100644 index 000000000..f40b7f576 --- /dev/null +++ b/docs/administration/1_createusers.md @@ -0,0 +1,53 @@ +--- +title: Creating user accounts +parent: Administration +permalink: /administration/createusers +nav_order: 1 +--- + +# Creating user accounts + +User accounts can be created on a Dendrite instance in a number of ways. + +## From the command line + +The `create-account` tool is built in the `bin` folder when building Dendrite with +the `build.sh` script. + +It uses the `dendrite.yaml` configuration file to connect to the Dendrite user database +and create the account entries directly. It can therefore be used even if Dendrite is not +running yet, as long as the database is up. + +An example of using `create-account` to create a **normal account**: + +```bash +./bin/create-account -config /path/to/dendrite.yaml -username USERNAME +``` + +You will be prompted to enter a new password for the new account. + +To create a new **admin account**, add the `-admin` flag: + +```bash +./bin/create-account -config /path/to/dendrite.yaml -username USERNAME -admin +``` + +## Using shared secret registration + +Dendrite supports the Synapse-compatible shared secret registration endpoint. + +To enable shared secret registration, you must first enable it in the `dendrite.yaml` +configuration file by specifying a shared secret. In the `client_api` section of the config, +enter a new secret into the `registration_shared_secret` field: + +```yaml +client_api: + # ... + registration_shared_secret: "" +``` + +You can then use the `/_synapse/admin/v1/register` endpoint as per the +[Synapse documentation](https://matrix-org.github.io/synapse/latest/admin_api/register_api.html). + +Shared secret registration is only enabled once a secret is configured. To disable shared +secret registration again, remove the secret from the configuration file. diff --git a/docs/administration/2_registration.md b/docs/administration/2_registration.md new file mode 100644 index 000000000..66949f2ca --- /dev/null +++ b/docs/administration/2_registration.md @@ -0,0 +1,53 @@ +--- +title: Enabling registration +parent: Administration +permalink: /administration/registration +nav_order: 2 +--- + +# Enabling registration + +Enabling registration allows users to register their own user accounts on your +Dendrite server using their Matrix client. They will be able to choose their own +username and password and log in. + +Registration is controlled by the `registration_disabled` field in the `client_api` +section of the configuration. By default, `registration_disabled` is set to `true`, +disabling registration. If you want to enable registration, you should change this +setting to `false`. + +Currently Dendrite supports secondary verification using [reCAPTCHA](https://www.google.com/recaptcha/about/). +Other methods will be supported in the future. + +## reCAPTCHA verification + +Dendrite supports reCAPTCHA as a secondary verification method. If you want to enable +registration, it is **highly recommended** to configure reCAPTCHA. This will make it +much more difficult for automated spam systems from registering accounts on your +homeserver automatically. + +You will need an API key from the [reCAPTCHA Admin Panel](https://www.google.com/recaptcha/admin). +Then configure the relevant details in the `client_api` section of the configuration: + +```yaml +client_api: + # ... + registration_disabled: false + recaptcha_public_key: "PUBLIC_KEY_HERE" + recaptcha_private_key: "PRIVATE_KEY_HERE" + enable_registration_captcha: true + captcha_bypass_secret: "" + recaptcha_siteverify_api: "https://www.google.com/recaptcha/api/siteverify" +``` + +## Open registration + +Dendrite does support open registration — that is, allowing users to create their own +user accounts without any verification or secondary authentication. However, it +is **not recommended** to enable open registration, as this leaves your homeserver +vulnerable to abuse by spammers or attackers, who create large numbers of user +accounts on Matrix homeservers in order to send spam or abuse into the network. + +It isn't possible to enable open registration in Dendrite in a single step. If you +try to disable the `registration_disabled` option without any secondary verification +methods enabled (such as reCAPTCHA), Dendrite will log an error and fail to start. diff --git a/docs/administration/3_presence.md b/docs/administration/3_presence.md new file mode 100644 index 000000000..858025370 --- /dev/null +++ b/docs/administration/3_presence.md @@ -0,0 +1,39 @@ +--- +title: Enabling presence +parent: Administration +permalink: /administration/presence +nav_order: 3 +--- + +# Enabling presence + +Dendrite supports presence, which allows you to send your online/offline status +to other users, and to receive their statuses automatically. They will be displayed +by supported clients. + +Note that enabling presence **can negatively impact** the performance of your Dendrite +server — it will require more CPU time and will increase the "chattiness" of your server +over federation. It is disabled by default for this reason. + +Dendrite has two options for controlling presence: + +* **Enable inbound presence**: Dendrite will handle presence updates for remote users + and distribute them to local users on your homeserver; +* **Enable outbound presence**: Dendrite will generate presence notifications for your + local users and distribute them to remote users over the federation. + +This means that you can configure only one or other direction if you prefer, i.e. to +receive presence from other servers without revealing the presence of your own users. + +## Configuring presence + +Presence is controlled by the `presence` block in the `global` section of the +configuration file: + +```yaml +global: + # ... + presence: + enable_inbound: false + enable_outbound: false +``` diff --git a/docs/administration/4_adminapi.md b/docs/administration/4_adminapi.md new file mode 100644 index 000000000..e33482ec9 --- /dev/null +++ b/docs/administration/4_adminapi.md @@ -0,0 +1,25 @@ +--- +title: Supported admin APIs +parent: Administration +permalink: /administration/adminapi +--- + +# Supported admin APIs + +Dendrite supports, at present, a very small number of endpoints that allow +admin users to perform administrative functions. Please note that there is no +API stability guarantee on these endpoints at present — they may change shape +without warning. + +More endpoints will be added in the future. + +## `/_dendrite/admin/evacuateRoom/{roomID}` + +This endpoint will instruct Dendrite to part all local users from the given `roomID` +in the URL. It may take some time to complete. A JSON body will be returned containing +the user IDs of all affected users. + +## `/_synapse/admin/v1/register` + +Shared secret registration — please see the [user creation page](createusers) for +guidance on configuring and using this endpoint. diff --git a/docs/coverage.md b/docs/coverage.md new file mode 100644 index 000000000..7a3b7cb9e --- /dev/null +++ b/docs/coverage.md @@ -0,0 +1,84 @@ +--- +title: Coverage +parent: Development +permalink: /development/coverage +--- + +To generate a test coverage report for Sytest, a small patch needs to be applied to the Sytest repository to compile and use the instrumented binary: +```patch +diff --git a/lib/SyTest/Homeserver/Dendrite.pm b/lib/SyTest/Homeserver/Dendrite.pm +index 8f0e209c..ad057e52 100644 +--- a/lib/SyTest/Homeserver/Dendrite.pm ++++ b/lib/SyTest/Homeserver/Dendrite.pm +@@ -337,7 +337,7 @@ sub _start_monolith + + $output->diag( "Starting monolith server" ); + my @command = ( +- $self->{bindir} . '/dendrite-monolith-server', ++ $self->{bindir} . '/dendrite-monolith-server', '--test.coverprofile=' . $self->{hs_dir} . '/integrationcover.log', "DEVEL", + '--config', $self->{paths}{config}, + '--http-bind-address', $self->{bind_host} . ':' . $self->unsecure_port, + '--https-bind-address', $self->{bind_host} . ':' . $self->secure_port, +diff --git a/scripts/dendrite_sytest.sh b/scripts/dendrite_sytest.sh +index f009332b..7ea79869 100755 +--- a/scripts/dendrite_sytest.sh ++++ b/scripts/dendrite_sytest.sh +@@ -34,7 +34,8 @@ export GOBIN=/tmp/bin + echo >&2 "--- Building dendrite from source" + cd /src + mkdir -p $GOBIN +-go install -v ./cmd/dendrite-monolith-server ++# go install -v ./cmd/dendrite-monolith-server ++go test -c -cover -covermode=atomic -o $GOBIN/dendrite-monolith-server -coverpkg "github.com/matrix-org/..." ./cmd/dendrite-monolith-server + go install -v ./cmd/generate-keys + cd - + ``` + + Then run Sytest. This will generate a new file `integrationcover.log` in each server's directory e.g `server-0/integrationcover.log`. To parse it, + ensure your working directory is under the Dendrite repository then run: + ```bash + go tool cover -func=/path/to/server-0/integrationcover.log + ``` + which will produce an output like: + ``` + ... + github.com/matrix-org/util/json.go:83: NewJSONRequestHandler 100.0% +github.com/matrix-org/util/json.go:90: Protect 57.1% +github.com/matrix-org/util/json.go:110: RequestWithLogging 100.0% +github.com/matrix-org/util/json.go:132: MakeJSONAPI 70.0% +github.com/matrix-org/util/json.go:151: respond 61.5% +github.com/matrix-org/util/json.go:180: WithCORSOptions 0.0% +github.com/matrix-org/util/json.go:191: SetCORSHeaders 100.0% +github.com/matrix-org/util/json.go:202: RandomString 100.0% +github.com/matrix-org/util/json.go:210: init 100.0% +github.com/matrix-org/util/unique.go:13: Unique 91.7% +github.com/matrix-org/util/unique.go:48: SortAndUnique 100.0% +github.com/matrix-org/util/unique.go:55: UniqueStrings 100.0% +total: (statements) 53.7% +``` +The total coverage for this run is the last line at the bottom. However, this value is misleading because Dendrite can run in many different configurations, +which will never be tested in a single test run (e.g sqlite or postgres, monolith or polylith). To get a more accurate value, additional processing is required +to remove packages which will never be tested and extension MSCs: +```bash +# These commands are all similar but change which package paths are _removed_ from the output. + +# For Postgres (monolith) +go tool cover -func=/path/to/server-0/integrationcover.log | grep 'github.com/matrix-org/dendrite' | grep -Ev 'inthttp|sqlite|setup/mscs|api_trace' > coverage.txt + +# For Postgres (polylith) +go tool cover -func=/path/to/server-0/integrationcover.log | grep 'github.com/matrix-org/dendrite' | grep -Ev 'sqlite|setup/mscs|api_trace' > coverage.txt + +# For SQLite (monolith) +go tool cover -func=/path/to/server-0/integrationcover.log | grep 'github.com/matrix-org/dendrite' | grep -Ev 'inthttp|postgres|setup/mscs|api_trace' > coverage.txt + +# For SQLite (polylith) +go tool cover -func=/path/to/server-0/integrationcover.log | grep 'github.com/matrix-org/dendrite' | grep -Ev 'postgres|setup/mscs|api_trace' > coverage.txt +``` + +A total value can then be calculated using: +```bash +cat coverage.txt | awk -F '\t+' '{x = x + $3} END {print x/NR}' +``` + + +We currently do not have a way to combine Sytest/Complement/Unit Tests into a single coverage report. \ No newline at end of file diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 000000000..cf296fb53 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,10 @@ +--- +title: Development +has_children: true +permalink: /development +--- + +# Development + +This section contains documentation that may be useful when helping to develop +Dendrite. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..64836152c --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +--- +layout: home +nav_exclude: true +--- + +# Dendrite + +Dendrite is a second-generation Matrix homeserver written in Go! Following the microservice +architecture model, Dendrite is designed to be efficient, reliable and scalable. Despite being beta, +many Matrix features are already supported. + +This site aims to include relevant documentation to help you to get started with and +run Dendrite. Check out the following sections: + +* **[Installation](installation.md)** for building and deploying your own Dendrite homeserver +* **[Administration](administration.md)** for managing an existing Dendrite deployment +* **[Development](development.md)** for developing against Dendrite + +You can also join us in our Matrix rooms dedicated to Dendrite, but please check first that +your question hasn't already been [answered in the FAQ](FAQ.md): + +* **[#dendrite:matrix.org](https://matrix.to/#/#dendrite:matrix.org)** for general project discussion and support +* **[#dendrite-dev:matrix.org](https://matrix.to/#/#dendrite-dev:matrix.org)** for chat on Dendrite development specifically +* **[#dendrite-alerts:matrix.org](https://matrix.to/#/#dendrite-alerts:matrix.org)** for release notifications and other important announcements diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 000000000..c38a6dbb2 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,10 @@ +--- +title: Installation +has_children: true +nav_order: 2 +permalink: /installation +--- + +# Installation + +This section contains documentation on installing a new Dendrite deployment. diff --git a/docs/installation/1_planning.md b/docs/installation/1_planning.md new file mode 100644 index 000000000..89cc5b4a6 --- /dev/null +++ b/docs/installation/1_planning.md @@ -0,0 +1,110 @@ +--- +title: Planning your installation +parent: Installation +nav_order: 1 +permalink: /installation/planning +--- + +# Planning your installation + +## Modes + +Dendrite can be run in one of two configurations: + +* **Monolith mode**: All components run in the same process. In this mode, + it is possible to run an in-process NATS Server instead of running a standalone deployment. + This will usually be the preferred model for low-to-mid volume deployments, providing the best + balance between performance and resource usage. + +* **Polylith mode**: A cluster of individual components running in their own processes, dealing + with different aspects of the Matrix protocol. Components communicate with each other using + internal HTTP APIs and NATS Server. This will almost certainly be the preferred model for very + large deployments but scalability comes with a cost. API calls are expensive and therefore a + polylith deployment may end up using disproportionately more resources for a smaller number of + users compared to a monolith deployment. + +At present, we **recommend monolith mode deployments** in all cases. + +## Databases + +Dendrite can run with either a PostgreSQL or a SQLite backend. There are considerable tradeoffs +to consider: + +* **PostgreSQL**: Needs to run separately to Dendrite, needs to be installed and configured separately + and and will use more resources over all, but will be **considerably faster** than SQLite. PostgreSQL + has much better write concurrency which will allow Dendrite to process more tasks in parallel. This + will be necessary for federated deployments to perform adequately. + +* **SQLite**: Built into Dendrite, therefore no separate database engine is necessary and is quite + a bit easier to set up, but will be much slower than PostgreSQL in most cases. SQLite only allows a + single writer on a database at a given time, which will significantly restrict Dendrite's ability + to process multiple tasks in parallel. + +At this time, we **recommend the PostgreSQL database engine** for all production deployments. + +## Requirements + +Dendrite will run on Linux, macOS and Windows Server. It should also run fine on variants +of BSD such as FreeBSD and OpenBSD. We have not tested Dendrite on AIX, Solaris, Plan 9 or z/OS — +your mileage may vary with these platforms. + +It is difficult to state explicitly the amount of CPU, RAM or disk space that a Dendrite +installation will need, as this varies considerably based on a number of factors. In particular: + +* The number of users using the server; +* The number of rooms that the server is joined to — federated rooms in particular will typically + use more resources than rooms with only local users; +* The complexity of rooms that the server is joined to — rooms with more members coming and + going will typically be of a much higher complexity. + +Some tasks are more expensive than others, such as joining rooms over federation, running state +resolution or sending messages into very large federated rooms with lots of remote users. Therefore +you should plan accordingly and ensure that you have enough resources available to endure spikes +in CPU or RAM usage, as these may be considerably higher than the idle resource usage. + +At an absolute minimum, Dendrite will expect 1GB RAM. For a comfortable day-to-day deployment +which can participate in federated rooms for a number of local users, be prepared to assign 2-4 +CPU cores and 8GB RAM — more if your user count increases. + +If you are running PostgreSQL on the same machine, allow extra headroom for this too, as the +database engine will also have CPU and RAM requirements of its own. Running too many heavy +services on the same machine may result in resource starvation and processes may end up being +killed by the operating system if they try to use too much memory. + +## Dependencies + +In order to install Dendrite, you will need to satisfy the following dependencies. + +### Go + +At this time, Dendrite supports being built with Go 1.16 or later. We do not support building +Dendrite with older versions of Go than this. If you are installing Go using a package manager, +you should check (by running `go version`) that you are using a suitable version before you start. + +### PostgreSQL + +If using the PostgreSQL database engine, you should install PostgreSQL 12 or later. + +### NATS Server + +Monolith deployments come with a built-in [NATS Server](https://github.com/nats-io/nats-server) and +therefore do not need this to be manually installed. If you are planning a monolith installation, you +do not need to do anything. + +Polylith deployments, however, currently need a standalone NATS Server installation with JetStream +enabled. + +To do so, follow the [NATS Server installation instructions](https://docs.nats.io/running-a-nats-service/introduction/installation) and then [start your NATS deployment](https://docs.nats.io/running-a-nats-service/introduction/running). JetStream must be enabled, either by passing the `-js` flag to `nats-server`, +or by specifying the `store_dir` option in the the `jetstream` configuration. + +### Reverse proxy (polylith deployments) + +Polylith deployments require a reverse proxy, such as [NGINX](https://www.nginx.com) or +[HAProxy](http://www.haproxy.org). Configuring those is not covered in this documentation, +although a [sample configuration for NGINX](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/polylith-sample.conf) +is provided. + +### Windows + +Finally, if you want to build Dendrite on Windows, you will need need `gcc` in the path. The best +way to achieve this is by installing and building Dendrite under [MinGW-w64](https://www.mingw-w64.org/). diff --git a/docs/installation/2_domainname.md b/docs/installation/2_domainname.md new file mode 100644 index 000000000..0d4300eca --- /dev/null +++ b/docs/installation/2_domainname.md @@ -0,0 +1,93 @@ +--- +title: Setting up the domain +parent: Installation +nav_order: 2 +permalink: /installation/domainname +--- + +# Setting up the domain + +Every Matrix server deployment requires a server name which uniquely identifies it. For +example, if you are using the server name `example.com`, then your users will have usernames +that take the format `@user:example.com`. + +For federation to work, the server name must be resolvable by other homeservers on the internet +— that is, the domain must be registered and properly configured with the relevant DNS records. + +Matrix servers discover each other when federating using the following methods: + +1. If a well-known delegation exists on `example.com`, use the path server from the + well-known file to connect to the remote homeserver; +2. If a DNS SRV delegation exists on `example.com`, use the hostname and port from the DNS SRV + record to connect to the remote homeserver; +3. If neither well-known or DNS SRV delegation are configured, attempt to connect to the remote + homeserver by connecting to `example.com` port TCP/8448 using HTTPS. + +## TLS certificates + +Matrix federation requires that valid TLS certificates are present on the domain. You must +obtain certificates from a publicly accepted Certificate Authority (CA). [LetsEncrypt](https://letsencrypt.org) +is an example of such a CA that can be used. Self-signed certificates are not suitable for +federation and will typically not be accepted by other homeservers. + +A common practice to help ease the management of certificates is to install a reverse proxy in +front of Dendrite which manages the TLS certificates and HTTPS proxying itself. Software such as +[NGINX](https://www.nginx.com) and [HAProxy](http://www.haproxy.org) can be used for the task. +Although the finer details of configuring these are not described here, you must reverse proxy +all `/_matrix` paths to your Dendrite server. + +It is possible for the reverse proxy to listen on the standard HTTPS port TCP/443 so long as your +domain delegation is configured to point to port TCP/443. + +## Delegation + +Delegation allows you to specify the server name and port that your Dendrite installation is +reachable at, or to host the Dendrite server at a different server name to the domain that +is being delegated. + +For example, if your Dendrite installation is actually reachable at `matrix.example.com` port 8448, +you will be able to delegate from `example.com` to `matrix.example.com` so that your users will have +`@user:example.com` user names instead of `@user:matrix.example.com` usernames. + +Delegation can be performed in one of two ways: + +* **Well-known delegation**: A well-known text file is served over HTTPS on the domain name + that you want to use, pointing to your server on `matrix.example.com` port 8448; +* **DNS SRV delegation**: A DNS SRV record is created on the domain name that you want to + use, pointing to your server on `matrix.example.com` port TCP/8448. + +If you are using a reverse proxy to forward `/_matrix` to Dendrite, your well-known or DNS SRV +delegation must refer to the hostname and port that the reverse proxy is listening on instead. + +Well-known delegation is typically easier to set up and usually preferred. However, you can use +either or both methods to delegate. If you configure both methods of delegation, it is important +that they both agree and refer to the same hostname and port. + +## Well-known delegation + +Using well-known delegation requires that you are running a web server at `example.com` which +is listening on the standard HTTPS port TCP/443. + +Assuming that your Dendrite installation is listening for HTTPS connections at `matrix.example.com` +on port 8448, the delegation file must be served at `https://example.com/.well-known/matrix/server` +and contain the following JSON document: + +```json +{ + "m.server": "https://matrix.example.com:8448" +} +``` + +## DNS SRV delegation + +Using DNS SRV delegation requires creating DNS SRV records on the `example.com` zone which +refer to your Dendrite installation. + +Assuming that your Dendrite installation is listening for HTTPS connections at `matrix.example.com` +port 8448, the DNS SRV record must have the following fields: + +* Name: `@` (or whichever term your DNS provider uses to signal the root) +* Service: `_matrix` +* Protocol: `_tcp` +* Port: `8448` +* Target: `matrix.example.com` diff --git a/docs/installation/3_database.md b/docs/installation/3_database.md new file mode 100644 index 000000000..f64fe9150 --- /dev/null +++ b/docs/installation/3_database.md @@ -0,0 +1,106 @@ +--- +title: Preparing database storage +parent: Installation +nav_order: 3 +permalink: /installation/database +--- + +# Preparing database storage + +Dendrite uses SQL databases to store data. Depending on the database engine being used, you +may need to perform some manual steps outlined below. + +## SQLite + +SQLite deployments do not require manual database creation. Simply configure the database +filenames in the Dendrite configuration file and start Dendrite. The databases will be created +and populated automatically. + +Note that Dendrite **cannot share a single SQLite database across multiple components**. Each +component must be configured with its own SQLite database filename. + +### Connection strings + +Connection strings for SQLite databases take the following forms: + +* Current working directory path: `file:dendrite_component.db` +* Full specified path: `file:///path/to/dendrite_component.db` + +## PostgreSQL + +Dendrite can automatically populate the database with the relevant tables and indexes, but +it is not capable of creating the databases themselves. You will need to create the databases +manually. + +At this point, you can choose to either use a single database for all Dendrite components, +or you can run each component with its own separate database: + +* **Single database**: You will need to create a single PostgreSQL database. Monolith deployments + can use a single global connection pool, which makes updating the configuration file much easier. + Only one database connection string to manage and likely simpler to back up the database. All + components will be sharing the same database resources (CPU, RAM, storage). + +* **Separate databases**: You will need to create a separate PostgreSQL database for each + component. You will need to configure each component that has storage in the Dendrite + configuration file with its own connection parameters. Allows running a different database engine + for each component on a different machine if needs be, each with their own CPU, RAM and storage — + almost certainly overkill unless you are running a very large Dendrite deployment. + +For either configuration, you will want to: + +1. Configure a role (with a username and password) which Dendrite can use to connect to the + database; +2. Create the database(s) themselves, ensuring that the Dendrite role has privileges over them. + As Dendrite will create and manage the database tables, indexes and sequences by itself, the + Dendrite role must have suitable privileges over the database. + +### Connection strings + +The format of connection strings for PostgreSQL databases is described in the [PostgreSQL libpq manual](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). Note that Dendrite only +supports the "Connection URIs" format and **will not** work with the "Keyword/Value Connection +string" format. + +Example supported connection strings take the format: + +* `postgresql://user:pass@hostname/database?options=...` +* `postgres://user:pass@hostname/database?options=...` + +If you need to disable SSL/TLS on the database connection, you may need to append `?sslmode=disable` to the end of the connection string. + +### Role creation + +Create a role which Dendrite can use to connect to the database, choosing a new password when +prompted. On macOS, you may need to omit the `sudo -u postgres` from the below instructions. + +```bash +sudo -u postgres createuser -P dendrite +``` + +### Single database creation + +Create the database itself, using the `dendrite` role from above: + +```bash +sudo -u postgres createdb -O dendrite dendrite +``` + +### Multiple database creation + +The following eight components require a database. In this example they will be named: + +| Appservice API | `dendrite_appservice` | +| Federation API | `dendrite_federationapi` | +| Media API | `dendrite_mediaapi` | +| MSCs | `dendrite_mscs` | +| Roomserver | `dendrite_roomserver` | +| Sync API | `dendrite_syncapi` | +| Key server | `dendrite_keyserver` | +| User API | `dendrite_userapi` | + +... therefore you will need to create eight different databases: + +```bash +for i in appservice federationapi mediaapi mscs roomserver syncapi keyserver userapi; do + sudo -u postgres createdb -O dendrite dendrite_$i +done +``` diff --git a/docs/installation/4_signingkey.md b/docs/installation/4_signingkey.md new file mode 100644 index 000000000..07dc485ff --- /dev/null +++ b/docs/installation/4_signingkey.md @@ -0,0 +1,79 @@ +--- +title: Generating signing keys +parent: Installation +nav_order: 4 +permalink: /installation/signingkeys +--- + +# Generating signing keys + +All Matrix homeservers require a signing private key, which will be used to authenticate +federation requests and events. + +The `generate-keys` utility can be used to generate a private key. Assuming that Dendrite was +built using `build.sh`, you should find the `generate-keys` utility in the `bin` folder. + +To generate a Matrix signing private key: + +```bash +./bin/generate-keys --private-key matrix_key.pem +``` + +The generated `matrix_key.pem` file is your new signing key. + +## Important warning + +You must treat this key as if it is highly sensitive and private, so **never share it with +anyone**. No one should ever ask you for this key for any reason, even to debug a problematic +Dendrite server. + +Make sure take a safe backup of this key. You will likely need it if you want to reinstall +Dendrite, or any other Matrix homeserver, on the same domain name in the future. If you lose +this key, you may have trouble joining federated rooms. + +## Old signing keys + +If you already have old signing keys from a previous Matrix installation on the same domain +name, you can reuse those instead, as long as they have not been previously marked as expired — +a key that has been marked as expired in the past is unusable. + +Old keys from a previous Dendrite installation can be reused as-is without any further +configuration required. Simply use that key file in the Dendrite configuration. + +If you have server keys from an older Synapse instance, you can convert them to Dendrite's PEM +format and configure them as `old_private_keys` in your config. + +## Key format + +Dendrite stores the server signing key in the PEM format with the following structure. + +``` +-----BEGIN MATRIX PRIVATE KEY----- +Key-ID: ed25519: + + +-----END MATRIX PRIVATE KEY----- +``` + +## Converting Synapse keys + +If you have signing keys from a previous Synapse installation, you should ideally configure them +as `old_private_keys` in your Dendrite config file. Synapse stores signing keys in the following +format: + +``` +ed25519 +``` + +To convert this key to Dendrite's PEM format, use the following template. You must copy the Key ID +exactly without modifying it. **It is important to include the trailing equals sign on the Base64 +Encoded Key Data** if it is not already present in the original key, as the key data needs to be +padded to exactly 32 bytes: + +``` +-----BEGIN MATRIX PRIVATE KEY----- +Key-ID: ed25519: + += +-----END MATRIX PRIVATE KEY----- +``` diff --git a/docs/installation/5_install_monolith.md b/docs/installation/5_install_monolith.md new file mode 100644 index 000000000..7de066cf7 --- /dev/null +++ b/docs/installation/5_install_monolith.md @@ -0,0 +1,21 @@ +--- +title: Installing as a monolith +parent: Installation +has_toc: true +nav_order: 5 +permalink: /installation/install/monolith +--- + +# Installing as a monolith + +You can install the Dendrite monolith binary into `$GOPATH/bin` by using `go install`: + +```sh +go install ./cmd/dendrite-monolith-server +``` + +Alternatively, you can specify a custom path for the binary to be written to using `go build`: + +```sh +go build -o /usr/local/bin/ ./cmd/dendrite-monolith-server +``` diff --git a/docs/installation/6_install_polylith.md b/docs/installation/6_install_polylith.md new file mode 100644 index 000000000..375512f8f --- /dev/null +++ b/docs/installation/6_install_polylith.md @@ -0,0 +1,33 @@ +--- +title: Installing as a polylith +parent: Installation +has_toc: true +nav_order: 6 +permalink: /installation/install/polylith +--- + +# Installing as a polylith + +You can install the Dendrite polylith binary into `$GOPATH/bin` by using `go install`: + +```sh +go install ./cmd/dendrite-polylith-multi +``` + +Alternatively, you can specify a custom path for the binary to be written to using `go build`: + +```sh +go build -o /usr/local/bin/ ./cmd/dendrite-polylith-multi +``` + +The `dendrite-polylith-multi` binary is a "multi-personality" binary which can run as +any of the components depending on the supplied command line parameters. + +## Reverse proxy + +Polylith deployments require a reverse proxy in order to ensure that requests are +sent to the correct endpoint. You must ensure that a suitable reverse proxy is installed +and configured. + +A [sample configuration file](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/polylith-sample.conf) +is provided for [NGINX](https://www.nginx.com). diff --git a/docs/installation/7_configuration.md b/docs/installation/7_configuration.md new file mode 100644 index 000000000..e676afbe6 --- /dev/null +++ b/docs/installation/7_configuration.md @@ -0,0 +1,147 @@ +--- +title: Populate the configuration +parent: Installation +nav_order: 7 +permalink: /installation/configuration +--- + +# Populate the configuration + +The configuration file is used to configure Dendrite. Sample configuration files are +present in the top level of the Dendrite repository: + +* [`dendrite-sample.monolith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.monolith.yaml) +* [`dendrite-sample.polylith.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-sample.polylith.yaml) + +You will need to duplicate the sample, calling it `dendrite.yaml` for example, and then +tailor it to your installation. At a minimum, you will need to populate the following +sections: + +## Server name + +First of all, you will need to configure the server name of your Matrix homeserver. +This must match the domain name that you have selected whilst [configuring the domain +name delegation](domainname). + +In the `global` section, set the `server_name` to your delegated domain name: + +```yaml +global: + # ... + server_name: example.com +``` + +## Server signing keys + +Next, you should tell Dendrite where to find your [server signing keys](signingkeys). + +In the `global` section, set the `private_key` to the path to your server signing key: + +```yaml +global: + # ... + private_key: /path/to/matrix_key.pem +``` + +## JetStream configuration + +Monolith deployments can use the built-in NATS Server rather than running a standalone +server. If you are building a polylith deployment, or you want to use a standalone NATS +Server anyway, you can also configure that too. + +### Built-in NATS Server (monolith only) + +In the `global` section, under the `jetstream` key, ensure that no server addresses are +configured and set a `storage_path` to a persistent folder on the filesystem: + +```yaml +global: + # ... + jetstream: + in_memory: false + storage_path: /path/to/storage/folder + topic_prefix: Dendrite +``` + +### Standalone NATS Server (monolith and polylith) + +To use a standalone NATS Server instance, you will need to configure `addresses` field +to point to the port that your NATS Server is listening on: + +```yaml +global: + # ... + jetstream: + addresses: + - localhost:4222 + topic_prefix: Dendrite +``` + +You do not need to configure the `storage_path` when using a standalone NATS Server instance. +In the case that you are connecting to a multi-node NATS cluster, you can configure more than +one address in the `addresses` field. + +## Database connections + +Configuring database connections varies based on the [database configuration](database) +that you chose. + +### Global connection pool (monolith with a single PostgreSQL database only) + +If you are running a monolith deployment and want to use a single connection pool to a +single PostgreSQL database, then you must uncomment and configure the `database` section +within the `global` section: + +```yaml +global: + # ... + database: + connection_string: postgres://user:pass@hostname/database?sslmode=disable + max_open_conns: 100 + max_idle_conns: 5 + conn_max_lifetime: -1 +``` + +**You must then remove or comment out** the `database` sections from other areas of the +configuration file, e.g. under the `app_service_api`, `federation_api`, `key_server`, +`media_api`, `mscs`, `room_server`, `sync_api` and `user_api` blocks, otherwise these will +override the `global` database configuration. + +### Per-component connections (all other configurations) + +If you are building a polylith deployment, are using SQLite databases or separate PostgreSQL +databases per component, then you must instead configure the `database` sections under each +of the component blocks ,e.g. under the `app_service_api`, `federation_api`, `key_server`, +`media_api`, `mscs`, `room_server`, `sync_api` and `user_api` blocks. + +For example, with PostgreSQL: + +```yaml +room_server: + # ... + database: + connection_string: postgres://user:pass@hostname/dendrite_component?sslmode=disable + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 +``` + +... or with SQLite: + +```yaml +room_server: + # ... + database: + connection_string: file:roomserver.db + max_open_conns: 10 + max_idle_conns: 2 + conn_max_lifetime: -1 +``` + +## Other sections + +There are other options which may be useful so review them all. In particular, if you are +trying to federate from your Dendrite instance into public rooms then configuring the +`key_perspectives` (like `matrix.org` in the sample) can help to improve reliability +considerably by allowing your homeserver to fetch public keys for dead homeservers from +another living server. diff --git a/docs/installation/8_starting_monolith.md b/docs/installation/8_starting_monolith.md new file mode 100644 index 000000000..e0e7309d2 --- /dev/null +++ b/docs/installation/8_starting_monolith.md @@ -0,0 +1,41 @@ +--- +title: Starting the monolith +parent: Installation +has_toc: true +nav_order: 9 +permalink: /installation/start/monolith +--- + +# Starting the monolith + +Once you have completed all of the preparation and installation steps, +you can start your Dendrite monolith deployment by starting the `dendrite-monolith-server`: + +```bash +./dendrite-monolith-server -config /path/to/dendrite.yaml +``` + +If you want to change the addresses or ports that Dendrite listens on, you +can use the `-http-bind-address` and `-https-bind-address` command line arguments: + +```bash +./dendrite-monolith-server -config /path/to/dendrite.yaml \ + -http-bind-address 1.2.3.4:12345 \ + -https-bind-address 1.2.3.4:54321 +``` + +## Running under systemd + +A common deployment pattern is to run the monolith under systemd. For this, you +will need to create a service unit file. An example service unit file is available +in the [GitHub repository](https://github.com/matrix-org/dendrite/blob/main/docs/systemd/monolith-example.service). + +Once you have installed the service unit, you can notify systemd, enable and start +the service: + +```bash +systemctl daemon-reload +systemctl enable dendrite +systemctl start dendrite +journalctl -fu dendrite +``` diff --git a/docs/installation/9_starting_polylith.md b/docs/installation/9_starting_polylith.md new file mode 100644 index 000000000..228e52e85 --- /dev/null +++ b/docs/installation/9_starting_polylith.md @@ -0,0 +1,73 @@ +--- +title: Starting the polylith +parent: Installation +has_toc: true +nav_order: 9 +permalink: /installation/start/polylith +--- + +# Starting the polylith + +Once you have completed all of the preparation and installation steps, +you can start your Dendrite polylith deployment by starting the various components +using the `dendrite-polylith-multi` personalities. + +## Start the reverse proxy + +Ensure that your reverse proxy is started and is proxying the correct +endpoints to the correct components. Software such as [NGINX](https://www.nginx.com) or +[HAProxy](http://www.haproxy.org) can be used for this purpose. A [sample configuration +for NGINX](https://github.com/matrix-org/dendrite/blob/main/docs/nginx/polylith-sample.conf) +is provided. + +## Starting the components + +Each component must be started individually: + +### Client API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml clientapi +``` + +### Sync API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml syncapi +``` + +### Media API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml mediaapi +``` + +### Federation API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml federationapi +``` + +### Roomserver + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml roomserver +``` + +### Appservice API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml appservice +``` + +### User API + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml userapi +``` + +### Key server + +```bash +./dendrite-polylith-multi -config /path/to/dendrite.yaml keyserver +``` diff --git a/docs/p2p.md b/docs/other/p2p.md similarity index 71% rename from docs/p2p.md rename to docs/other/p2p.md index 4e9a50524..9f104f025 100644 --- a/docs/p2p.md +++ b/docs/other/p2p.md @@ -1,27 +1,34 @@ -## Peer-to-peer Matrix +--- +title: P2P Matrix +nav_exclude: true +--- + +# P2P Matrix 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 +### Build - The `main` branch has a WASM-only binary for dendrite: `./cmd/dendritejs`. - Build it and copy assets to riot-web. + ``` -$ ./build-dendritejs.sh -$ cp bin/main.wasm ../riot-web/src/vector/dendrite.wasm +./build-dendritejs.sh +cp bin/main.wasm ../riot-web/src/vector/dendrite.wasm ``` -#### Test +### Test To check that the Dendrite side is working well as Wasm, you can run the Wasm-specific tests: + ``` -$ ./test-dendritejs.sh +./test-dendritejs.sh ``` -### Rendezvous +## Rendezvous This is how peers discover each other and communicate. @@ -29,18 +36,18 @@ By default, Dendrite uses the Matrix-hosted websocket star relay server at TODO This is currently hard-coded in `./cmd/dendritejs/main.go` - you can also use a local one if you run your own relay: ``` -$ npm install --global libp2p-websocket-star-rendezvous -$ rendezvous --port=9090 --host=127.0.0.1 +npm install --global libp2p-websocket-star-rendezvous +rendezvous --port=9090 --host=127.0.0.1 ``` Then use `/ip4/127.0.0.1/tcp/9090/ws/p2p-websocket-star/`. -### Riot-web +## Riot-web You need to check out this repo: ``` -$ git clone git@github.com:matrix-org/go-http-js-libp2p.git +git clone git@github.com:matrix-org/go-http-js-libp2p.git ``` Make sure to `yarn install` in the repo. Then: @@ -53,26 +60,30 @@ if (!global.fs && global.require) { global.fs = require("fs"); } ``` -- Add the diff at https://github.com/vector-im/riot-web/compare/matthew/p2p?expand=1 - ignore the `package.json` stuff. + +- Add the diff at - ignore the `package.json` stuff. - Add the following symlinks: they HAVE to be symlinks as the diff in `webpack.config.js` references specific paths. + ``` -$ cd node_modules -$ ln -s ../../go-http-js-libp2p +cd node_modules +ln -s ../../go-http-js-libp2p ``` NB: If you don't run the server with `yarn start` you need to make sure your server is sending the header `Service-Worker-Allowed: /`. TODO: Make a Docker image with all of this in it and a volume mount for `dendrite.wasm`. -### Running +## Running You need a Chrome and a Firefox running to test locally as service workers don't work in incognito tabs. + - For Chrome, use `chrome://serviceworker-internals/` to unregister/see logs. - For Firefox, use `about:debugging#/runtime/this-firefox` to unregister. Use the console window to see logs. Assuming you've `yarn start`ed Riot-Web, go to `http://localhost:8080` and register with `http://localhost:8080` as your HS URL. You can: - - join rooms by room alias e.g `/join #foo:bar`. - - invite specific users to a room. - - explore the published room list. All members of the room can re-publish aliases (unlike Synapse). + +- join rooms by room alias e.g `/join #foo:bar`. +- invite specific users to a room. +- explore the published room list. All members of the room can re-publish aliases (unlike Synapse). diff --git a/docs/other/peeking.md b/docs/other/peeking.md new file mode 100644 index 000000000..c4ae89811 --- /dev/null +++ b/docs/other/peeking.md @@ -0,0 +1,33 @@ +--- +nav_exclude: true +--- + +## Peeking + +Local peeking is implemented as per [MSC2753](https://github.com/matrix-org/matrix-doc/pull/2753). + +Implementationwise, this means: + +* Users call `/peek` and `/unpeek` on the clientapi from a given device. +* The clientapi delegates these via HTTP to the roomserver, which coordinates peeking in general for a given room +* The roomserver writes an NewPeek event into the kafka log headed to the syncserver +* The syncserver tracks the existence of the local peek in the syncapi_peeks table in its DB, and then starts waking up the peeking devices for the room in question, putting it in the `peek` section of the /sync response. + +Peeking over federation is implemented as per [MSC2444](https://github.com/matrix-org/matrix-doc/pull/2444). + +For requests to peek our rooms ("inbound peeks"): + +* Remote servers call `/peek` on federationapi + * The federationapi queries the federationsender to check if this is renewing an inbound peek or not. + * If not, it hits the PerformInboundPeek on the roomserver to ask it for the current state of the room. + * The roomserver atomically (in theory) adds a NewInboundPeek to its kafka stream to tell the federationserver to start peeking. + * The federationsender receives the event, tracks the inbound peek in the federationsender_inbound_peeks table, and starts sending events to the peeking server. + * The federationsender evicts stale inbound peeks which haven't been renewed. + +For peeking into other server's rooms ("outbound peeks"): + +* The `roomserver` will kick the `federationsender` much as it does for a federated `/join` in order to trigger a federated outbound `/peek` +* The `federationsender` tracks the existence of the outbound peek in in its federationsender_outbound_peeks table. +* The `federationsender` regularly renews the remote peek as long as there are still peeking devices syncing for it. +* TBD: how do we tell if there are no devices currently syncing for a given peeked room? The syncserver needs to tell the roomserver + somehow who then needs to warn the federationsender. diff --git a/docs/peeking.md b/docs/peeking.md deleted file mode 100644 index 60f359072..000000000 --- a/docs/peeking.md +++ /dev/null @@ -1,26 +0,0 @@ -## Peeking - -Local peeking is implemented as per [MSC2753](https://github.com/matrix-org/matrix-doc/pull/2753). - -Implementationwise, this means: - * Users call `/peek` and `/unpeek` on the clientapi from a given device. - * The clientapi delegates these via HTTP to the roomserver, which coordinates peeking in general for a given room - * The roomserver writes an NewPeek event into the kafka log headed to the syncserver - * The syncserver tracks the existence of the local peek in the syncapi_peeks table in its DB, and then starts waking up the peeking devices for the room in question, putting it in the `peek` section of the /sync response. - -Peeking over federation is implemented as per [MSC2444](https://github.com/matrix-org/matrix-doc/pull/2444). - -For requests to peek our rooms ("inbound peeks"): - * Remote servers call `/peek` on federationapi - * The federationapi queries the federationsender to check if this is renewing an inbound peek or not. - * If not, it hits the PerformInboundPeek on the roomserver to ask it for the current state of the room. - * The roomserver atomically (in theory) adds a NewInboundPeek to its kafka stream to tell the federationserver to start peeking. - * The federationsender receives the event, tracks the inbound peek in the federationsender_inbound_peeks table, and starts sending events to the peeking server. - * The federationsender evicts stale inbound peeks which haven't been renewed. - -For peeking into other server's rooms ("outbound peeks"): - * The `roomserver` will kick the `federationsender` much as it does for a federated `/join` in order to trigger a federated outbound `/peek` - * The `federationsender` tracks the existence of the outbound peek in in its federationsender_outbound_peeks table. - * The `federationsender` regularly renews the remote peek as long as there are still peeking devices syncing for it. - * TBD: how do we tell if there are no devices currently syncing for a given peeked room? The syncserver needs to tell the roomserver - somehow who then needs to warn the federationsender. \ No newline at end of file diff --git a/docs/serverkeyformat.md b/docs/serverkeyformat.md deleted file mode 100644 index feda93454..000000000 --- a/docs/serverkeyformat.md +++ /dev/null @@ -1,29 +0,0 @@ -# Server Key Format - -Dendrite stores the server signing key in the PEM format with the following structure. - -``` ------BEGIN MATRIX PRIVATE KEY----- -Key-ID: ed25519: - - ------END MATRIX PRIVATE KEY----- -``` - -## Converting Synapse Keys - -If you have signing keys from a previous synapse server, you should ideally configure them as `old_private_keys` in your Dendrite config file. Synapse stores signing keys in the following format. - -``` -ed25519 -``` - -To convert this key to Dendrite's PEM format, use the following template. **It is important to include the equals sign, as the key data needs to be padded to 32 bytes.** - -``` ------BEGIN MATRIX PRIVATE KEY----- -Key-ID: ed25519: - -= ------END MATRIX PRIVATE KEY----- -``` \ No newline at end of file diff --git a/docs/sytest.md b/docs/sytest.md index 0d42013ec..3cfb99e60 100644 --- a/docs/sytest.md +++ b/docs/sytest.md @@ -1,3 +1,9 @@ +--- +title: SyTest +parent: Development +permalink: /development/sytest +--- + # SyTest Dendrite uses [SyTest](https://github.com/matrix-org/sytest) for its @@ -43,6 +49,7 @@ source code. The test results TAP file and homeserver logging output will go to add any tests to `sytest-whitelist`. When debugging, the following Docker `run` options may also be useful: + * `-v /path/to/sytest/:/sytest/`: Use your local SyTest repository at `/path/to/sytest` instead of pulling from GitHub. This is useful when you want to speed things up or make modifications to SyTest. @@ -58,6 +65,7 @@ When debugging, the following Docker `run` options may also be useful: The docker command also supports a single positional argument for the test file to run, so you can run a single `.pl` file rather than the whole test suite. For example: + ``` docker run --rm --name sytest -v "/Users/kegan/github/sytest:/sytest" -v "/Users/kegan/github/dendrite:/src" -v "/Users/kegan/logs:/logs" @@ -118,7 +126,7 @@ POSTGRES=1 ./run-tests.pl -I Dendrite::Monolith -d ../dendrite/bin -W ../dendrit where `tee` lets you see the results while they're being piped to the file, and `POSTGRES=1` enables testing with PostgeSQL. If the `POSTGRES` environment variable is not set or is set to 0, SyTest will fall back to SQLite 3. For more -flags and options, see https://github.com/matrix-org/sytest#running. +flags and options, see . Once the tests are complete, run the helper script to see if you need to add any newly passing test names to `sytest-whitelist` in the project's root diff --git a/docs/tracing/jaeger.png b/docs/tracing/jaeger.png deleted file mode 100644 index 8b1e61feb..000000000 Binary files a/docs/tracing/jaeger.png and /dev/null differ diff --git a/docs/tracing/opentracing.md b/docs/tracing/opentracing.md index a2110bc0e..8528c2ba3 100644 --- a/docs/tracing/opentracing.md +++ b/docs/tracing/opentracing.md @@ -1,5 +1,11 @@ -Opentracing -=========== +--- +title: OpenTracing +has_children: true +parent: Development +permalink: /development/opentracing +--- + +# OpenTracing Dendrite extensively uses the [opentracing.io](http://opentracing.io) framework to trace work across the different logical components. @@ -23,7 +29,6 @@ This is useful to see where the time is being spent processing a request on a component. However, opentracing allows tracking of spans across components. This makes it possible to see exactly what work goes into processing a request: - ``` Component 1 |<─────────────────── HTTP ────────────────────>| |<──────────────── RPC ─────────────────>| @@ -39,7 +44,6 @@ deserialized span as the parent). A collection of spans that are related is called a trace. - Spans are passed through the code via contexts, rather than manually. It is therefore important that all spans that are created are immediately added to the current context. Thankfully the opentracing library gives helper functions for @@ -53,11 +57,11 @@ defer span.Finish() This will create a new span, adding any span already in `ctx` as a parent to the new span. - Adding Information ------------------ Opentracing allows adding information to a trace via three mechanisms: + - "tags" ─ A span can be tagged with a key/value pair. This is typically information that relates to the span, e.g. for spans created for incoming HTTP requests could include the request path and response codes as tags, spans for @@ -69,12 +73,10 @@ Opentracing allows adding information to a trace via three mechanisms: inspecting the traces, but can be used to add context to logs or tags in child spans. - See [specification.md](https://github.com/opentracing/specification/blob/master/specification.md) for some of the common tags and log fields used. - Span Relationships ------------------ @@ -86,7 +88,6 @@ A second relation type is `followsFrom`, where the parent has no dependence on the child span. This usually indicates some sort of fire and forget behaviour, e.g. adding a message to a pipeline or inserting into a kafka topic. - Jaeger ------ @@ -99,6 +100,7 @@ giving a UI for viewing and interacting with traces. To enable jaeger a `Tracer` object must be instansiated from the config (as well as having a jaeger server running somewhere, usually locally). A `Tracer` does several things: + - Decides which traces to save and send to the server. There are multiple schemes for doing this, with a simple example being to save a certain fraction of traces. diff --git a/docs/tracing/setup.md b/docs/tracing/setup.md index 2cab4d1ef..06f89bf85 100644 --- a/docs/tracing/setup.md +++ b/docs/tracing/setup.md @@ -1,14 +1,20 @@ -## OpenTracing Setup +--- +title: Setup +parent: OpenTracing +grand_parent: Development +permalink: /development/opentracing/setup +--- -![Trace when sending an event into a room](/docs/tracing/jaeger.png) +# OpenTracing Setup Dendrite uses [Jaeger](https://www.jaegertracing.io/) for tracing between microservices. Tracing shows the nesting of logical spans which provides visibility on how the microservices interact. This document explains how to set up Jaeger locally on a single machine. -### Set up the Jaeger backend +## Set up the Jaeger backend The [easiest way](https://www.jaegertracing.io/docs/1.18/getting-started/) is to use the all-in-one Docker image: + ``` $ docker run -d --name jaeger \ -e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \ @@ -23,9 +29,10 @@ $ docker run -d --name jaeger \ jaegertracing/all-in-one:1.18 ``` -### Configuring Dendrite to talk to Jaeger +## Configuring Dendrite to talk to Jaeger Modify your config to look like: (this will send every single span to Jaeger which will be slow on large instances, but for local testing it's fine) + ``` tracing: enabled: true @@ -40,10 +47,11 @@ tracing: ``` then run the monolith server with `--api true` to use polylith components which do tracing spans: + ``` -$ ./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml --api true +./dendrite-monolith-server --tls-cert server.crt --tls-key server.key --config dendrite.yaml --api true ``` -### Checking traces +## Checking traces -Visit http://localhost:16686 to see traces under `DendriteMonolith`. +Visit to see traces under `DendriteMonolith`. diff --git a/federationapi/api/api.go b/federationapi/api/api.go index 4d6b0211c..53d4701f3 100644 --- a/federationapi/api/api.go +++ b/federationapi/api/api.go @@ -10,20 +10,96 @@ import ( "github.com/matrix-org/gomatrixserverlib" ) -// FederationClient is a subset of gomatrixserverlib.FederationClient functions which the fedsender -// implements as proxy calls, with built-in backoff/retries/etc. Errors returned from functions in -// this interface are of type FederationClientError -type FederationClient interface { +// FederationInternalAPI is used to query information from the federation sender. +type FederationInternalAPI interface { + gomatrixserverlib.FederatedStateClient + KeyserverFederationAPI + gomatrixserverlib.KeyDatabase + ClientFederationAPI + RoomserverFederationAPI + + QueryServerKeys(ctx context.Context, request *QueryServerKeysRequest, response *QueryServerKeysResponse) error + LookupServerKeys(ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp) ([]gomatrixserverlib.ServerKeys, 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, suggestedOnly bool) (res gomatrixserverlib.MSC2946SpacesResponse, err error) + + // Broadcasts an EDU to all servers in rooms we are joined to. Used in the yggdrasil demos. + PerformBroadcastEDU( + ctx context.Context, + request *PerformBroadcastEDURequest, + response *PerformBroadcastEDUResponse, + ) error +} + +type ClientFederationAPI interface { + // Query the server names of the joined hosts in a room. + // Unlike QueryJoinedHostsInRoom, this function returns a de-duplicated slice + // containing only the server names (without information for membership events). + // The response will include this server if they are joined to the room. + QueryJoinedHostServerNamesInRoom(ctx context.Context, request *QueryJoinedHostServerNamesInRoomRequest, response *QueryJoinedHostServerNamesInRoomResponse) error +} + +type RoomserverFederationAPI interface { gomatrixserverlib.BackfillClient gomatrixserverlib.FederatedStateClient + KeyRing() *gomatrixserverlib.KeyRing + + // PerformDirectoryLookup looks up a remote room ID from a room alias. + PerformDirectoryLookup(ctx context.Context, request *PerformDirectoryLookupRequest, response *PerformDirectoryLookupResponse) error + // Handle an instruction to make_join & send_join with a remote server. + PerformJoin(ctx context.Context, request *PerformJoinRequest, response *PerformJoinResponse) + // Handle an instruction to make_leave & send_leave with a remote server. + PerformLeave(ctx context.Context, request *PerformLeaveRequest, response *PerformLeaveResponse) error + // Handle sending an invite to a remote server. + PerformInvite(ctx context.Context, request *PerformInviteRequest, response *PerformInviteResponse) error + // Handle an instruction to peek a room on a remote server. + PerformOutboundPeek(ctx context.Context, request *PerformOutboundPeekRequest, response *PerformOutboundPeekResponse) error + // Query the server names of the joined hosts in a room. + // Unlike QueryJoinedHostsInRoom, this function returns a de-duplicated slice + // containing only the server names (without information for membership events). + // The response will include this server if they are joined to the room. + QueryJoinedHostServerNamesInRoom(ctx context.Context, request *QueryJoinedHostServerNamesInRoomRequest, response *QueryJoinedHostServerNamesInRoomResponse) error + GetEventAuth(ctx context.Context, s gomatrixserverlib.ServerName, roomVersion gomatrixserverlib.RoomVersion, roomID, eventID string) (res gomatrixserverlib.RespEventAuth, err error) + GetEvent(ctx context.Context, s gomatrixserverlib.ServerName, eventID string) (res gomatrixserverlib.Transaction, err error) + LookupMissingEvents(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, missing gomatrixserverlib.MissingEvents, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMissingEvents, err error) +} + +// KeyserverFederationAPI is a subset of gomatrixserverlib.FederationClient functions which the keyserver +// implements as proxy calls, with built-in backoff/retries/etc. Errors returned from functions in +// this interface are of type FederationClientError +type KeyserverFederationAPI interface { GetUserDevices(ctx context.Context, s gomatrixserverlib.ServerName, userID string) (res gomatrixserverlib.RespUserDevices, 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) +} + +// an interface for gmsl.FederationClient - contains functions called by federationapi only. +type FederationClient interface { + gomatrixserverlib.KeyClient + SendTransaction(ctx context.Context, t gomatrixserverlib.Transaction) (res gomatrixserverlib.RespSend, err error) + + // Perform operations + LookupRoomAlias(ctx context.Context, s gomatrixserverlib.ServerName, roomAlias string) (res gomatrixserverlib.RespDirectory, err error) + Peek(ctx context.Context, s gomatrixserverlib.ServerName, roomID, peekID string, roomVersions []gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespPeek, err error) + MakeJoin(ctx context.Context, s gomatrixserverlib.ServerName, roomID, userID string, roomVersions []gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMakeJoin, err error) + SendJoin(ctx context.Context, s gomatrixserverlib.ServerName, event *gomatrixserverlib.Event) (res gomatrixserverlib.RespSendJoin, err error) + MakeLeave(ctx context.Context, s gomatrixserverlib.ServerName, roomID, userID string) (res gomatrixserverlib.RespMakeLeave, err error) + SendLeave(ctx context.Context, s gomatrixserverlib.ServerName, event *gomatrixserverlib.Event) (err error) + SendInviteV2(ctx context.Context, s gomatrixserverlib.ServerName, request gomatrixserverlib.InviteV2Request) (res gomatrixserverlib.RespInviteV2, err error) + GetEvent(ctx context.Context, s gomatrixserverlib.ServerName, eventID string) (res gomatrixserverlib.Transaction, err error) + + GetEventAuth(ctx context.Context, s gomatrixserverlib.ServerName, roomVersion gomatrixserverlib.RoomVersion, roomID, eventID string) (res gomatrixserverlib.RespEventAuth, err error) + GetUserDevices(ctx context.Context, s gomatrixserverlib.ServerName, userID string) (gomatrixserverlib.RespUserDevices, error) + ClaimKeys(ctx context.Context, s gomatrixserverlib.ServerName, oneTimeKeys map[string]map[string]string) (gomatrixserverlib.RespClaimKeys, error) + QueryKeys(ctx context.Context, s gomatrixserverlib.ServerName, keys map[string][]string) (gomatrixserverlib.RespQueryKeys, error) + Backfill(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, limit int, eventIDs []string) (res gomatrixserverlib.Transaction, 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, suggestedOnly bool) (res gomatrixserverlib.MSC2946SpacesResponse, err error) - LookupServerKeys(ctx context.Context, s gomatrixserverlib.ServerName, keyRequests map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp) ([]gomatrixserverlib.ServerKeys, error) - GetEventAuth(ctx context.Context, s gomatrixserverlib.ServerName, roomVersion gomatrixserverlib.RoomVersion, roomID, eventID string) (res gomatrixserverlib.RespEventAuth, err error) + + ExchangeThirdPartyInvite(ctx context.Context, s gomatrixserverlib.ServerName, builder gomatrixserverlib.EventBuilder) (err error) + LookupState(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, eventID string, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespState, err error) + LookupStateIDs(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, eventID string) (res gomatrixserverlib.RespStateIDs, err error) LookupMissingEvents(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, missing gomatrixserverlib.MissingEvents, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMissingEvents, err error) } @@ -38,68 +114,6 @@ func (e *FederationClientError) Error() string { return fmt.Sprintf("%s - (retry_after=%s, blacklisted=%v)", e.Err, e.RetryAfter.String(), e.Blacklisted) } -// FederationInternalAPI is used to query information from the federation sender. -type FederationInternalAPI interface { - FederationClient - gomatrixserverlib.KeyDatabase - - KeyRing() *gomatrixserverlib.KeyRing - - QueryServerKeys(ctx context.Context, request *QueryServerKeysRequest, response *QueryServerKeysResponse) error - - // PerformDirectoryLookup looks up a remote room ID from a room alias. - PerformDirectoryLookup( - ctx context.Context, - request *PerformDirectoryLookupRequest, - response *PerformDirectoryLookupResponse, - ) error - // Query the server names of the joined hosts in a room. - // Unlike QueryJoinedHostsInRoom, this function returns a de-duplicated slice - // containing only the server names (without information for membership events). - // The response will include this server if they are joined to the room. - QueryJoinedHostServerNamesInRoom( - ctx context.Context, - request *QueryJoinedHostServerNamesInRoomRequest, - response *QueryJoinedHostServerNamesInRoomResponse, - ) error - // Handle an instruction to make_join & send_join with a remote server. - PerformJoin( - ctx context.Context, - request *PerformJoinRequest, - response *PerformJoinResponse, - ) - // Handle an instruction to peek a room on a remote server. - PerformOutboundPeek( - ctx context.Context, - request *PerformOutboundPeekRequest, - response *PerformOutboundPeekResponse, - ) error - // Handle an instruction to make_leave & send_leave with a remote server. - PerformLeave( - ctx context.Context, - request *PerformLeaveRequest, - response *PerformLeaveResponse, - ) error - // Handle sending an invite to a remote server. - PerformInvite( - ctx context.Context, - request *PerformInviteRequest, - response *PerformInviteResponse, - ) error - // Notifies the federation sender that these servers may be online and to retry sending messages. - PerformServersAlive( - ctx context.Context, - request *PerformServersAliveRequest, - response *PerformServersAliveResponse, - ) error - // Broadcasts an EDU to all servers in rooms we are joined to. - PerformBroadcastEDU( - ctx context.Context, - request *PerformBroadcastEDURequest, - response *PerformBroadcastEDUResponse, - ) error -} - type QueryServerKeysRequest struct { ServerName gomatrixserverlib.ServerName KeyIDToCriteria map[gomatrixserverlib.KeyID]gomatrixserverlib.PublicKeyNotaryQueryCriteria @@ -179,13 +193,6 @@ type PerformInviteResponse struct { Event *gomatrixserverlib.HeaderedEvent `json:"event"` } -type PerformServersAliveRequest struct { - Servers []gomatrixserverlib.ServerName -} - -type PerformServersAliveResponse struct { -} - // QueryJoinedHostServerNamesInRoomRequest is a request to QueryJoinedHostServerNames type QueryJoinedHostServerNamesInRoomRequest struct { RoomID string `json:"room_id"` diff --git a/federationapi/consumers/keychange.go b/federationapi/consumers/keychange.go index 0ece18e97..6d3cf0e46 100644 --- a/federationapi/consumers/keychange.go +++ b/federationapi/consumers/keychange.go @@ -39,7 +39,7 @@ type KeyChangeConsumer struct { db storage.Database queues *queue.OutgoingQueues serverName gomatrixserverlib.ServerName - rsAPI roomserverAPI.RoomserverInternalAPI + rsAPI roomserverAPI.FederationRoomserverAPI topic string } @@ -50,7 +50,7 @@ func NewKeyChangeConsumer( js nats.JetStreamContext, queues *queue.OutgoingQueues, store storage.Database, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.FederationRoomserverAPI, ) *KeyChangeConsumer { return &KeyChangeConsumer{ ctx: process.Context(), @@ -120,6 +120,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool { logger.WithError(err).Error("failed to calculate joined rooms for user") return true } + // send this key change to all servers who share rooms with this user. destinations, err := t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, true) if err != nil { diff --git a/federationapi/consumers/roomserver.go b/federationapi/consumers/roomserver.go index ff2c8e5d4..e50ec66ad 100644 --- a/federationapi/consumers/roomserver.go +++ b/federationapi/consumers/roomserver.go @@ -36,7 +36,7 @@ import ( type OutputRoomEventConsumer struct { ctx context.Context cfg *config.FederationAPI - rsAPI api.RoomserverInternalAPI + rsAPI api.FederationRoomserverAPI jetstream nats.JetStreamContext durable string db storage.Database @@ -51,7 +51,7 @@ func NewOutputRoomEventConsumer( js nats.JetStreamContext, queues *queue.OutgoingQueues, store storage.Database, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, ) *OutputRoomEventConsumer { return &OutputRoomEventConsumer{ ctx: process.Context(), @@ -89,15 +89,7 @@ func (s *OutputRoomEventConsumer) onMessage(ctx context.Context, msg *nats.Msg) switch output.Type { case api.OutputTypeNewRoomEvent: ev := output.NewRoomEvent.Event - - if output.NewRoomEvent.RewritesState { - if err := s.db.PurgeRoomState(s.ctx, ev.RoomID()); err != nil { - log.WithError(err).Errorf("roomserver output log: purge room state failure") - return false - } - } - - if err := s.processMessage(*output.NewRoomEvent); err != nil { + if err := s.processMessage(*output.NewRoomEvent, output.NewRoomEvent.RewritesState); err != nil { // panic rather than continue with an inconsistent database log.WithFields(log.Fields{ "event_id": ev.EventID(), @@ -145,29 +137,26 @@ func (s *OutputRoomEventConsumer) processInboundPeek(orp api.OutputNewInboundPee // processMessage updates the list of currently joined hosts in the room // and then sends the event to the hosts that were joined before the event. -func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) error { - eventsRes := &api.QueryEventsByIDResponse{} - if len(ore.AddsStateEventIDs) > 0 { +func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent, rewritesState bool) error { + addsStateEvents, missingEventIDs := ore.NeededStateEventIDs() + + // Ask the roomserver and add in the rest of the results into the set. + // Finally, work out if there are any more events missing. + if len(missingEventIDs) > 0 { eventsReq := &api.QueryEventsByIDRequest{ - EventIDs: ore.AddsStateEventIDs, + EventIDs: missingEventIDs, } + eventsRes := &api.QueryEventsByIDResponse{} if err := s.rsAPI.QueryEventsByID(s.ctx, eventsReq, eventsRes); err != nil { return fmt.Errorf("s.rsAPI.QueryEventsByID: %w", err) } - - found := false - for _, event := range eventsRes.Events { - if event.EventID() == ore.Event.EventID() { - found = true - break - } - } - if !found { - eventsRes.Events = append(eventsRes.Events, ore.Event) + if len(eventsRes.Events) != len(missingEventIDs) { + return fmt.Errorf("missing state events") } + addsStateEvents = append(addsStateEvents, eventsRes.Events...) } - addsJoinedHosts, err := joinedHostsFromEvents(gomatrixserverlib.UnwrapEventHeaders(eventsRes.Events)) + addsJoinedHosts, err := JoinedHostsFromEvents(gomatrixserverlib.UnwrapEventHeaders(addsStateEvents)) if err != nil { return err } @@ -179,10 +168,9 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) err oldJoinedHosts, err := s.db.UpdateRoom( s.ctx, ore.Event.RoomID(), - ore.LastSentEventID, - ore.Event.EventID(), addsJoinedHosts, ore.RemovesStateEventIDs, + rewritesState, // if we're re-writing state, nuke all joined hosts before adding ) if err != nil { return err @@ -241,7 +229,7 @@ func (s *OutputRoomEventConsumer) joinedHostsAtEvent( return nil, err } - combinedAddsJoinedHosts, err := joinedHostsFromEvents(combinedAddsEvents) + combinedAddsJoinedHosts, err := JoinedHostsFromEvents(combinedAddsEvents) if err != nil { return nil, err } @@ -287,10 +275,10 @@ func (s *OutputRoomEventConsumer) joinedHostsAtEvent( return result, nil } -// joinedHostsFromEvents turns a list of state events into a list of joined hosts. +// JoinedHostsFromEvents turns a list of state events into a list of joined hosts. // This errors if one of the events was invalid. // It should be impossible for an invalid event to get this far in the pipeline. -func joinedHostsFromEvents(evs []*gomatrixserverlib.Event) ([]types.JoinedHost, error) { +func JoinedHostsFromEvents(evs []*gomatrixserverlib.Event) ([]types.JoinedHost, error) { var joinedHosts []types.JoinedHost for _, ev := range evs { if ev.Type() != "m.room.member" || ev.StateKey() == nil { diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index 5bfe237a8..ff159beea 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -29,9 +29,7 @@ import ( keyserverAPI "github.com/matrix-org/dendrite/keyserver/api" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/base" - "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/jetstream" - "github.com/matrix-org/dendrite/setup/process" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/sirupsen/logrus" @@ -47,20 +45,18 @@ func AddInternalRoutes(router *mux.Router, intAPI api.FederationInternalAPI) { // AddPublicRoutes sets up and registers HTTP handlers on the base API muxes for the FederationAPI component. func AddPublicRoutes( - process *process.ProcessContext, - fedRouter, keyRouter, wellKnownRouter *mux.Router, - cfg *config.FederationAPI, + base *base.BaseDendrite, userAPI userapi.UserInternalAPI, federation *gomatrixserverlib.FederationClient, keyRing gomatrixserverlib.JSONVerifier, - rsAPI roomserverAPI.RoomserverInternalAPI, - federationAPI federationAPI.FederationInternalAPI, - keyAPI keyserverAPI.KeyInternalAPI, - mscCfg *config.MSCs, + rsAPI roomserverAPI.FederationRoomserverAPI, + fedAPI federationAPI.FederationInternalAPI, + keyAPI keyserverAPI.FederationKeyAPI, servers federationAPI.ServersInRoomProvider, ) { - - js, _ := jetstream.Prepare(process, &cfg.Matrix.JetStream) + cfg := &base.Cfg.FederationAPI + mscCfg := &base.Cfg.MSCs + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) producer := &producers.SyncAPIProducer{ JetStream: js, TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent), @@ -71,9 +67,23 @@ func AddPublicRoutes( UserAPI: userAPI, } + // the federationapi component is a bit unique in that it attaches public routes AND serves + // internal APIs (because it used to be 2 components: the 2nd being fedsender). As a result, + // the constructor shape is a bit wonky in that it is not valid to AddPublicRoutes without a + // concrete impl of FederationInternalAPI as the public routes and the internal API _should_ + // be the same thing now. + f, ok := fedAPI.(*internal.FederationInternalAPI) + if !ok { + panic("federationapi.AddPublicRoutes called with a FederationInternalAPI impl which was not " + + "FederationInternalAPI. This is a programming error.") + } + routing.Setup( - fedRouter, keyRouter, wellKnownRouter, cfg, rsAPI, - federationAPI, keyRing, + base.PublicFederationAPIMux, + base.PublicKeyAPIMux, + base.PublicWellKnownAPIMux, + cfg, + rsAPI, f, keyRing, federation, userAPI, keyAPI, mscCfg, servers, producer, ) @@ -83,15 +93,15 @@ func AddPublicRoutes( // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( base *base.BaseDendrite, - federation *gomatrixserverlib.FederationClient, - rsAPI roomserverAPI.RoomserverInternalAPI, + federation api.FederationClient, + rsAPI roomserverAPI.FederationRoomserverAPI, caches *caching.Caches, keyRing *gomatrixserverlib.KeyRing, resetBlacklist bool, ) api.FederationInternalAPI { cfg := &base.Cfg.FederationAPI - federationDB, err := storage.NewDatabase(&cfg.Database, base.Caches, base.Cfg.Global.ServerName) + federationDB, err := storage.NewDatabase(base, &cfg.Database, base.Caches, base.Cfg.Global.ServerName) if err != nil { logrus.WithError(err).Panic("failed to connect to federation sender db") } @@ -105,7 +115,7 @@ func NewInternalAPI( FailuresUntilBlacklist: cfg.FederationMaxRetries, } - js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) queues := queue.NewOutgoingQueues( federationDB, base.ProcessContext, diff --git a/federationapi/federationapi_keys_test.go b/federationapi/federationapi_keys_test.go index 4774c8820..31e9a4c73 100644 --- a/federationapi/federationapi_keys_test.go +++ b/federationapi/federationapi_keys_test.go @@ -102,7 +102,7 @@ func TestMain(m *testing.M) { ) // Finally, build the server key APIs. - sbase := base.NewBaseDendrite(cfg, "Monolith", base.NoCacheMetrics) + sbase := base.NewBaseDendrite(cfg, "Monolith", base.DisableMetrics) s.api = NewInternalAPI(sbase, s.fedclient, nil, s.cache, nil, true) } diff --git a/federationapi/federationapi_test.go b/federationapi/federationapi_test.go index 833359c11..ae244c566 100644 --- a/federationapi/federationapi_test.go +++ b/federationapi/federationapi_test.go @@ -3,17 +3,250 @@ package federationapi_test import ( "context" "crypto/ed25519" + "encoding/json" + "fmt" "strings" "testing" + "time" "github.com/matrix-org/dendrite/federationapi" - "github.com/matrix-org/dendrite/internal/test" + "github.com/matrix-org/dendrite/federationapi/api" + "github.com/matrix-org/dendrite/federationapi/internal" + keyapi "github.com/matrix-org/dendrite/keyserver/api" + rsapi "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/setup/jetstream" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/dendrite/test/testrig" "github.com/matrix-org/gomatrix" "github.com/matrix-org/gomatrixserverlib" + "github.com/nats-io/nats.go" ) +type fedRoomserverAPI struct { + rsapi.FederationRoomserverAPI + inputRoomEvents func(ctx context.Context, req *rsapi.InputRoomEventsRequest, res *rsapi.InputRoomEventsResponse) + queryRoomsForUser func(ctx context.Context, req *rsapi.QueryRoomsForUserRequest, res *rsapi.QueryRoomsForUserResponse) error +} + +// PerformJoin will call this function +func (f *fedRoomserverAPI) InputRoomEvents(ctx context.Context, req *rsapi.InputRoomEventsRequest, res *rsapi.InputRoomEventsResponse) { + if f.inputRoomEvents == nil { + return + } + f.inputRoomEvents(ctx, req, res) +} + +// keychange consumer calls this +func (f *fedRoomserverAPI) QueryRoomsForUser(ctx context.Context, req *rsapi.QueryRoomsForUserRequest, res *rsapi.QueryRoomsForUserResponse) error { + if f.queryRoomsForUser == nil { + return nil + } + return f.queryRoomsForUser(ctx, req, res) +} + +// TODO: This struct isn't generic, only works for TestFederationAPIJoinThenKeyUpdate +type fedClient struct { + api.FederationClient + allowJoins []*test.Room + keys map[gomatrixserverlib.ServerName]struct { + key ed25519.PrivateKey + keyID gomatrixserverlib.KeyID + } + t *testing.T + sentTxn bool +} + +func (f *fedClient) GetServerKeys(ctx context.Context, matrixServer gomatrixserverlib.ServerName) (gomatrixserverlib.ServerKeys, error) { + fmt.Println("GetServerKeys:", matrixServer) + var keys gomatrixserverlib.ServerKeys + var keyID gomatrixserverlib.KeyID + var pkey ed25519.PrivateKey + for srv, data := range f.keys { + if srv == matrixServer { + pkey = data.key + keyID = data.keyID + break + } + } + if pkey == nil { + return keys, nil + } + + keys.ServerName = matrixServer + keys.ValidUntilTS = gomatrixserverlib.AsTimestamp(time.Now().Add(10 * time.Hour)) + publicKey := pkey.Public().(ed25519.PublicKey) + keys.VerifyKeys = map[gomatrixserverlib.KeyID]gomatrixserverlib.VerifyKey{ + keyID: { + Key: gomatrixserverlib.Base64Bytes(publicKey), + }, + } + toSign, err := json.Marshal(keys.ServerKeyFields) + if err != nil { + return keys, err + } + + keys.Raw, err = gomatrixserverlib.SignJSON( + string(matrixServer), keyID, pkey, toSign, + ) + if err != nil { + return keys, err + } + + return keys, nil +} + +func (f *fedClient) MakeJoin(ctx context.Context, s gomatrixserverlib.ServerName, roomID, userID string, roomVersions []gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMakeJoin, err error) { + for _, r := range f.allowJoins { + if r.ID == roomID { + res.RoomVersion = r.Version + res.JoinEvent = gomatrixserverlib.EventBuilder{ + Sender: userID, + RoomID: roomID, + Type: "m.room.member", + StateKey: &userID, + Content: gomatrixserverlib.RawJSON([]byte(`{"membership":"join"}`)), + PrevEvents: r.ForwardExtremities(), + } + var needed gomatrixserverlib.StateNeeded + needed, err = gomatrixserverlib.StateNeededForEventBuilder(&res.JoinEvent) + if err != nil { + f.t.Errorf("StateNeededForEventBuilder: %v", err) + return + } + res.JoinEvent.AuthEvents = r.MustGetAuthEventRefsForEvent(f.t, needed) + return + } + } + return +} +func (f *fedClient) SendJoin(ctx context.Context, s gomatrixserverlib.ServerName, event *gomatrixserverlib.Event) (res gomatrixserverlib.RespSendJoin, err error) { + for _, r := range f.allowJoins { + if r.ID == event.RoomID() { + r.InsertEvent(f.t, event.Headered(r.Version)) + f.t.Logf("Join event: %v", event.EventID()) + res.StateEvents = gomatrixserverlib.NewEventJSONsFromHeaderedEvents(r.CurrentState()) + res.AuthEvents = gomatrixserverlib.NewEventJSONsFromHeaderedEvents(r.Events()) + } + } + return +} + +func (f *fedClient) SendTransaction(ctx context.Context, t gomatrixserverlib.Transaction) (res gomatrixserverlib.RespSend, err error) { + for _, edu := range t.EDUs { + if edu.Type == gomatrixserverlib.MDeviceListUpdate { + f.sentTxn = true + } + } + f.t.Logf("got /send") + return +} + +// Regression test to make sure that /send_join is updating the destination hosts synchronously and +// isn't relying on the roomserver. +func TestFederationAPIJoinThenKeyUpdate(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + testFederationAPIJoinThenKeyUpdate(t, dbType) + }) +} + +func testFederationAPIJoinThenKeyUpdate(t *testing.T, dbType test.DBType) { + base, close := testrig.CreateBaseDendrite(t, dbType) + base.Cfg.FederationAPI.PreferDirectFetch = true + defer close() + jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) + + serverA := gomatrixserverlib.ServerName("server.a") + serverAKeyID := gomatrixserverlib.KeyID("ed25519:servera") + serverAPrivKey := test.PrivateKeyA + creator := test.NewUser(t, test.WithSigningServer(serverA, serverAKeyID, serverAPrivKey)) + + myServer := base.Cfg.Global.ServerName + myServerKeyID := base.Cfg.Global.KeyID + myServerPrivKey := base.Cfg.Global.PrivateKey + joiningUser := test.NewUser(t, test.WithSigningServer(myServer, myServerKeyID, myServerPrivKey)) + fmt.Printf("creator: %v joining user: %v\n", creator.ID, joiningUser.ID) + room := test.NewRoom(t, creator) + + rsapi := &fedRoomserverAPI{ + inputRoomEvents: func(ctx context.Context, req *rsapi.InputRoomEventsRequest, res *rsapi.InputRoomEventsResponse) { + if req.Asynchronous { + t.Errorf("InputRoomEvents from PerformJoin MUST be synchronous") + } + }, + queryRoomsForUser: func(ctx context.Context, req *rsapi.QueryRoomsForUserRequest, res *rsapi.QueryRoomsForUserResponse) error { + if req.UserID == joiningUser.ID && req.WantMembership == "join" { + res.RoomIDs = []string{room.ID} + return nil + } + return fmt.Errorf("unexpected queryRoomsForUser: %+v", *req) + }, + } + fc := &fedClient{ + allowJoins: []*test.Room{room}, + t: t, + keys: map[gomatrixserverlib.ServerName]struct { + key ed25519.PrivateKey + keyID gomatrixserverlib.KeyID + }{ + serverA: { + key: serverAPrivKey, + keyID: serverAKeyID, + }, + myServer: { + key: myServerPrivKey, + keyID: myServerKeyID, + }, + }, + } + fsapi := federationapi.NewInternalAPI(base, fc, rsapi, base.Caches, nil, false) + + var resp api.PerformJoinResponse + fsapi.PerformJoin(context.Background(), &api.PerformJoinRequest{ + RoomID: room.ID, + UserID: joiningUser.ID, + ServerNames: []gomatrixserverlib.ServerName{serverA}, + }, &resp) + if resp.JoinedVia != serverA { + t.Errorf("PerformJoin: joined via %v want %v", resp.JoinedVia, serverA) + } + if resp.LastError != nil { + t.Fatalf("PerformJoin: returned error: %+v", *resp.LastError) + } + + // Inject a keyserver key change event and ensure we try to send it out. If we don't, then the + // federationapi is incorrectly waiting for an output room event to arrive to update the joined + // hosts table. + key := keyapi.DeviceMessage{ + Type: keyapi.TypeDeviceKeyUpdate, + DeviceKeys: &keyapi.DeviceKeys{ + UserID: joiningUser.ID, + DeviceID: "MY_DEVICE", + DisplayName: "BLARGLE", + KeyJSON: []byte(`{}`), + }, + } + b, err := json.Marshal(key) + if err != nil { + t.Fatalf("Failed to marshal device message: %s", err) + } + + msg := &nats.Msg{ + Subject: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), + Header: nats.Header{}, + Data: b, + } + msg.Header.Set(jetstream.UserID, key.UserID) + + testrig.MustPublishMsgs(t, jsctx, msg) + time.Sleep(500 * time.Millisecond) + if !fc.sentTxn { + t.Fatalf("did not send device list update") + } +} + // Tests that event IDs with '/' in them (escaped as %2F) are correctly passed to the right handler and don't 404. // Relevant for v3 rooms and a cause of flakey sytests as the IDs are randomly generated. func TestRoomsV3URLEscapeDoNot404(t *testing.T) { @@ -27,10 +260,9 @@ func TestRoomsV3URLEscapeDoNot404(t *testing.T) { cfg.FederationAPI.Database.ConnectionString = config.DataSource("file::memory:") base := base.NewBaseDendrite(cfg, "Monolith") keyRing := &test.NopJSONVerifier{} - fsAPI := base.FederationAPIHTTPClient() // 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. - federationapi.AddPublicRoutes(base.ProcessContext, base.PublicFederationAPIMux, base.PublicKeyAPIMux, base.PublicWellKnownAPIMux, &cfg.FederationAPI, nil, nil, keyRing, nil, fsAPI, nil, &cfg.MSCs, nil) + federationapi.AddPublicRoutes(base, nil, nil, keyRing, nil, &internal.FederationInternalAPI{}, nil, nil) baseURL, cancel := test.ListenAndServe(t, base.PublicFederationAPIMux, true) defer cancel() serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://")) @@ -86,7 +318,7 @@ func TestRoomsV3URLEscapeDoNot404(t *testing.T) { } gerr, ok := err.(gomatrix.HTTPError) if !ok { - t.Errorf("failed to cast response error as gomatrix.HTTPError") + t.Errorf("failed to cast response error as gomatrix.HTTPError: %s", err) continue } t.Logf("Error: %+v", gerr) diff --git a/federationapi/internal/api.go b/federationapi/internal/api.go index 4e9fa8410..14056eafc 100644 --- a/federationapi/internal/api.go +++ b/federationapi/internal/api.go @@ -25,8 +25,8 @@ type FederationInternalAPI struct { db storage.Database cfg *config.FederationAPI statistics *statistics.Statistics - rsAPI roomserverAPI.RoomserverInternalAPI - federation *gomatrixserverlib.FederationClient + rsAPI roomserverAPI.FederationRoomserverAPI + federation api.FederationClient keyRing *gomatrixserverlib.KeyRing queues *queue.OutgoingQueues joins sync.Map // joins currently in progress @@ -34,8 +34,8 @@ type FederationInternalAPI struct { func NewFederationInternalAPI( db storage.Database, cfg *config.FederationAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - federation *gomatrixserverlib.FederationClient, + rsAPI roomserverAPI.FederationRoomserverAPI, + federation api.FederationClient, statistics *statistics.Statistics, caches *caching.Caches, queues *queue.OutgoingQueues, diff --git a/federationapi/internal/perform.go b/federationapi/internal/perform.go index 8cd944346..7ccd68ef0 100644 --- a/federationapi/internal/perform.go +++ b/federationapi/internal/perform.go @@ -8,6 +8,7 @@ import ( "time" "github.com/matrix-org/dendrite/federationapi/api" + "github.com/matrix-org/dendrite/federationapi/consumers" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/version" "github.com/matrix-org/gomatrix" @@ -75,7 +76,7 @@ func (r *FederationInternalAPI) PerformJoin( seenSet := make(map[gomatrixserverlib.ServerName]bool) var uniqueList []gomatrixserverlib.ServerName for _, srv := range request.ServerNames { - if seenSet[srv] { + if seenSet[srv] || srv == r.cfg.Matrix.ServerName { continue } seenSet[srv] = true @@ -235,6 +236,21 @@ func (r *FederationInternalAPI) performJoinUsingServer( return fmt.Errorf("respSendJoin.Check: %w", err) } + // We need to immediately update our list of joined hosts for this room now as we are technically + // joined. We must do this synchronously: we cannot rely on the roomserver output events as they + // will happen asyncly. If we don't update this table, you can end up with bad failure modes like + // joining a room, waiting for 200 OK then changing device keys and have those keys not be sent + // to other servers (this was a cause of a flakey sytest "Local device key changes get to remote servers") + // The events are trusted now as we performed auth checks above. + joinedHosts, err := consumers.JoinedHostsFromEvents(respState.StateEvents.TrustedEvents(respMakeJoin.RoomVersion, false)) + if err != nil { + return fmt.Errorf("JoinedHostsFromEvents: failed to get joined hosts: %s", err) + } + logrus.WithField("hosts", joinedHosts).WithField("room", roomID).Info("Joined federated room with hosts") + if _, err = r.db.UpdateRoom(context.Background(), roomID, joinedHosts, nil, true); err != nil { + return fmt.Errorf("UpdatedRoom: failed to update room with joined hosts: %s", err) + } + // If we successfully performed a send_join above then the other // server now thinks we're a part of the room. Send the newly // returned state to the roomserver to update our local view. @@ -563,20 +579,6 @@ func (r *FederationInternalAPI) PerformInvite( return nil } -// PerformServersAlive implements api.FederationInternalAPI -func (r *FederationInternalAPI) PerformServersAlive( - ctx context.Context, - request *api.PerformServersAliveRequest, - response *api.PerformServersAliveResponse, -) (err error) { - for _, srv := range request.Servers { - _ = r.db.RemoveServerFromBlacklist(srv) - r.queues.RetryServer(srv) - } - - return nil -} - // PerformServersAlive implements api.FederationInternalAPI func (r *FederationInternalAPI) PerformBroadcastEDU( ctx context.Context, @@ -600,18 +602,18 @@ func (r *FederationInternalAPI) PerformBroadcastEDU( if err = r.queues.SendEDU(edu, r.cfg.Matrix.ServerName, destinations); err != nil { return fmt.Errorf("r.queues.SendEDU: %w", err) } - - wakeReq := &api.PerformServersAliveRequest{ - Servers: destinations, - } - wakeRes := &api.PerformServersAliveResponse{} - if err := r.PerformServersAlive(ctx, wakeReq, wakeRes); err != nil { - return fmt.Errorf("r.PerformServersAlive: %w", err) - } + r.MarkServersAlive(destinations) return nil } +func (r *FederationInternalAPI) MarkServersAlive(destinations []gomatrixserverlib.ServerName) { + for _, srv := range destinations { + _ = r.db.RemoveServerFromBlacklist(srv) + r.queues.RetryServer(srv) + } +} + func sanityCheckAuthChain(authChain []*gomatrixserverlib.Event) error { // sanity check we have a create event and it has a known room version for _, ev := range authChain { @@ -664,7 +666,7 @@ func setDefaultRoomVersionFromJoinEvent(joinEvent gomatrixserverlib.EventBuilder // FederatedAuthProvider is an auth chain provider which fetches events from the server provided func federatedAuthProvider( - ctx context.Context, federation *gomatrixserverlib.FederationClient, + ctx context.Context, federation api.FederationClient, keyRing gomatrixserverlib.JSONVerifier, server gomatrixserverlib.ServerName, ) gomatrixserverlib.AuthChainProvider { // A list of events that we have retried, if they were not included in diff --git a/federationapi/inthttp/client.go b/federationapi/inthttp/client.go index 01ca6595d..295ddc495 100644 --- a/federationapi/inthttp/client.go +++ b/federationapi/inthttp/client.go @@ -23,7 +23,6 @@ const ( FederationAPIPerformLeaveRequestPath = "/federationapi/performLeaveRequest" FederationAPIPerformInviteRequestPath = "/federationapi/performInviteRequest" FederationAPIPerformOutboundPeekRequestPath = "/federationapi/performOutboundPeekRequest" - FederationAPIPerformServersAlivePath = "/federationapi/performServersAlive" FederationAPIPerformBroadcastEDUPath = "/federationapi/performBroadcastEDU" FederationAPIGetUserDevicesPath = "/federationapi/client/getUserDevices" @@ -97,18 +96,6 @@ func (h *httpFederationInternalAPI) PerformOutboundPeek( return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response) } -func (h *httpFederationInternalAPI) PerformServersAlive( - ctx context.Context, - request *api.PerformServersAliveRequest, - response *api.PerformServersAliveResponse, -) error { - span, ctx := opentracing.StartSpanFromContext(ctx, "PerformServersAlive") - defer span.Finish() - - apiURL := h.federationAPIURL + FederationAPIPerformServersAlivePath - return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response) -} - // QueryJoinedHostServerNamesInRoom implements FederationInternalAPI func (h *httpFederationInternalAPI) QueryJoinedHostServerNamesInRoom( ctx context.Context, diff --git a/federationapi/inthttp/server.go b/federationapi/inthttp/server.go index ca4930f20..28e52b32d 100644 --- a/federationapi/inthttp/server.go +++ b/federationapi/inthttp/server.go @@ -81,20 +81,6 @@ func AddRoutes(intAPI api.FederationInternalAPI, internalAPIMux *mux.Router) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) - internalAPIMux.Handle( - FederationAPIPerformServersAlivePath, - httputil.MakeInternalAPI("PerformServersAliveRequest", func(req *http.Request) util.JSONResponse { - var request api.PerformServersAliveRequest - var response api.PerformServersAliveResponse - if err := json.NewDecoder(req.Body).Decode(&request); err != nil { - return util.MessageResponse(http.StatusBadRequest, err.Error()) - } - if err := intAPI.PerformServersAlive(req.Context(), &request, &response); err != nil { - return util.ErrorResponse(err) - } - return util.JSONResponse{Code: http.StatusOK, JSON: &response} - }), - ) internalAPIMux.Handle( FederationAPIPerformBroadcastEDUPath, httputil.MakeInternalAPI("PerformBroadcastEDU", func(req *http.Request) util.JSONResponse { diff --git a/federationapi/queue/destinationqueue.go b/federationapi/queue/destinationqueue.go index 09814b31f..b6edec5da 100644 --- a/federationapi/queue/destinationqueue.go +++ b/federationapi/queue/destinationqueue.go @@ -21,6 +21,7 @@ import ( "sync" "time" + fedapi "github.com/matrix-org/dendrite/federationapi/api" "github.com/matrix-org/dendrite/federationapi/statistics" "github.com/matrix-org/dendrite/federationapi/storage" "github.com/matrix-org/dendrite/federationapi/storage/shared" @@ -49,21 +50,21 @@ type destinationQueue struct { db storage.Database process *process.ProcessContext signing *SigningInfo - rsAPI api.RoomserverInternalAPI - client *gomatrixserverlib.FederationClient // federation client - origin gomatrixserverlib.ServerName // origin of requests - destination gomatrixserverlib.ServerName // destination of requests - running atomic.Bool // is the queue worker running? - backingOff atomic.Bool // true if we're backing off - overflowed atomic.Bool // the queues exceed maxPDUsInMemory/maxEDUsInMemory, so we should consult the database for more - statistics *statistics.ServerStatistics // statistics about this remote server - transactionIDMutex sync.Mutex // protects transactionID - transactionID gomatrixserverlib.TransactionID // last transaction ID if retrying, or "" if last txn was successful - notify chan struct{} // interrupts idle wait pending PDUs/EDUs - pendingPDUs []*queuedPDU // PDUs waiting to be sent - pendingEDUs []*queuedEDU // EDUs waiting to be sent - pendingMutex sync.RWMutex // protects pendingPDUs and pendingEDUs - interruptBackoff chan bool // interrupts backoff + rsAPI api.FederationRoomserverAPI + client fedapi.FederationClient // federation client + origin gomatrixserverlib.ServerName // origin of requests + destination gomatrixserverlib.ServerName // destination of requests + running atomic.Bool // is the queue worker running? + backingOff atomic.Bool // true if we're backing off + overflowed atomic.Bool // the queues exceed maxPDUsInMemory/maxEDUsInMemory, so we should consult the database for more + statistics *statistics.ServerStatistics // statistics about this remote server + transactionIDMutex sync.Mutex // protects transactionID + transactionID gomatrixserverlib.TransactionID // last transaction ID if retrying, or "" if last txn was successful + notify chan struct{} // interrupts idle wait pending PDUs/EDUs + pendingPDUs []*queuedPDU // PDUs waiting to be sent + pendingEDUs []*queuedEDU // EDUs waiting to be sent + pendingMutex sync.RWMutex // protects pendingPDUs and pendingEDUs + interruptBackoff chan bool // interrupts backoff } // Send event adds the event to the pending queue for the destination. @@ -78,7 +79,7 @@ func (oq *destinationQueue) sendEvent(event *gomatrixserverlib.HeaderedEvent, re // this destination queue. We'll then be able to retrieve the PDU // later. if err := oq.db.AssociatePDUWithDestination( - context.TODO(), + oq.process.Context(), "", // TODO: remove this, as we don't need to persist the transaction ID oq.destination, // the destination server name receipt, // NIDs from federationapi_queue_json table @@ -122,9 +123,10 @@ func (oq *destinationQueue) sendEDU(event *gomatrixserverlib.EDU, receipt *share // this destination queue. We'll then be able to retrieve the PDU // later. if err := oq.db.AssociateEDUWithDestination( - context.TODO(), + oq.process.Context(), oq.destination, // the destination server name receipt, // NIDs from federationapi_queue_json table + event.Type, ); err != nil { logrus.WithError(err).Errorf("failed to associate EDU with destination %q", oq.destination) return @@ -176,7 +178,7 @@ func (oq *destinationQueue) getPendingFromDatabase() { // Check to see if there's anything to do for this server // in the database. retrieved := false - ctx := context.Background() + ctx := oq.process.Context() oq.pendingMutex.Lock() defer oq.pendingMutex.Unlock() @@ -270,6 +272,9 @@ func (oq *destinationQueue) backgroundSend() { // restarted automatically the next time we have an event to // send. return + case <-oq.process.Context().Done(): + // The parent process is shutting down, so stop. + return } // If we are backing off this server then wait for the @@ -419,13 +424,13 @@ func (oq *destinationQueue) nextTransaction( // Clean up the transaction in the database. if pduReceipts != nil { //logrus.Infof("Cleaning PDUs %q", pduReceipt.String()) - if err = oq.db.CleanPDUs(context.Background(), oq.destination, pduReceipts); err != nil { + if err = oq.db.CleanPDUs(oq.process.Context(), oq.destination, pduReceipts); err != nil { logrus.WithError(err).Errorf("Failed to clean PDUs for server %q", t.Destination) } } if eduReceipts != nil { //logrus.Infof("Cleaning EDUs %q", eduReceipt.String()) - if err = oq.db.CleanEDUs(context.Background(), oq.destination, eduReceipts); err != nil { + if err = oq.db.CleanEDUs(oq.process.Context(), oq.destination, eduReceipts); err != nil { logrus.WithError(err).Errorf("Failed to clean EDUs for server %q", t.Destination) } } diff --git a/federationapi/queue/queue.go b/federationapi/queue/queue.go index 5b5481274..4c25c4ce6 100644 --- a/federationapi/queue/queue.go +++ b/federationapi/queue/queue.go @@ -15,7 +15,6 @@ package queue import ( - "context" "crypto/ed25519" "encoding/json" "fmt" @@ -27,6 +26,7 @@ import ( log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" + fedapi "github.com/matrix-org/dendrite/federationapi/api" "github.com/matrix-org/dendrite/federationapi/statistics" "github.com/matrix-org/dendrite/federationapi/storage" "github.com/matrix-org/dendrite/federationapi/storage/shared" @@ -40,9 +40,9 @@ type OutgoingQueues struct { db storage.Database process *process.ProcessContext disabled bool - rsAPI api.RoomserverInternalAPI + rsAPI api.FederationRoomserverAPI origin gomatrixserverlib.ServerName - client *gomatrixserverlib.FederationClient + client fedapi.FederationClient statistics *statistics.Statistics signing *SigningInfo queuesMutex sync.Mutex // protects the below @@ -86,8 +86,8 @@ func NewOutgoingQueues( process *process.ProcessContext, disabled bool, origin gomatrixserverlib.ServerName, - client *gomatrixserverlib.FederationClient, - rsAPI api.RoomserverInternalAPI, + client fedapi.FederationClient, + rsAPI api.FederationRoomserverAPI, statistics *statistics.Statistics, signing *SigningInfo, ) *OutgoingQueues { @@ -105,14 +105,14 @@ func NewOutgoingQueues( // Look up which servers we have pending items for and then rehydrate those queues. if !disabled { serverNames := map[gomatrixserverlib.ServerName]struct{}{} - if names, err := db.GetPendingPDUServerNames(context.Background()); err == nil { + if names, err := db.GetPendingPDUServerNames(process.Context()); err == nil { for _, serverName := range names { serverNames[serverName] = struct{}{} } } else { log.WithError(err).Error("Failed to get PDU server names for destination queue hydration") } - if names, err := db.GetPendingEDUServerNames(context.Background()); err == nil { + if names, err := db.GetPendingEDUServerNames(process.Context()); err == nil { for _, serverName := range names { serverNames[serverName] = struct{}{} } @@ -210,11 +210,12 @@ func (oqs *OutgoingQueues) SendEvent( destmap[d] = struct{}{} } delete(destmap, oqs.origin) + delete(destmap, oqs.signing.ServerName) // Check if any of the destinations are prohibited by server ACLs. for destination := range destmap { if api.IsServerBannedFromRoom( - context.TODO(), + oqs.process.Context(), oqs.rsAPI, ev.RoomID(), destination, @@ -237,7 +238,7 @@ func (oqs *OutgoingQueues) SendEvent( return fmt.Errorf("json.Marshal: %w", err) } - nid, err := oqs.db.StoreJSON(context.TODO(), string(headeredJSON)) + nid, err := oqs.db.StoreJSON(oqs.process.Context(), string(headeredJSON)) if err != nil { return fmt.Errorf("sendevent: oqs.db.StoreJSON: %w", err) } @@ -275,6 +276,7 @@ func (oqs *OutgoingQueues) SendEDU( destmap[d] = struct{}{} } delete(destmap, oqs.origin) + delete(destmap, oqs.signing.ServerName) // There is absolutely no guarantee that the EDU will have a room_id // field, as it is not required by the spec. However, if it *does* @@ -284,7 +286,7 @@ func (oqs *OutgoingQueues) SendEDU( if result := gjson.GetBytes(e.Content, "room_id"); result.Exists() { for destination := range destmap { if api.IsServerBannedFromRoom( - context.TODO(), + oqs.process.Context(), oqs.rsAPI, result.Str, destination, @@ -308,7 +310,7 @@ func (oqs *OutgoingQueues) SendEDU( return fmt.Errorf("json.Marshal: %w", err) } - nid, err := oqs.db.StoreJSON(context.TODO(), string(ephemeralJSON)) + nid, err := oqs.db.StoreJSON(oqs.process.Context(), string(ephemeralJSON)) if err != nil { return fmt.Errorf("sendevent: oqs.db.StoreJSON: %w", err) } diff --git a/federationapi/routing/backfill.go b/federationapi/routing/backfill.go index 31005209f..7b9ca66f6 100644 --- a/federationapi/routing/backfill.go +++ b/federationapi/routing/backfill.go @@ -33,7 +33,7 @@ import ( func Backfill( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, cfg *config.FederationAPI, ) util.JSONResponse { @@ -51,6 +51,12 @@ func Backfill( } } + // If we don't think we belong to this room then don't waste the effort + // responding to expensive requests for it. + if err := ErrorIfLocalServerNotInRoom(httpReq.Context(), rsAPI, roomID); err != nil { + return *err + } + // Check if all of the required parameters are there. eIDs, exists = httpReq.URL.Query()["v"] if !exists { diff --git a/federationapi/routing/devices.go b/federationapi/routing/devices.go index 4cd199960..1a092645f 100644 --- a/federationapi/routing/devices.go +++ b/federationapi/routing/devices.go @@ -20,12 +20,13 @@ import ( keyapi "github.com/matrix-org/dendrite/keyserver/api" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" + "github.com/tidwall/gjson" ) // GetUserDevices for the given user id func GetUserDevices( req *http.Request, - keyAPI keyapi.KeyInternalAPI, + keyAPI keyapi.FederationKeyAPI, userID string, ) util.JSONResponse { var res keyapi.QueryDeviceMessagesResponse @@ -43,6 +44,9 @@ func GetUserDevices( }, } sigRes := &keyapi.QuerySignaturesResponse{} + for _, dev := range res.Devices { + sigReq.TargetIDs[userID] = append(sigReq.TargetIDs[userID], gomatrixserverlib.KeyID(dev.DeviceID)) + } keyAPI.QuerySignatures(req.Context(), sigReq, sigRes) response := gomatrixserverlib.RespUserDevices{ @@ -66,9 +70,14 @@ func GetUserDevices( continue } + displayName := dev.DisplayName + if displayName == "" { + displayName = gjson.GetBytes(dev.DeviceKeys.KeyJSON, "unsigned.device_display_name").Str + } + device := gomatrixserverlib.RespUserDevice{ DeviceID: dev.DeviceID, - DisplayName: dev.DisplayName, + DisplayName: displayName, Keys: key, } diff --git a/federationapi/routing/eventauth.go b/federationapi/routing/eventauth.go index 0a03a0cb4..868785a9b 100644 --- a/federationapi/routing/eventauth.go +++ b/federationapi/routing/eventauth.go @@ -26,10 +26,16 @@ import ( func GetEventAuth( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, eventID string, ) util.JSONResponse { + // If we don't think we belong to this room then don't waste the effort + // responding to expensive requests for it. + if err := ErrorIfLocalServerNotInRoom(ctx, rsAPI, roomID); err != nil { + return *err + } + event, resErr := fetchEvent(ctx, rsAPI, eventID) if resErr != nil { return *resErr diff --git a/federationapi/routing/events.go b/federationapi/routing/events.go index 312ef9f8e..23796edfa 100644 --- a/federationapi/routing/events.go +++ b/federationapi/routing/events.go @@ -29,7 +29,7 @@ import ( func GetEvent( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, eventID string, origin gomatrixserverlib.ServerName, ) util.JSONResponse { @@ -56,7 +56,7 @@ func GetEvent( func allowedToSeeEvent( ctx context.Context, origin gomatrixserverlib.ServerName, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, eventID string, ) *util.JSONResponse { var authResponse api.QueryServerAllowedToSeeEventResponse @@ -82,7 +82,7 @@ func allowedToSeeEvent( } // fetchEvent fetches the event without auth checks. Returns an error if the event cannot be found. -func fetchEvent(ctx context.Context, rsAPI api.RoomserverInternalAPI, eventID string) (*gomatrixserverlib.Event, *util.JSONResponse) { +func fetchEvent(ctx context.Context, rsAPI api.FederationRoomserverAPI, eventID string) (*gomatrixserverlib.Event, *util.JSONResponse) { var eventsResponse api.QueryEventsByIDResponse err := rsAPI.QueryEventsByID( ctx, diff --git a/federationapi/routing/invite.go b/federationapi/routing/invite.go index 58bf99f4a..a5797645e 100644 --- a/federationapi/routing/invite.go +++ b/federationapi/routing/invite.go @@ -35,7 +35,7 @@ func InviteV2( roomID string, eventID string, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keys gomatrixserverlib.JSONVerifier, ) util.JSONResponse { inviteReq := gomatrixserverlib.InviteV2Request{} @@ -72,7 +72,7 @@ func InviteV1( roomID string, eventID string, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keys gomatrixserverlib.JSONVerifier, ) util.JSONResponse { roomVer := gomatrixserverlib.RoomVersionV1 @@ -110,7 +110,7 @@ func processInvite( roomID string, eventID string, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keys gomatrixserverlib.JSONVerifier, ) util.JSONResponse { @@ -166,31 +166,36 @@ func processInvite( ) // Add the invite event to the roomserver. - err = api.SendInvite( - ctx, rsAPI, signedEvent.Headered(roomVer), strippedState, api.DoNotSendToOtherServers, nil, - ) - switch e := err.(type) { - case *api.PerformError: - return e.JSONResponse() - case nil: - // Return the signed event to the originating server, it should then tell - // the other servers in the room that we have been invited. - if isInviteV2 { - return util.JSONResponse{ - Code: http.StatusOK, - JSON: gomatrixserverlib.RespInviteV2{Event: signedEvent.JSON()}, - } - } else { - return util.JSONResponse{ - Code: http.StatusOK, - JSON: gomatrixserverlib.RespInvite{Event: signedEvent.JSON()}, - } - } - default: - util.GetLogger(ctx).WithError(err).Error("api.SendInvite failed") + inviteEvent := signedEvent.Headered(roomVer) + request := &api.PerformInviteRequest{ + Event: inviteEvent, + InviteRoomState: strippedState, + RoomVersion: inviteEvent.RoomVersion, + SendAsServer: string(api.DoNotSendToOtherServers), + TransactionID: nil, + } + response := &api.PerformInviteResponse{} + if err := rsAPI.PerformInvite(ctx, request, response); err != nil { + util.GetLogger(ctx).WithError(err).Error("PerformInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: jsonerror.InternalServerError(), } } + if response.Error != nil { + return response.Error.JSONResponse() + } + // Return the signed event to the originating server, it should then tell + // the other servers in the room that we have been invited. + if isInviteV2 { + return util.JSONResponse{ + Code: http.StatusOK, + JSON: gomatrixserverlib.RespInviteV2{Event: signedEvent.JSON()}, + } + } else { + return util.JSONResponse{ + Code: http.StatusOK, + JSON: gomatrixserverlib.RespInvite{Event: signedEvent.JSON()}, + } + } } diff --git a/federationapi/routing/join.go b/federationapi/routing/join.go index 495b8c914..767699728 100644 --- a/federationapi/routing/join.go +++ b/federationapi/routing/join.go @@ -34,7 +34,7 @@ func MakeJoin( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID, userID string, remoteVersions []gomatrixserverlib.RoomVersion, ) util.JSONResponse { @@ -165,7 +165,7 @@ func SendJoin( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keys gomatrixserverlib.JSONVerifier, roomID, eventID string, ) util.JSONResponse { diff --git a/federationapi/routing/keys.go b/federationapi/routing/keys.go index 49a6c558f..b1a9b6710 100644 --- a/federationapi/routing/keys.go +++ b/federationapi/routing/keys.go @@ -37,7 +37,7 @@ type queryKeysRequest struct { // QueryDeviceKeys returns device keys for users on this server. // https://matrix.org/docs/spec/server_server/latest#post-matrix-federation-v1-user-keys-query func QueryDeviceKeys( - httpReq *http.Request, request *gomatrixserverlib.FederationRequest, keyAPI api.KeyInternalAPI, thisServer gomatrixserverlib.ServerName, + httpReq *http.Request, request *gomatrixserverlib.FederationRequest, keyAPI api.FederationKeyAPI, thisServer gomatrixserverlib.ServerName, ) util.JSONResponse { var qkr queryKeysRequest err := json.Unmarshal(request.Content(), &qkr) @@ -89,7 +89,7 @@ type claimOTKsRequest struct { // ClaimOneTimeKeys claims OTKs for users on this server. // https://matrix.org/docs/spec/server_server/latest#post-matrix-federation-v1-user-keys-claim func ClaimOneTimeKeys( - httpReq *http.Request, request *gomatrixserverlib.FederationRequest, keyAPI api.KeyInternalAPI, thisServer gomatrixserverlib.ServerName, + httpReq *http.Request, request *gomatrixserverlib.FederationRequest, keyAPI api.FederationKeyAPI, thisServer gomatrixserverlib.ServerName, ) util.JSONResponse { var cor claimOTKsRequest err := json.Unmarshal(request.Content(), &cor) diff --git a/federationapi/routing/leave.go b/federationapi/routing/leave.go index 0b83f04ae..54b2c3e84 100644 --- a/federationapi/routing/leave.go +++ b/federationapi/routing/leave.go @@ -30,7 +30,7 @@ func MakeLeave( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID, userID string, ) util.JSONResponse { _, domain, err := gomatrixserverlib.SplitID('@', userID) @@ -122,7 +122,7 @@ func SendLeave( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keys gomatrixserverlib.JSONVerifier, roomID, eventID string, ) util.JSONResponse { diff --git a/federationapi/routing/missingevents.go b/federationapi/routing/missingevents.go index dd3df7aa9..531cb9e28 100644 --- a/federationapi/routing/missingevents.go +++ b/federationapi/routing/missingevents.go @@ -34,7 +34,7 @@ type getMissingEventRequest struct { func GetMissingEvents( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, ) util.JSONResponse { var gme getMissingEventRequest @@ -45,6 +45,12 @@ func GetMissingEvents( } } + // If we don't think we belong to this room then don't waste the effort + // responding to expensive requests for it. + if err := ErrorIfLocalServerNotInRoom(httpReq.Context(), rsAPI, roomID); err != nil { + return *err + } + var eventsResponse api.QueryMissingEventsResponse if err := rsAPI.QueryMissingEvents( httpReq.Context(), &api.QueryMissingEventsRequest{ diff --git a/federationapi/routing/openid.go b/federationapi/routing/openid.go index 829dbccad..cbc75a9a7 100644 --- a/federationapi/routing/openid.go +++ b/federationapi/routing/openid.go @@ -30,7 +30,7 @@ type openIDUserInfoResponse struct { // GetOpenIDUserInfo implements GET /_matrix/federation/v1/openid/userinfo func GetOpenIDUserInfo( httpReq *http.Request, - userAPI userapi.UserInternalAPI, + userAPI userapi.FederationUserAPI, ) util.JSONResponse { token := httpReq.URL.Query().Get("access_token") if len(token) == 0 { diff --git a/federationapi/routing/peek.go b/federationapi/routing/peek.go index 827d1116d..bc4dac90f 100644 --- a/federationapi/routing/peek.go +++ b/federationapi/routing/peek.go @@ -29,7 +29,7 @@ func Peek( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID, peekID string, remoteVersions []gomatrixserverlib.RoomVersion, ) util.JSONResponse { diff --git a/federationapi/routing/profile.go b/federationapi/routing/profile.go index dbc209ce1..f672811af 100644 --- a/federationapi/routing/profile.go +++ b/federationapi/routing/profile.go @@ -29,7 +29,7 @@ import ( // GetProfile implements GET /_matrix/federation/v1/query/profile func GetProfile( httpReq *http.Request, - userAPI userapi.UserInternalAPI, + userAPI userapi.FederationUserAPI, cfg *config.FederationAPI, ) util.JSONResponse { userID, field := httpReq.FormValue("user_id"), httpReq.FormValue("field") diff --git a/federationapi/routing/publicrooms.go b/federationapi/routing/publicrooms.go index a253f86eb..1a54f5a7d 100644 --- a/federationapi/routing/publicrooms.go +++ b/federationapi/routing/publicrooms.go @@ -23,7 +23,7 @@ type filter struct { } // GetPostPublicRooms implements GET and POST /publicRooms -func GetPostPublicRooms(req *http.Request, rsAPI roomserverAPI.RoomserverInternalAPI) util.JSONResponse { +func GetPostPublicRooms(req *http.Request, rsAPI roomserverAPI.FederationRoomserverAPI) util.JSONResponse { var request PublicRoomReq if fillErr := fillPublicRoomsReq(req, &request); fillErr != nil { return *fillErr @@ -42,7 +42,7 @@ func GetPostPublicRooms(req *http.Request, rsAPI roomserverAPI.RoomserverInterna } func publicRooms( - ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.RoomserverInternalAPI, + ctx context.Context, request PublicRoomReq, rsAPI roomserverAPI.FederationRoomserverAPI, ) (*gomatrixserverlib.RespPublicRooms, error) { var response gomatrixserverlib.RespPublicRooms @@ -111,7 +111,7 @@ func fillPublicRoomsReq(httpReq *http.Request, request *PublicRoomReq) *util.JSO } // due to lots of switches -func fillInRooms(ctx context.Context, roomIDs []string, rsAPI roomserverAPI.RoomserverInternalAPI) ([]gomatrixserverlib.PublicRoom, error) { +func fillInRooms(ctx context.Context, roomIDs []string, rsAPI roomserverAPI.FederationRoomserverAPI) ([]gomatrixserverlib.PublicRoom, error) { avatarTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.avatar", StateKey: ""} nameTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.name", StateKey: ""} canonicalTuple := gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomCanonicalAlias, StateKey: ""} diff --git a/federationapi/routing/query.go b/federationapi/routing/query.go index 47d3b2df9..316c61a14 100644 --- a/federationapi/routing/query.go +++ b/federationapi/routing/query.go @@ -30,9 +30,9 @@ import ( // RoomAliasToID converts the queried alias into a room ID and returns it func RoomAliasToID( httpReq *http.Request, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, cfg *config.FederationAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.FederationRoomserverAPI, senderAPI federationAPI.FederationInternalAPI, ) util.JSONResponse { roomAlias := httpReq.FormValue("room_alias") diff --git a/federationapi/routing/routing.go b/federationapi/routing/routing.go index a085ed780..e25f9866e 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -15,15 +15,22 @@ package routing import ( + "context" + "fmt" "net/http" + "sync" + "time" + "github.com/getsentry/sentry-go" "github.com/gorilla/mux" "github.com/matrix-org/dendrite/clientapi/jsonerror" federationAPI "github.com/matrix-org/dendrite/federationapi/api" + fedInternal "github.com/matrix-org/dendrite/federationapi/internal" "github.com/matrix-org/dendrite/federationapi/producers" "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/httputil" keyserverAPI "github.com/matrix-org/dendrite/keyserver/api" + "github.com/matrix-org/dendrite/roomserver/api" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" @@ -44,12 +51,12 @@ import ( func Setup( fedMux, keyMux, wkMux *mux.Router, cfg *config.FederationAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - fsAPI federationAPI.FederationInternalAPI, + rsAPI roomserverAPI.FederationRoomserverAPI, + fsAPI *fedInternal.FederationInternalAPI, keys gomatrixserverlib.JSONVerifier, - federation *gomatrixserverlib.FederationClient, - userAPI userapi.UserInternalAPI, - keyAPI keyserverAPI.KeyInternalAPI, + federation federationAPI.FederationClient, + userAPI userapi.FederationUserAPI, + keyAPI keyserverAPI.FederationKeyAPI, mscCfg *config.MSCs, servers federationAPI.ServersInRoomProvider, producer *producers.SyncAPIProducer, @@ -62,7 +69,7 @@ func Setup( v1fedmux := fedMux.PathPrefix("/v1").Subrouter() v2fedmux := fedMux.PathPrefix("/v2").Subrouter() - wakeup := &httputil.FederationWakeups{ + wakeup := &FederationWakeups{ FsAPI: fsAPI, } @@ -116,7 +123,7 @@ func Setup( v2keysmux.Handle("/query/{serverName}/{keyID}", notaryKeys).Methods(http.MethodGet) mu := internal.NewMutexByRoom() - v1fedmux.Handle("/send/{txnID}", httputil.MakeFedAPI( + v1fedmux.Handle("/send/{txnID}", MakeFedAPI( "federation_send", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return Send( @@ -126,7 +133,7 @@ func Setup( }, )).Methods(http.MethodPut, http.MethodOptions) - v1fedmux.Handle("/invite/{roomID}/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/invite/{roomID}/{eventID}", MakeFedAPI( "federation_invite", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -142,7 +149,7 @@ func Setup( }, )).Methods(http.MethodPut, http.MethodOptions) - v2fedmux.Handle("/invite/{roomID}/{eventID}", httputil.MakeFedAPI( + v2fedmux.Handle("/invite/{roomID}/{eventID}", MakeFedAPI( "federation_invite", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -164,7 +171,7 @@ func Setup( }, )).Methods(http.MethodPost, http.MethodOptions) - v1fedmux.Handle("/exchange_third_party_invite/{roomID}", httputil.MakeFedAPI( + v1fedmux.Handle("/exchange_third_party_invite/{roomID}", MakeFedAPI( "exchange_third_party_invite", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return ExchangeThirdPartyInvite( @@ -173,7 +180,7 @@ func Setup( }, )).Methods(http.MethodPut, http.MethodOptions) - v1fedmux.Handle("/event/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/event/{eventID}", MakeFedAPI( "federation_get_event", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return GetEvent( @@ -182,7 +189,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/state/{roomID}", httputil.MakeFedAPI( + v1fedmux.Handle("/state/{roomID}", MakeFedAPI( "federation_get_state", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -197,7 +204,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/state_ids/{roomID}", httputil.MakeFedAPI( + v1fedmux.Handle("/state_ids/{roomID}", MakeFedAPI( "federation_get_state_ids", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -212,7 +219,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/event_auth/{roomID}/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/event_auth/{roomID}/{eventID}", MakeFedAPI( "federation_get_event_auth", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -227,7 +234,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/query/directory", httputil.MakeFedAPI( + v1fedmux.Handle("/query/directory", MakeFedAPI( "federation_query_room_alias", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return RoomAliasToID( @@ -236,7 +243,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/query/profile", httputil.MakeFedAPI( + v1fedmux.Handle("/query/profile", MakeFedAPI( "federation_query_profile", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return GetProfile( @@ -245,7 +252,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/user/devices/{userID}", httputil.MakeFedAPI( + v1fedmux.Handle("/user/devices/{userID}", MakeFedAPI( "federation_user_devices", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return GetUserDevices( @@ -255,7 +262,7 @@ func Setup( )).Methods(http.MethodGet) if mscCfg.Enabled("msc2444") { - v1fedmux.Handle("/peek/{roomID}/{peekID}", httputil.MakeFedAPI( + v1fedmux.Handle("/peek/{roomID}/{peekID}", MakeFedAPI( "federation_peek", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -286,7 +293,7 @@ func Setup( )).Methods(http.MethodPut, http.MethodDelete) } - v1fedmux.Handle("/make_join/{roomID}/{userID}", httputil.MakeFedAPI( + v1fedmux.Handle("/make_join/{roomID}/{userID}", MakeFedAPI( "federation_make_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -317,7 +324,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/send_join/{roomID}/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/send_join/{roomID}/{eventID}", MakeFedAPI( "federation_send_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -349,7 +356,7 @@ func Setup( }, )).Methods(http.MethodPut) - v2fedmux.Handle("/send_join/{roomID}/{eventID}", httputil.MakeFedAPI( + v2fedmux.Handle("/send_join/{roomID}/{eventID}", MakeFedAPI( "federation_send_join", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -366,7 +373,7 @@ func Setup( }, )).Methods(http.MethodPut) - v1fedmux.Handle("/make_leave/{roomID}/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/make_leave/{roomID}/{eventID}", MakeFedAPI( "federation_make_leave", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -383,7 +390,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/send_leave/{roomID}/{eventID}", httputil.MakeFedAPI( + v1fedmux.Handle("/send_leave/{roomID}/{eventID}", MakeFedAPI( "federation_send_leave", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -415,7 +422,7 @@ func Setup( }, )).Methods(http.MethodPut) - v2fedmux.Handle("/send_leave/{roomID}/{eventID}", httputil.MakeFedAPI( + v2fedmux.Handle("/send_leave/{roomID}/{eventID}", MakeFedAPI( "federation_send_leave", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -439,7 +446,7 @@ func Setup( }, )).Methods(http.MethodGet) - v1fedmux.Handle("/get_missing_events/{roomID}", httputil.MakeFedAPI( + v1fedmux.Handle("/get_missing_events/{roomID}", MakeFedAPI( "federation_get_missing_events", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -452,7 +459,7 @@ func Setup( }, )).Methods(http.MethodPost) - v1fedmux.Handle("/backfill/{roomID}", httputil.MakeFedAPI( + v1fedmux.Handle("/backfill/{roomID}", MakeFedAPI( "federation_backfill", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { if roomserverAPI.IsServerBannedFromRoom(httpReq.Context(), rsAPI, vars["roomID"], request.Origin()) { @@ -471,14 +478,14 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodPost) - v1fedmux.Handle("/user/keys/claim", httputil.MakeFedAPI( + v1fedmux.Handle("/user/keys/claim", MakeFedAPI( "federation_keys_claim", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return ClaimOneTimeKeys(httpReq, request, keyAPI, cfg.Matrix.ServerName) }, )).Methods(http.MethodPost) - v1fedmux.Handle("/user/keys/query", httputil.MakeFedAPI( + v1fedmux.Handle("/user/keys/query", MakeFedAPI( "federation_keys_query", cfg.Matrix.ServerName, keys, wakeup, func(httpReq *http.Request, request *gomatrixserverlib.FederationRequest, vars map[string]string) util.JSONResponse { return QueryDeviceKeys(httpReq, request, keyAPI, cfg.Matrix.ServerName) @@ -491,3 +498,91 @@ func Setup( }), ).Methods(http.MethodGet) } + +func ErrorIfLocalServerNotInRoom( + ctx context.Context, + rsAPI api.FederationRoomserverAPI, + roomID string, +) *util.JSONResponse { + // Check if we think we're in this room. If we aren't then + // we won't waste CPU cycles serving this request. + joinedReq := &api.QueryServerJoinedToRoomRequest{ + RoomID: roomID, + } + joinedRes := &api.QueryServerJoinedToRoomResponse{} + if err := rsAPI.QueryServerJoinedToRoom(ctx, joinedReq, joinedRes); err != nil { + res := util.ErrorResponse(err) + return &res + } + if !joinedRes.IsInRoom { + return &util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound(fmt.Sprintf("This server is not joined to room %s", roomID)), + } + } + return nil +} + +// MakeFedAPI makes an http.Handler that checks matrix federation authentication. +func MakeFedAPI( + metricsName string, + serverName gomatrixserverlib.ServerName, + keyRing gomatrixserverlib.JSONVerifier, + wakeup *FederationWakeups, + f func(*http.Request, *gomatrixserverlib.FederationRequest, map[string]string) util.JSONResponse, +) http.Handler { + h := func(req *http.Request) util.JSONResponse { + fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest( + req, time.Now(), serverName, keyRing, + ) + if fedReq == nil { + return errResp + } + // add the user to Sentry, if enabled + hub := sentry.GetHubFromContext(req.Context()) + if hub != nil { + hub.Scope().SetTag("origin", string(fedReq.Origin())) + hub.Scope().SetTag("uri", fedReq.RequestURI()) + } + defer func() { + if r := recover(); r != nil { + if hub != nil { + hub.CaptureException(fmt.Errorf("%s panicked", req.URL.Path)) + } + // re-panic to return the 500 + panic(r) + } + }() + go wakeup.Wakeup(req.Context(), fedReq.Origin()) + vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.MatrixErrorResponse(400, "M_UNRECOGNISED", "badly encoded query params") + } + + jsonRes := f(req, fedReq, vars) + // do not log 4xx as errors as they are client fails, not server fails + if hub != nil && jsonRes.Code >= 500 { + hub.Scope().SetExtra("response", jsonRes) + hub.CaptureException(fmt.Errorf("%s returned HTTP %d", req.URL.Path, jsonRes.Code)) + } + return jsonRes + } + return httputil.MakeExternalAPI(metricsName, h) +} + +type FederationWakeups struct { + FsAPI *fedInternal.FederationInternalAPI + origins sync.Map +} + +func (f *FederationWakeups) Wakeup(ctx context.Context, origin gomatrixserverlib.ServerName) { + key, keyok := f.origins.Load(origin) + if keyok { + lastTime, ok := key.(time.Time) + if ok && time.Since(lastTime) < time.Minute { + return + } + } + f.FsAPI.MarkServersAlive([]gomatrixserverlib.ServerName{origin}) + f.origins.Store(origin, time.Now()) +} diff --git a/federationapi/routing/send.go b/federationapi/routing/send.go index f2b902b6f..c25dabce9 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -82,10 +82,10 @@ func Send( request *gomatrixserverlib.FederationRequest, txnID gomatrixserverlib.TransactionID, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, - keyAPI keyapi.KeyInternalAPI, + rsAPI api.FederationRoomserverAPI, + keyAPI keyapi.FederationKeyAPI, keys gomatrixserverlib.JSONVerifier, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, mu *internal.MutexByRoom, servers federationAPI.ServersInRoomProvider, producer *producers.SyncAPIProducer, @@ -124,6 +124,7 @@ func Send( t := txnReq{ rsAPI: rsAPI, keys: keys, + ourServerName: cfg.Matrix.ServerName, federation: federation, servers: servers, keyAPI: keyAPI, @@ -181,8 +182,9 @@ func Send( type txnReq struct { gomatrixserverlib.Transaction - rsAPI api.RoomserverInternalAPI - keyAPI keyapi.KeyInternalAPI + rsAPI api.FederationRoomserverAPI + keyAPI keyapi.FederationKeyAPI + ourServerName gomatrixserverlib.ServerName keys gomatrixserverlib.JSONVerifier federation txnFederationClient roomsMu *internal.MutexByRoom @@ -303,6 +305,7 @@ func (t *txnReq) processTransaction(ctx context.Context) (*gomatrixserverlib.Res return &gomatrixserverlib.RespSend{PDUs: results}, nil } +// nolint:gocyclo func (t *txnReq) processEDUs(ctx context.Context) { for _, e := range t.EDUs { eduCountTotal.Inc() @@ -318,13 +321,11 @@ func (t *txnReq) processEDUs(ctx context.Context) { util.GetLogger(ctx).WithError(err).Debug("Failed to unmarshal typing event") continue } - _, domain, err := gomatrixserverlib.SplitID('@', typingPayload.UserID) - if err != nil { - util.GetLogger(ctx).WithError(err).Debug("Failed to split domain from typing event sender") + if _, serverName, err := gomatrixserverlib.SplitID('@', typingPayload.UserID); err != nil { continue - } - if domain != t.Origin { - util.GetLogger(ctx).Debugf("Dropping typing event where sender domain (%q) doesn't match origin (%q)", domain, t.Origin) + } else if serverName == t.ourServerName { + continue + } else if serverName != t.Origin { continue } if err := t.producer.SendTyping(ctx, typingPayload.UserID, typingPayload.RoomID, typingPayload.Typing, 30*1000); err != nil { @@ -337,6 +338,13 @@ func (t *txnReq) processEDUs(ctx context.Context) { util.GetLogger(ctx).WithError(err).Debug("Failed to unmarshal send-to-device events") continue } + if _, serverName, err := gomatrixserverlib.SplitID('@', directPayload.Sender); err != nil { + continue + } else if serverName == t.ourServerName { + continue + } else if serverName != t.Origin { + continue + } for userID, byUser := range directPayload.Messages { for deviceID, message := range byUser { // TODO: check that the user and the device actually exist here @@ -405,6 +413,13 @@ func (t *txnReq) processPresence(ctx context.Context, e gomatrixserverlib.EDU) e return err } for _, content := range payload.Push { + if _, serverName, err := gomatrixserverlib.SplitID('@', content.UserID); err != nil { + continue + } else if serverName == t.ourServerName { + continue + } else if serverName != t.Origin { + continue + } presence, ok := syncTypes.PresenceFromString(content.Presence) if !ok { continue @@ -424,7 +439,13 @@ func (t *txnReq) processSigningKeyUpdate(ctx context.Context, e gomatrixserverli }).Debug("Failed to unmarshal signing key update") return err } - + if _, serverName, err := gomatrixserverlib.SplitID('@', updatePayload.UserID); err != nil { + return nil + } else if serverName == t.ourServerName { + return nil + } else if serverName != t.Origin { + return nil + } keys := gomatrixserverlib.CrossSigningKeys{} if updatePayload.MasterKey != nil { keys.MasterKey = *updatePayload.MasterKey @@ -450,6 +471,13 @@ func (t *txnReq) processReceiptEvent(ctx context.Context, timestamp gomatrixserverlib.Timestamp, eventIDs []string, ) error { + if _, serverName, err := gomatrixserverlib.SplitID('@', userID); err != nil { + return nil + } else if serverName == t.ourServerName { + return nil + } else if serverName != t.Origin { + return nil + } // store every event for _, eventID := range eventIDs { if err := t.producer.SendReceipt(ctx, userID, roomID, eventID, receiptType, timestamp); err != nil { @@ -466,6 +494,13 @@ func (t *txnReq) processDeviceListUpdate(ctx context.Context, e gomatrixserverli util.GetLogger(ctx).WithError(err).Error("Failed to unmarshal device list update event") return } + if _, serverName, err := gomatrixserverlib.SplitID('@', payload.UserID); err != nil { + return + } else if serverName == t.ourServerName { + return + } else if serverName != t.Origin { + return + } var inputRes keyapi.InputDeviceListUpdateResponse t.keyAPI.InputDeviceListUpdate(context.Background(), &keyapi.InputDeviceListUpdateRequest{ Event: payload, diff --git a/federationapi/routing/send_test.go b/federationapi/routing/send_test.go index 8d2d85040..a111580c7 100644 --- a/federationapi/routing/send_test.go +++ b/federationapi/routing/send_test.go @@ -8,8 +8,8 @@ import ( "time" "github.com/matrix-org/dendrite/internal" - "github.com/matrix-org/dendrite/internal/test" "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/test" "github.com/matrix-org/gomatrixserverlib" ) @@ -183,7 +183,7 @@ func (c *txnFedClient) LookupMissingEvents(ctx context.Context, s gomatrixserver return c.getMissingEvents(missing) } -func mustCreateTransaction(rsAPI api.RoomserverInternalAPI, fedClient txnFederationClient, pdus []json.RawMessage) *txnReq { +func mustCreateTransaction(rsAPI api.FederationRoomserverAPI, fedClient txnFederationClient, pdus []json.RawMessage) *txnReq { t := &txnReq{ rsAPI: rsAPI, keys: &test.NopJSONVerifier{}, diff --git a/federationapi/routing/state.go b/federationapi/routing/state.go index 37cbb9d1e..6fdce20ce 100644 --- a/federationapi/routing/state.go +++ b/federationapi/routing/state.go @@ -27,7 +27,7 @@ import ( func GetState( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, ) util.JSONResponse { eventID, err := parseEventIDParam(request) @@ -50,7 +50,7 @@ func GetState( func GetStateIDs( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, ) util.JSONResponse { eventID, err := parseEventIDParam(request) @@ -97,10 +97,16 @@ func parseEventIDParam( func getState( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, eventID string, ) (stateEvents, authEvents []*gomatrixserverlib.HeaderedEvent, errRes *util.JSONResponse) { + // If we don't think we belong to this room then don't waste the effort + // responding to expensive requests for it. + if err := ErrorIfLocalServerNotInRoom(ctx, rsAPI, roomID); err != nil { + return nil, nil, err + } + event, resErr := fetchEvent(ctx, rsAPI, eventID) if resErr != nil { return nil, nil, resErr @@ -129,6 +135,13 @@ func getState( return nil, nil, &resErr } + if response.IsRejected { + return nil, nil, &util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Event not found"), + } + } + if !response.RoomExists { return nil, nil, &util.JSONResponse{Code: http.StatusNotFound, JSON: nil} } diff --git a/federationapi/routing/threepid.go b/federationapi/routing/threepid.go index 8ae7130c3..ccde9168e 100644 --- a/federationapi/routing/threepid.go +++ b/federationapi/routing/threepid.go @@ -23,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" + federationAPI "github.com/matrix-org/dendrite/federationapi/api" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" @@ -55,10 +56,10 @@ var ( // CreateInvitesFrom3PIDInvites implements POST /_matrix/federation/v1/3pid/onbind func CreateInvitesFrom3PIDInvites( - req *http.Request, rsAPI api.RoomserverInternalAPI, + req *http.Request, rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, - federation *gomatrixserverlib.FederationClient, - userAPI userapi.UserInternalAPI, + federation federationAPI.FederationClient, + userAPI userapi.FederationUserAPI, ) util.JSONResponse { var body invites if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { @@ -105,9 +106,9 @@ func ExchangeThirdPartyInvite( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, roomID string, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, ) util.JSONResponse { var builder gomatrixserverlib.EventBuilder if err := json.Unmarshal(request.Content(), &builder); err != nil { @@ -165,7 +166,12 @@ func ExchangeThirdPartyInvite( // Ask the requesting server to sign the newly created event so we know it // acknowledged it - signedEvent, err := federation.SendInvite(httpReq.Context(), request.Origin(), event) + inviteReq, err := gomatrixserverlib.NewInviteV2Request(event.Headered(verRes.RoomVersion), nil) + if err != nil { + util.GetLogger(httpReq.Context()).WithError(err).Error("failed to make invite v2 request") + return jsonerror.InternalServerError() + } + signedEvent, err := federation.SendInviteV2(httpReq.Context(), request.Origin(), inviteReq) if err != nil { util.GetLogger(httpReq.Context()).WithError(err).Error("federation.SendInvite failed") return jsonerror.InternalServerError() @@ -203,10 +209,10 @@ func ExchangeThirdPartyInvite( // Returns an error if there was a problem building the event or fetching the // necessary data to do so. func createInviteFrom3PIDInvite( - ctx context.Context, rsAPI api.RoomserverInternalAPI, + ctx context.Context, rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, - inv invite, federation *gomatrixserverlib.FederationClient, - userAPI userapi.UserInternalAPI, + inv invite, federation federationAPI.FederationClient, + userAPI userapi.FederationUserAPI, ) (*gomatrixserverlib.Event, error) { verReq := api.QueryRoomVersionForRoomRequest{RoomID: inv.RoomID} verRes := api.QueryRoomVersionForRoomResponse{} @@ -270,7 +276,7 @@ func createInviteFrom3PIDInvite( // Returns an error if something failed during the process. func buildMembershipEvent( ctx context.Context, - builder *gomatrixserverlib.EventBuilder, rsAPI api.RoomserverInternalAPI, + builder *gomatrixserverlib.EventBuilder, rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, ) (*gomatrixserverlib.Event, error) { eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) @@ -335,7 +341,7 @@ func buildMembershipEvent( // them responded with an error. func sendToRemoteServer( ctx context.Context, inv invite, - federation *gomatrixserverlib.FederationClient, _ *config.FederationAPI, + federation federationAPI.FederationClient, _ *config.FederationAPI, builder gomatrixserverlib.EventBuilder, ) (err error) { remoteServers := make([]gomatrixserverlib.ServerName, 2) diff --git a/federationapi/storage/interface.go b/federationapi/storage/interface.go index 3fa8d1f7a..29254948b 100644 --- a/federationapi/storage/interface.go +++ b/federationapi/storage/interface.go @@ -25,13 +25,12 @@ import ( type Database interface { gomatrixserverlib.KeyDatabase - UpdateRoom(ctx context.Context, roomID, oldEventID, newEventID string, addHosts []types.JoinedHost, removeHosts []string) (joinedHosts []types.JoinedHost, err error) + UpdateRoom(ctx context.Context, roomID string, addHosts []types.JoinedHost, removeHosts []string, purgeRoomFirst bool) (joinedHosts []types.JoinedHost, err error) GetJoinedHosts(ctx context.Context, roomID string) ([]types.JoinedHost, error) GetAllJoinedHosts(ctx context.Context) ([]gomatrixserverlib.ServerName, error) // GetJoinedHostsForRooms returns the complete set of servers in the rooms given. GetJoinedHostsForRooms(ctx context.Context, roomIDs []string, excludeSelf bool) ([]gomatrixserverlib.ServerName, error) - PurgeRoomState(ctx context.Context, roomID string) error StoreJSON(ctx context.Context, js string) (*shared.Receipt, error) @@ -39,7 +38,7 @@ type Database interface { GetPendingEDUs(ctx context.Context, serverName gomatrixserverlib.ServerName, limit int) (edus map[*shared.Receipt]*gomatrixserverlib.EDU, err error) AssociatePDUWithDestination(ctx context.Context, transactionID gomatrixserverlib.TransactionID, serverName gomatrixserverlib.ServerName, receipt *shared.Receipt) error - AssociateEDUWithDestination(ctx context.Context, serverName gomatrixserverlib.ServerName, receipt *shared.Receipt) error + AssociateEDUWithDestination(ctx context.Context, serverName gomatrixserverlib.ServerName, receipt *shared.Receipt, eduType string) error CleanPDUs(ctx context.Context, serverName gomatrixserverlib.ServerName, receipts []*shared.Receipt) error CleanEDUs(ctx context.Context, serverName gomatrixserverlib.ServerName, receipts []*shared.Receipt) error diff --git a/federationapi/storage/postgres/storage.go b/federationapi/storage/postgres/storage.go index ac1489757..cd8d45c6c 100644 --- a/federationapi/storage/postgres/storage.go +++ b/federationapi/storage/postgres/storage.go @@ -24,6 +24,7 @@ import ( "github.com/matrix-org/dendrite/federationapi/storage/shared" "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) @@ -36,13 +37,12 @@ type Database struct { } // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (*Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (*Database, error) { var d Database var err error - if d.db, err = sqlutil.Open(dbProperties); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil { return nil, err } - d.writer = sqlutil.NewDummyWriter() joinedHosts, err := NewPostgresJoinedHostsTable(d.db) if err != nil { return nil, err diff --git a/federationapi/storage/shared/storage.go b/federationapi/storage/shared/storage.go index 160c7f6fa..a00d782f1 100644 --- a/federationapi/storage/shared/storage.go +++ b/federationapi/storage/shared/storage.go @@ -63,11 +63,21 @@ func (r *Receipt) String() string { // this isn't a duplicate message. func (d *Database) UpdateRoom( ctx context.Context, - roomID, oldEventID, newEventID string, + roomID string, addHosts []types.JoinedHost, removeHosts []string, + purgeRoomFirst bool, ) (joinedHosts []types.JoinedHost, err error) { err = d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { + if purgeRoomFirst { + // If the event is a create event then we'll delete all of the existing + // data for the room. The only reason that a create event would be replayed + // to us in this way is if we're about to receive the entire room state. + if err = d.FederationJoinedHosts.DeleteJoinedHostsForRoom(ctx, txn, roomID); err != nil { + return fmt.Errorf("d.FederationJoinedHosts.DeleteJoinedHosts: %w", err) + } + } + joinedHosts, err = d.FederationJoinedHosts.SelectJoinedHostsWithTx(ctx, txn, roomID) if err != nil { return err @@ -138,20 +148,6 @@ func (d *Database) StoreJSON( }, nil } -func (d *Database) PurgeRoomState( - ctx context.Context, roomID string, -) error { - return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { - // If the event is a create event then we'll delete all of the existing - // data for the room. The only reason that a create event would be replayed - // to us in this way is if we're about to receive the entire room state. - if err := d.FederationJoinedHosts.DeleteJoinedHostsForRoom(ctx, txn, roomID); err != nil { - return fmt.Errorf("d.FederationJoinedHosts.DeleteJoinedHosts: %w", err) - } - return nil - }) -} - func (d *Database) AddServerToBlacklist(serverName gomatrixserverlib.ServerName) error { return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { return d.FederationBlacklist.InsertBlacklist(context.TODO(), txn, serverName) diff --git a/federationapi/storage/shared/storage_edus.go b/federationapi/storage/shared/storage_edus.go index 6e3c7e367..02a23338f 100644 --- a/federationapi/storage/shared/storage_edus.go +++ b/federationapi/storage/shared/storage_edus.go @@ -31,12 +31,13 @@ func (d *Database) AssociateEDUWithDestination( ctx context.Context, serverName gomatrixserverlib.ServerName, receipt *Receipt, + eduType string, ) error { return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { if err := d.FederationQueueEDUs.InsertQueueEDU( ctx, // context txn, // SQL transaction - "", // TODO: EDU type for coalescing + eduType, // EDU type for coalescing serverName, // destination server name receipt.nid, // NID from the federationapi_queue_json table ); err != nil { diff --git a/federationapi/storage/sqlite3/storage.go b/federationapi/storage/sqlite3/storage.go index f6c30f0c2..d63e15efe 100644 --- a/federationapi/storage/sqlite3/storage.go +++ b/federationapi/storage/sqlite3/storage.go @@ -23,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/federationapi/storage/sqlite3/deltas" "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) @@ -35,13 +36,12 @@ type Database struct { } // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (*Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (*Database, error) { var d Database var err error - if d.db, err = sqlutil.Open(dbProperties); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil { return nil, err } - d.writer = sqlutil.NewExclusiveWriter() joinedHosts, err := NewSQLiteJoinedHostsTable(d.db) if err != nil { return nil, err diff --git a/federationapi/storage/storage.go b/federationapi/storage/storage.go index 4b52ca206..f246b9bc9 100644 --- a/federationapi/storage/storage.go +++ b/federationapi/storage/storage.go @@ -23,17 +23,18 @@ import ( "github.com/matrix-org/dendrite/federationapi/storage/postgres" "github.com/matrix-org/dendrite/federationapi/storage/sqlite3" "github.com/matrix-org/dendrite/internal/caching" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties, cache, serverName) + return sqlite3.NewDatabase(base, dbProperties, cache, serverName) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties, cache, serverName) + return postgres.NewDatabase(base, dbProperties, cache, serverName) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/federationapi/storage/storage_wasm.go b/federationapi/storage/storage_wasm.go index 09abed63e..84d5a3a4c 100644 --- a/federationapi/storage/storage_wasm.go +++ b/federationapi/storage/storage_wasm.go @@ -19,15 +19,16 @@ import ( "github.com/matrix-org/dendrite/federationapi/storage/sqlite3" "github.com/matrix-org/dendrite/internal/caching" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) // NewDatabase opens a new database -func NewDatabase(dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.FederationCache, serverName gomatrixserverlib.ServerName) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties, cache, serverName) + return sqlite3.NewDatabase(base, dbProperties, cache, serverName) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/go.mod b/go.mod index c3a9046e9..3f86c3e24 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/matrix-org/dendrite -replace github.com/nats-io/nats-server/v2 => github.com/neilalexander/nats-server/v2 v2.8.1-0.20220419100629-2278c94774f9 +replace github.com/nats-io/nats-server/v2 => github.com/neilalexander/nats-server/v2 v2.8.3-0.20220513095553-73a9a246d34f replace github.com/nats-io/nats.go => github.com/neilalexander/nats.go v1.13.1-0.20220419101051-b262d9f0be1e @@ -25,11 +25,12 @@ require ( github.com/h2non/filetype v1.1.3 // indirect github.com/hashicorp/golang-lru v0.5.4 github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect + github.com/kardianos/minwinsvc v1.0.0 github.com/lib/pq v1.10.5 github.com/matrix-org/dugong v0.0.0-20210921133753-66e6b1c67e2e github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91 github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 - github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f + github.com/matrix-org/gomatrixserverlib v0.0.0-20220513103617-eee8fd528433 github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48 github.com/matrix-org/util v0.0.0-20200807132607-55161520e1d4 github.com/mattn/go-sqlite3 v1.14.10 @@ -45,17 +46,18 @@ require ( github.com/pkg/errors v0.9.1 github.com/prometheus/client_golang v1.12.1 github.com/sirupsen/logrus v1.8.1 - github.com/tidwall/gjson v1.14.0 + github.com/stretchr/testify v1.7.0 + github.com/tidwall/gjson v1.14.1 github.com/tidwall/sjson v1.2.4 github.com/uber/jaeger-client-go v2.30.0+incompatible github.com/uber/jaeger-lib v2.4.1+incompatible github.com/yggdrasil-network/yggdrasil-go v0.4.3 go.uber.org/atomic v1.9.0 - golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 + golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 golang.org/x/image v0.0.0-20220321031419-a8550c1d254a golang.org/x/mobile v0.0.0-20220407111146-e579adbbc4a2 golang.org/x/net v0.0.0-20220407224826-aac1ed45d8e3 - golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect + golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 gopkg.in/h2non/bimg.v1 v1.1.9 gopkg.in/yaml.v2 v2.4.0 diff --git a/go.sum b/go.sum index d8ac2df5d..ba6c2e741 100644 --- a/go.sum +++ b/go.sum @@ -721,6 +721,7 @@ github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/kardianos/minwinsvc v1.0.0 h1:+JfAi8IBJna0jY2dJGZqi7o15z13JelFIklJCAENALA= github.com/kardianos/minwinsvc v1.0.0/go.mod h1:Bgd0oc+D0Qo3bBytmNtyRKVlp85dAloLKhfxanPFFRc= github.com/kataras/golog v0.0.10/go.mod h1:yJ8YKCmyL+nWjERB90Qwn+bdyBZsaQwU3bTVFgkFIp8= github.com/kataras/iris/v12 v12.1.8/go.mod h1:LMYy4VlP67TQ3Zgriz8RE2h2kMZV2SgMYbq3UhfoFmE= @@ -794,8 +795,8 @@ github.com/matrix-org/go-sqlite3-js v0.0.0-20220419092513-28aa791a1c91/go.mod h1 github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26/go.mod h1:3fxX6gUjWyI/2Bt7J1OLhpCzOfO/bB3AiX0cJtEKud0= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 h1:ZtO5uywdd5dLDCud4r0r55eP4j9FuUNpl60Gmntcop4= github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16/go.mod h1:/gBX06Kw0exX1HrwmoBibFA98yBk/jxKpGVeyQbff+s= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f h1:MZrl4TgTnlaOn2Cu9gJCoJ3oyW5mT4/3QIZGgZXzKl4= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220408160933-cf558306b56f/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220513103617-eee8fd528433 h1:nwAlThHGPI2EAAJklXvgMcdhXF6ZiHp60+fmaYMoaDA= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220513103617-eee8fd528433/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48 h1:W0sjjC6yjskHX4mb0nk3p0fXAlbU5bAFUFeEtlrPASE= github.com/matrix-org/pinecone v0.0.0-20220408153826-2999ea29ed48/go.mod h1:ulJzsVOTssIVp1j/m5eI//4VpAGDkMt5NrRuAVX7wpc= github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7/go.mod h1:vVQlW/emklohkZnOPwD3LrZUBqdfsbiyO3p1lNV8F6U= @@ -888,8 +889,8 @@ github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uY github.com/ncw/swift v1.0.47/go.mod h1:23YIA4yWVnGwv2dQlN4bB7egfYX6YLn0Yo/S6zZO/ZM= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= -github.com/neilalexander/nats-server/v2 v2.8.1-0.20220419100629-2278c94774f9 h1:VGU5HYAwy8LRbSkrT+kCHvujVmwK8Aa/vc1O+eReTbM= -github.com/neilalexander/nats-server/v2 v2.8.1-0.20220419100629-2278c94774f9/go.mod h1:5vic7C58BFEVltiZhs7Kq81q2WcEPhJPsmNv1FOrdv0= +github.com/neilalexander/nats-server/v2 v2.8.3-0.20220513095553-73a9a246d34f h1:Fc+TjdV1mOy0oISSzfoxNWdTqjg7tN/Vdgf+B2cwvdo= +github.com/neilalexander/nats-server/v2 v2.8.3-0.20220513095553-73a9a246d34f/go.mod h1:vIdpKz3OG+DCg4q/xVPdXHoztEyKDWRtykQ4N7hd7C4= github.com/neilalexander/nats.go v1.13.1-0.20220419101051-b262d9f0be1e h1:kNIzIzj2OvnlreA+sTJ12nWJzTP3OSLNKDL/Iq9mF6Y= github.com/neilalexander/nats.go v1.13.1-0.20220419101051-b262d9f0be1e/go.mod h1:BPko4oXsySz4aSWeFgOHLZs3G4Jq4ZAyE6/zMCxRT6w= github.com/neilalexander/utp v0.1.1-0.20210727203401-54ae7b1cd5f9 h1:lrVQzBtkeQEGGYUHwSX1XPe1E5GL6U3KYCNe2G4bncQ= @@ -1127,8 +1128,8 @@ github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tchap/go-patricia v2.2.6+incompatible/go.mod h1:bmLyhP68RS6kStMGxByiQ23RP/odRBOTVjwp2cDyi6I= github.com/tidwall/gjson v1.12.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w= -github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo= +github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= @@ -1277,9 +1278,9 @@ golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o= -golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122 h1:NvGWuYG8dkDHFSKksI1P9faiVJ9rayE6l0+ouWVIDs8= +golang.org/x/crypto v0.0.0-20220507011949-2cf3adece122/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -1540,8 +1541,8 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM= -golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/caching/cache_lazy_load_members.go b/internal/caching/cache_lazy_load_members.go index 71a317624..f0d495065 100644 --- a/internal/caching/cache_lazy_load_members.go +++ b/internal/caching/cache_lazy_load_members.go @@ -15,33 +15,14 @@ const ( LazyLoadCacheMaxAge = time.Minute * 30 ) -type LazyLoadCache struct { - // InMemoryLRUCachePartition containing other InMemoryLRUCachePartitions - // with the actual cached members - userCaches *InMemoryLRUCachePartition +type LazyLoadCache interface { + StoreLazyLoadedUser(device *userapi.Device, roomID, userID, eventID string) + IsLazyLoadedUserCached(device *userapi.Device, roomID, userID string) (string, bool) } -// NewLazyLoadCache creates a new LazyLoadCache. -func NewLazyLoadCache() (*LazyLoadCache, error) { - cache, err := NewInMemoryLRUCachePartition( - LazyLoadCacheName, - LazyLoadCacheMutable, - LazyLoadCacheMaxEntries, - LazyLoadCacheMaxAge, - true, - ) - if err != nil { - return nil, err - } - go cacheCleaner(cache) - return &LazyLoadCache{ - userCaches: cache, - }, nil -} - -func (c *LazyLoadCache) lazyLoadCacheForUser(device *userapi.Device) (*InMemoryLRUCachePartition, error) { +func (c Caches) lazyLoadCacheForUser(device *userapi.Device) (*InMemoryLRUCachePartition, error) { cacheName := fmt.Sprintf("%s/%s", device.UserID, device.ID) - userCache, ok := c.userCaches.Get(cacheName) + userCache, ok := c.LazyLoading.Get(cacheName) if ok && userCache != nil { if cache, ok := userCache.(*InMemoryLRUCachePartition); ok { return cache, nil @@ -57,12 +38,12 @@ func (c *LazyLoadCache) lazyLoadCacheForUser(device *userapi.Device) (*InMemoryL if err != nil { return nil, err } - c.userCaches.Set(cacheName, cache) + c.LazyLoading.Set(cacheName, cache) go cacheCleaner(cache) return cache, nil } -func (c *LazyLoadCache) StoreLazyLoadedUser(device *userapi.Device, roomID, userID, eventID string) { +func (c Caches) StoreLazyLoadedUser(device *userapi.Device, roomID, userID, eventID string) { cache, err := c.lazyLoadCacheForUser(device) if err != nil { return @@ -71,7 +52,7 @@ func (c *LazyLoadCache) StoreLazyLoadedUser(device *userapi.Device, roomID, user cache.Set(cacheKey, eventID) } -func (c *LazyLoadCache) IsLazyLoadedUserCached(device *userapi.Device, roomID, userID string) (string, bool) { +func (c Caches) IsLazyLoadedUserCached(device *userapi.Device, roomID, userID string) (string, bool) { cache, err := c.lazyLoadCacheForUser(device) if err != nil { return "", false diff --git a/internal/caching/cache_typing_test.go b/internal/caching/cache_typing_test.go index c03d89bc3..2cef32d3e 100644 --- a/internal/caching/cache_typing_test.go +++ b/internal/caching/cache_typing_test.go @@ -20,7 +20,7 @@ import ( "testing" "time" - "github.com/matrix-org/dendrite/internal/test" + "github.com/matrix-org/dendrite/test" ) func TestEDUCache(t *testing.T) { diff --git a/internal/caching/caches.go b/internal/caching/caches.go index 722405de6..173e47e5b 100644 --- a/internal/caching/caches.go +++ b/internal/caching/caches.go @@ -1,6 +1,8 @@ package caching -import "time" +import ( + "time" +) // Caches contains a set of references to caches. They may be // different implementations as long as they satisfy the Cache @@ -13,6 +15,7 @@ type Caches struct { RoomInfos Cache // RoomInfoCache FederationEvents Cache // FederationEventsCache SpaceSummaryRooms Cache // SpaceSummaryRoomsCache + LazyLoading Cache // LazyLoadCache } // Cache is the interface that an implementation must satisfy. diff --git a/internal/caching/impl_inmemorylru.go b/internal/caching/impl_inmemorylru.go index 94fdd1a9b..594760892 100644 --- a/internal/caching/impl_inmemorylru.go +++ b/internal/caching/impl_inmemorylru.go @@ -70,9 +70,21 @@ func NewInMemoryLRUCache(enablePrometheus bool) (*Caches, error) { if err != nil { return nil, err } + + lazyLoadCache, err := NewInMemoryLRUCachePartition( + LazyLoadCacheName, + LazyLoadCacheMutable, + LazyLoadCacheMaxEntries, + LazyLoadCacheMaxAge, + enablePrometheus, + ) + if err != nil { + return nil, err + } + go cacheCleaner( roomVersions, serverKeys, roomServerRoomIDs, - roomInfos, federationEvents, spaceRooms, + roomInfos, federationEvents, spaceRooms, lazyLoadCache, ) return &Caches{ RoomVersions: roomVersions, @@ -81,6 +93,7 @@ func NewInMemoryLRUCache(enablePrometheus bool) (*Caches, error) { RoomInfos: roomInfos, FederationEvents: federationEvents, SpaceSummaryRooms: spaceRooms, + LazyLoading: lazyLoadCache, }, nil } diff --git a/internal/eventutil/events.go b/internal/eventutil/events.go index 47c83d515..ee67a6daf 100644 --- a/internal/eventutil/events.go +++ b/internal/eventutil/events.go @@ -39,7 +39,7 @@ var ErrRoomNoExists = errors.New("room does not exist") func QueryAndBuildEvent( ctx context.Context, builder *gomatrixserverlib.EventBuilder, cfg *config.Global, evTime time.Time, - rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, + rsAPI api.QueryLatestEventsAndStateAPI, queryRes *api.QueryLatestEventsAndStateResponse, ) (*gomatrixserverlib.HeaderedEvent, error) { if queryRes == nil { queryRes = &api.QueryLatestEventsAndStateResponse{} @@ -80,7 +80,7 @@ func BuildEvent( func queryRequiredEventsForBuilder( ctx context.Context, builder *gomatrixserverlib.EventBuilder, - rsAPI api.RoomserverInternalAPI, queryRes *api.QueryLatestEventsAndStateResponse, + rsAPI api.QueryLatestEventsAndStateAPI, queryRes *api.QueryLatestEventsAndStateResponse, ) (*gomatrixserverlib.StateNeeded, error) { eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) if err != nil { diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go index 5fcacd2ad..aba50ae4d 100644 --- a/internal/httputil/httpapi.go +++ b/internal/httputil/httpapi.go @@ -15,7 +15,6 @@ package httputil import ( - "context" "fmt" "io" "net/http" @@ -23,15 +22,10 @@ import ( "net/http/httputil" "os" "strings" - "sync" - "time" "github.com/getsentry/sentry-go" - "github.com/gorilla/mux" "github.com/matrix-org/dendrite/clientapi/auth" - federationapiAPI "github.com/matrix-org/dendrite/federationapi/api" userapi "github.com/matrix-org/dendrite/userapi/api" - "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" opentracing "github.com/opentracing/opentracing-go" "github.com/opentracing/opentracing-go/ext" @@ -49,7 +43,7 @@ type BasicAuth struct { // MakeAuthAPI turns a util.JSONRequestHandler function into an http.Handler which authenticates the request. func MakeAuthAPI( - metricsName string, userAPI userapi.UserInternalAPI, + metricsName string, userAPI userapi.QueryAcccessTokenAPI, f func(*http.Request, *userapi.Device) util.JSONResponse, ) http.Handler { h := func(req *http.Request) util.JSONResponse { @@ -226,79 +220,6 @@ func MakeInternalAPI(metricsName string, f func(*http.Request) util.JSONResponse ) } -// MakeFedAPI makes an http.Handler that checks matrix federation authentication. -func MakeFedAPI( - metricsName string, - serverName gomatrixserverlib.ServerName, - keyRing gomatrixserverlib.JSONVerifier, - wakeup *FederationWakeups, - f func(*http.Request, *gomatrixserverlib.FederationRequest, map[string]string) util.JSONResponse, -) http.Handler { - h := func(req *http.Request) util.JSONResponse { - fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest( - req, time.Now(), serverName, keyRing, - ) - if fedReq == nil { - return errResp - } - // add the user to Sentry, if enabled - hub := sentry.GetHubFromContext(req.Context()) - if hub != nil { - hub.Scope().SetTag("origin", string(fedReq.Origin())) - hub.Scope().SetTag("uri", fedReq.RequestURI()) - } - defer func() { - if r := recover(); r != nil { - if hub != nil { - hub.CaptureException(fmt.Errorf("%s panicked", req.URL.Path)) - } - // re-panic to return the 500 - panic(r) - } - }() - go wakeup.Wakeup(req.Context(), fedReq.Origin()) - vars, err := URLDecodeMapValues(mux.Vars(req)) - if err != nil { - return util.MatrixErrorResponse(400, "M_UNRECOGNISED", "badly encoded query params") - } - - jsonRes := f(req, fedReq, vars) - // do not log 4xx as errors as they are client fails, not server fails - if hub != nil && jsonRes.Code >= 500 { - hub.Scope().SetExtra("response", jsonRes) - hub.CaptureException(fmt.Errorf("%s returned HTTP %d", req.URL.Path, jsonRes.Code)) - } - return jsonRes - } - return MakeExternalAPI(metricsName, h) -} - -type FederationWakeups struct { - FsAPI federationapiAPI.FederationInternalAPI - origins sync.Map -} - -func (f *FederationWakeups) Wakeup(ctx context.Context, origin gomatrixserverlib.ServerName) { - key, keyok := f.origins.Load(origin) - if keyok { - lastTime, ok := key.(time.Time) - if ok && time.Since(lastTime) < time.Minute { - return - } - } - aliveReq := federationapiAPI.PerformServersAliveRequest{ - Servers: []gomatrixserverlib.ServerName{origin}, - } - aliveRes := federationapiAPI.PerformServersAliveResponse{} - if err := f.FsAPI.PerformServersAlive(ctx, &aliveReq, &aliveRes); err != nil { - util.GetLogger(ctx).WithError(err).WithFields(logrus.Fields{ - "origin": origin, - }).Warn("incoming federation request failed to notify server alive") - } else { - f.origins.Store(origin, time.Now()) - } -} - // WrapHandlerInBasicAuth adds basic auth to a handler. Only used for /metrics func WrapHandlerInBasicAuth(h http.Handler, b BasicAuth) http.HandlerFunc { if b.Username == "" || b.Password == "" { diff --git a/internal/pushgateway/client.go b/internal/pushgateway/client.go index 49907cee8..95f5afd90 100644 --- a/internal/pushgateway/client.go +++ b/internal/pushgateway/client.go @@ -25,6 +25,7 @@ func NewHTTPClient(disableTLSValidation bool) Client { TLSClientConfig: &tls.Config{ InsecureSkipVerify: disableTLSValidation, }, + Proxy: http.ProxyFromEnvironment, }, } return &httpClient{hc: hc} diff --git a/internal/sqlutil/sqlutil.go b/internal/sqlutil/sqlutil.go new file mode 100644 index 000000000..0cdae6d30 --- /dev/null +++ b/internal/sqlutil/sqlutil.go @@ -0,0 +1,51 @@ +package sqlutil + +import ( + "database/sql" + "fmt" + "regexp" + + "github.com/matrix-org/dendrite/setup/config" + "github.com/sirupsen/logrus" +) + +// Open opens a database specified by its database driver name and a driver-specific data source name, +// usually consisting of at least a database name and connection information. Includes tracing driver +// if DENDRITE_TRACE_SQL=1 +func Open(dbProperties *config.DatabaseOptions, writer Writer) (*sql.DB, error) { + var err error + var driverName, dsn string + switch { + case dbProperties.ConnectionString.IsSQLite(): + driverName = "sqlite3" + dsn, err = ParseFileURI(dbProperties.ConnectionString) + if err != nil { + return nil, fmt.Errorf("ParseFileURI: %w", err) + } + case dbProperties.ConnectionString.IsPostgres(): + driverName = "postgres" + dsn = string(dbProperties.ConnectionString) + default: + return nil, fmt.Errorf("invalid database connection string %q", dbProperties.ConnectionString) + } + if tracingEnabled { + // install the wrapped driver + driverName += "-trace" + } + db, err := sql.Open(driverName, dsn) + if err != nil { + return nil, err + } + if driverName != "sqlite3" { + logrus.WithFields(logrus.Fields{ + "MaxOpenConns": dbProperties.MaxOpenConns(), + "MaxIdleConns": dbProperties.MaxIdleConns(), + "ConnMaxLifetime": dbProperties.ConnMaxLifetime(), + "dataSourceName": regexp.MustCompile(`://[^@]*@`).ReplaceAllLiteralString(dsn, "://"), + }).Debug("Setting DB connection limits") + db.SetMaxOpenConns(dbProperties.MaxOpenConns()) + db.SetMaxIdleConns(dbProperties.MaxIdleConns()) + db.SetConnMaxLifetime(dbProperties.ConnMaxLifetime()) + } + return db, nil +} diff --git a/internal/sqlutil/trace.go b/internal/sqlutil/trace.go index 51eaa1b45..c16738616 100644 --- a/internal/sqlutil/trace.go +++ b/internal/sqlutil/trace.go @@ -16,19 +16,16 @@ package sqlutil import ( "context" - "database/sql" "database/sql/driver" "fmt" "io" "os" - "regexp" "runtime" "strconv" "strings" "sync" "time" - "github.com/matrix-org/dendrite/setup/config" "github.com/ngrok/sqlmw" "github.com/sirupsen/logrus" ) @@ -96,47 +93,6 @@ func trackGoID(query string) { logrus.Warnf("unsafe goid %d: SQL executed not on an ExclusiveWriter: %s", thisGoID, q) } -// Open opens a database specified by its database driver name and a driver-specific data source name, -// usually consisting of at least a database name and connection information. Includes tracing driver -// if DENDRITE_TRACE_SQL=1 -func Open(dbProperties *config.DatabaseOptions) (*sql.DB, error) { - var err error - var driverName, dsn string - switch { - case dbProperties.ConnectionString.IsSQLite(): - driverName = "sqlite3" - dsn, err = ParseFileURI(dbProperties.ConnectionString) - if err != nil { - return nil, fmt.Errorf("ParseFileURI: %w", err) - } - case dbProperties.ConnectionString.IsPostgres(): - driverName = "postgres" - dsn = string(dbProperties.ConnectionString) - default: - return nil, fmt.Errorf("invalid database connection string %q", dbProperties.ConnectionString) - } - if tracingEnabled { - // install the wrapped driver - driverName += "-trace" - } - db, err := sql.Open(driverName, dsn) - if err != nil { - return nil, err - } - if driverName != "sqlite3" { - logrus.WithFields(logrus.Fields{ - "MaxOpenConns": dbProperties.MaxOpenConns(), - "MaxIdleConns": dbProperties.MaxIdleConns(), - "ConnMaxLifetime": dbProperties.ConnMaxLifetime(), - "dataSourceName": regexp.MustCompile(`://[^@]*@`).ReplaceAllLiteralString(dsn, "://"), - }).Debug("Setting DB connection limits") - db.SetMaxOpenConns(dbProperties.MaxOpenConns()) - db.SetMaxIdleConns(dbProperties.MaxIdleConns()) - db.SetConnMaxLifetime(dbProperties.ConnMaxLifetime()) - } - return db, nil -} - func init() { registerDrivers() } diff --git a/internal/test/client.go b/internal/test/client.go deleted file mode 100644 index a38540ac9..000000000 --- a/internal/test/client.go +++ /dev/null @@ -1,158 +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 test - -import ( - "crypto/tls" - "fmt" - "io" - "io/ioutil" - "net/http" - "sync" - "time" - - "github.com/matrix-org/gomatrixserverlib" -) - -// Request contains the information necessary to issue a request and test its result -type Request struct { - Req *http.Request - WantedBody string - WantedStatusCode int - LastErr *LastRequestErr -} - -// LastRequestErr is a synchronised error wrapper -// Useful for obtaining the last error from a set of requests -type LastRequestErr struct { - sync.Mutex - Err error -} - -// Set sets the error -func (r *LastRequestErr) Set(err error) { - r.Lock() - defer r.Unlock() - r.Err = err -} - -// Get gets the error -func (r *LastRequestErr) Get() error { - r.Lock() - defer r.Unlock() - return r.Err -} - -// CanonicalJSONInput canonicalises a slice of JSON strings -// Useful for test input -func CanonicalJSONInput(jsonData []string) []string { - for i := range jsonData { - jsonBytes, err := gomatrixserverlib.CanonicalJSON([]byte(jsonData[i])) - if err != nil && err != io.EOF { - panic(err) - } - jsonData[i] = string(jsonBytes) - } - return jsonData -} - -// Do issues a request and checks the status code and body of the response -func (r *Request) Do() (err error) { - client := &http.Client{ - Timeout: 5 * time.Second, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: true, - }, - }, - } - res, err := client.Do(r.Req) - if err != nil { - return err - } - defer (func() { err = res.Body.Close() })() - - if res.StatusCode != r.WantedStatusCode { - return fmt.Errorf("incorrect status code. Expected: %d Got: %d", r.WantedStatusCode, res.StatusCode) - } - - if r.WantedBody != "" { - resBytes, err := ioutil.ReadAll(res.Body) - if err != nil { - return err - } - jsonBytes, err := gomatrixserverlib.CanonicalJSON(resBytes) - if err != nil { - return err - } - if string(jsonBytes) != r.WantedBody { - return fmt.Errorf("returned wrong bytes. Expected:\n%s\n\nGot:\n%s", r.WantedBody, string(jsonBytes)) - } - } - - return nil -} - -// DoUntilSuccess blocks and repeats the same request until the response returns the desired status code and body. -// It then closes the given channel and returns. -func (r *Request) DoUntilSuccess(done chan error) { - r.LastErr = &LastRequestErr{} - for { - if err := r.Do(); err != nil { - r.LastErr.Set(err) - time.Sleep(1 * time.Second) // don't tightloop - continue - } - close(done) - return - } -} - -// Run repeatedly issues a request until success, error or a timeout is reached -func (r *Request) Run(label string, timeout time.Duration, serverCmdChan chan error) { - fmt.Printf("==TESTING== %v (timeout: %v)\n", label, timeout) - done := make(chan error, 1) - - // We need to wait for the server to: - // - have connected to the database - // - have created the tables - // - be listening on the given port - go r.DoUntilSuccess(done) - - // wait for one of: - // - the test to pass (done channel is closed) - // - the server to exit with an error (error sent on serverCmdChan) - // - our test timeout to expire - // We don't need to clean up since the main() function handles that in the event we panic - select { - case <-time.After(timeout): - fmt.Printf("==TESTING== %v TIMEOUT\n", label) - if reqErr := r.LastErr.Get(); reqErr != nil { - fmt.Println("Last /sync request error:") - fmt.Println(reqErr) - } - panic(fmt.Sprintf("%v server timed out", label)) - case err := <-serverCmdChan: - if err != nil { - fmt.Println("=============================================================================================") - fmt.Printf("%v server failed to run. If failing with 'pq: password authentication failed for user' try:", label) - fmt.Println(" export PGHOST=/var/run/postgresql") - fmt.Println("=============================================================================================") - panic(err) - } - case <-done: - fmt.Printf("==TESTING== %v PASSED\n", label) - } -} diff --git a/internal/test/kafka.go b/internal/test/kafka.go deleted file mode 100644 index cbf246304..000000000 --- a/internal/test/kafka.go +++ /dev/null @@ -1,76 +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 test - -import ( - "io" - "os/exec" - "path/filepath" - "strings" -) - -// KafkaExecutor executes kafka scripts. -type KafkaExecutor struct { - // The location of Zookeeper. Typically this is `localhost:2181`. - ZookeeperURI string - // The directory where Kafka is installed to. Used to locate kafka scripts. - KafkaDirectory string - // The location of the Kafka logs. Typically this is `localhost:9092`. - KafkaURI string - // Where stdout and stderr should be written to. Typically this is `os.Stderr`. - OutputWriter io.Writer -} - -// CreateTopic creates a new kafka topic. This is created with a single partition. -func (e *KafkaExecutor) CreateTopic(topic string) error { - cmd := exec.Command( - filepath.Join(e.KafkaDirectory, "bin", "kafka-topics.sh"), - "--create", - "--zookeeper", e.ZookeeperURI, - "--replication-factor", "1", - "--partitions", "1", - "--topic", topic, - ) - cmd.Stdout = e.OutputWriter - cmd.Stderr = e.OutputWriter - return cmd.Run() -} - -// WriteToTopic writes data to a kafka topic. -func (e *KafkaExecutor) WriteToTopic(topic string, data []string) error { - cmd := exec.Command( - filepath.Join(e.KafkaDirectory, "bin", "kafka-console-producer.sh"), - "--broker-list", e.KafkaURI, - "--topic", topic, - ) - cmd.Stdout = e.OutputWriter - cmd.Stderr = e.OutputWriter - cmd.Stdin = strings.NewReader(strings.Join(data, "\n")) - return cmd.Run() -} - -// DeleteTopic deletes a given kafka topic if it exists. -func (e *KafkaExecutor) DeleteTopic(topic string) error { - cmd := exec.Command( - filepath.Join(e.KafkaDirectory, "bin", "kafka-topics.sh"), - "--delete", - "--if-exists", - "--zookeeper", e.ZookeeperURI, - "--topic", topic, - ) - cmd.Stderr = e.OutputWriter - cmd.Stdout = e.OutputWriter - return cmd.Run() -} diff --git a/internal/test/server.go b/internal/test/server.go deleted file mode 100644 index ca14ea1bf..000000000 --- a/internal/test/server.go +++ /dev/null @@ -1,152 +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 test - -import ( - "context" - "fmt" - "net" - "net/http" - "os" - "os/exec" - "path/filepath" - "strings" - "sync" - "testing" - - "github.com/matrix-org/dendrite/setup/config" -) - -// Defaulting allows assignment of string variables with a fallback default value -// Useful for use with os.Getenv() for example -func Defaulting(value, defaultValue string) string { - if value == "" { - value = defaultValue - } - return value -} - -// CreateDatabase creates a new database, dropping it first if it exists -func CreateDatabase(command string, args []string, database string) error { - cmd := exec.Command(command, args...) - 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() -} - -// CreateBackgroundCommand creates an executable command -// 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 CreateBackgroundCommand(command string, args []string) (*exec.Cmd, chan error) { - cmd := exec.Command(command, args...) - cmd.Stderr = os.Stderr - cmd.Stdout = os.Stderr - - if err := cmd.Start(); err != nil { - panic("failed to start server: " + err.Error()) - } - cmdChan := make(chan error, 1) - go func() { - cmdChan <- cmd.Wait() - close(cmdChan) - }() - return cmd, cmdChan -} - -// InitDatabase creates the database and config file needed for the server to run -func InitDatabase(postgresDatabase, postgresContainerName string, databases []string) { - if len(databases) > 0 { - var dbCmd string - var dbArgs []string - if postgresContainerName == "" { - dbCmd = "psql" - dbArgs = []string{postgresDatabase} - } else { - dbCmd = "docker" - dbArgs = []string{ - "exec", "-i", postgresContainerName, "psql", "-U", "postgres", postgresDatabase, - } - } - for _, database := range databases { - if err := CreateDatabase(dbCmd, dbArgs, database); err != nil { - panic(err) - } - } - } -} - -// StartProxy creates a reverse proxy -func StartProxy(bindAddr string, cfg *config.Dendrite) (*exec.Cmd, chan error) { - proxyArgs := []string{ - "--bind-address", bindAddr, - "--sync-api-server-url", "http://" + string(cfg.SyncAPI.InternalAPI.Connect), - "--client-api-server-url", "http://" + string(cfg.ClientAPI.InternalAPI.Connect), - "--media-api-server-url", "http://" + string(cfg.MediaAPI.InternalAPI.Connect), - "--tls-cert", "server.crt", - "--tls-key", "server.key", - } - return CreateBackgroundCommand( - filepath.Join(filepath.Dir(os.Args[0]), "client-api-proxy"), - proxyArgs, - ) -} - -// ListenAndServe will listen on a random high-numbered port and attach the given router. -// Returns the base URL to send requests to. Call `cancel` to shutdown the server, which will block until it has closed. -func ListenAndServe(t *testing.T, router http.Handler, useTLS bool) (apiURL string, cancel func()) { - listener, err := net.Listen("tcp", ":0") - if err != nil { - t.Fatalf("failed to listen: %s", err) - } - port := listener.Addr().(*net.TCPAddr).Port - srv := http.Server{} - - var wg sync.WaitGroup - wg.Add(1) - go func() { - defer wg.Done() - srv.Handler = router - var err error - if useTLS { - certFile := filepath.Join(os.TempDir(), "dendrite.cert") - keyFile := filepath.Join(os.TempDir(), "dendrite.key") - err = NewTLSKey(keyFile, certFile) - if err != nil { - t.Logf("failed to generate tls key/cert: %s", err) - return - } - err = srv.ServeTLS(listener, certFile, keyFile) - } else { - err = srv.Serve(listener) - } - if err != nil && err != http.ErrServerClosed { - t.Logf("Listen failed: %s", err) - } - }() - - secure := "" - if useTLS { - secure = "s" - } - return fmt.Sprintf("http%s://localhost:%d", secure, port), func() { - _ = srv.Shutdown(context.Background()) - wg.Wait() - } -} diff --git a/internal/version.go b/internal/version.go index 5227a03bf..04c9a8a88 100644 --- a/internal/version.go +++ b/internal/version.go @@ -17,7 +17,7 @@ var build string const ( VersionMajor = 0 VersionMinor = 8 - VersionPatch = 1 + VersionPatch = 5 VersionTag = "" // example: "rc1" ) diff --git a/keyserver/api/api.go b/keyserver/api/api.go index 429617b10..140f03569 100644 --- a/keyserver/api/api.go +++ b/keyserver/api/api.go @@ -27,21 +27,45 @@ import ( ) type KeyInternalAPI interface { + SyncKeyAPI + ClientKeyAPI + FederationKeyAPI + UserKeyAPI + // SetUserAPI assigns a user API to query when extracting device names. - SetUserAPI(i userapi.UserInternalAPI) - // InputDeviceListUpdate from a federated server EDU - InputDeviceListUpdate(ctx context.Context, req *InputDeviceListUpdateRequest, res *InputDeviceListUpdateResponse) + SetUserAPI(i userapi.KeyserverUserAPI) +} + +// API functions required by the clientapi +type ClientKeyAPI interface { + QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) PerformUploadKeys(ctx context.Context, req *PerformUploadKeysRequest, res *PerformUploadKeysResponse) - // PerformClaimKeys claims one-time keys for use in pre-key messages - PerformClaimKeys(ctx context.Context, req *PerformClaimKeysRequest, res *PerformClaimKeysResponse) - PerformDeleteKeys(ctx context.Context, req *PerformDeleteKeysRequest, res *PerformDeleteKeysResponse) PerformUploadDeviceKeys(ctx context.Context, req *PerformUploadDeviceKeysRequest, res *PerformUploadDeviceKeysResponse) PerformUploadDeviceSignatures(ctx context.Context, req *PerformUploadDeviceSignaturesRequest, res *PerformUploadDeviceSignaturesResponse) - QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) + // PerformClaimKeys claims one-time keys for use in pre-key messages + PerformClaimKeys(ctx context.Context, req *PerformClaimKeysRequest, res *PerformClaimKeysResponse) +} + +// API functions required by the userapi +type UserKeyAPI interface { + PerformUploadKeys(ctx context.Context, req *PerformUploadKeysRequest, res *PerformUploadKeysResponse) + PerformDeleteKeys(ctx context.Context, req *PerformDeleteKeysRequest, res *PerformDeleteKeysResponse) +} + +// API functions required by the syncapi +type SyncKeyAPI interface { QueryKeyChanges(ctx context.Context, req *QueryKeyChangesRequest, res *QueryKeyChangesResponse) QueryOneTimeKeys(ctx context.Context, req *QueryOneTimeKeysRequest, res *QueryOneTimeKeysResponse) - QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) +} + +type FederationKeyAPI interface { + QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) + QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) + // InputDeviceListUpdate from a federated server EDU + InputDeviceListUpdate(ctx context.Context, req *InputDeviceListUpdateRequest, res *InputDeviceListUpdateResponse) + PerformUploadDeviceKeys(ctx context.Context, req *PerformUploadDeviceKeysRequest, res *PerformUploadDeviceKeysResponse) + PerformClaimKeys(ctx context.Context, req *PerformClaimKeysRequest, res *PerformClaimKeysResponse) } // KeyError is returned if there was a problem performing/querying the server diff --git a/keyserver/internal/cross_signing.go b/keyserver/internal/cross_signing.go index 0d083b4ba..08bbfedb8 100644 --- a/keyserver/internal/cross_signing.go +++ b/keyserver/internal/cross_signing.go @@ -362,6 +362,13 @@ func (a *KeyInternalAPI) processSelfSignatures( for targetKeyID, signature := range forTargetUserID { switch sig := signature.CrossSigningBody.(type) { case *gomatrixserverlib.CrossSigningKey: + for keyID := range sig.Keys { + split := strings.SplitN(string(keyID), ":", 2) + if len(split) > 1 && gomatrixserverlib.KeyID(split[1]) == targetKeyID { + targetKeyID = keyID // contains the ed25519: or other scheme + break + } + } for originUserID, forOriginUserID := range sig.Signatures { for originKeyID, originSig := range forOriginUserID { if err := a.DB.StoreCrossSigningSigsForTarget( @@ -455,10 +462,10 @@ func (a *KeyInternalAPI) processOtherSignatures( func (a *KeyInternalAPI) crossSigningKeysFromDatabase( ctx context.Context, req *api.QueryKeysRequest, res *api.QueryKeysResponse, ) { - for userID := range req.UserToDevices { - keys, err := a.DB.CrossSigningKeysForUser(ctx, userID) + for targetUserID := range req.UserToDevices { + keys, err := a.DB.CrossSigningKeysForUser(ctx, targetUserID) if err != nil { - logrus.WithError(err).Errorf("Failed to get cross-signing keys for user %q", userID) + logrus.WithError(err).Errorf("Failed to get cross-signing keys for user %q", targetUserID) continue } @@ -469,9 +476,9 @@ func (a *KeyInternalAPI) crossSigningKeysFromDatabase( break } - sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, userID, keyID) + sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, req.UserID, targetUserID, keyID) if err != nil && err != sql.ErrNoRows { - logrus.WithError(err).Errorf("Failed to get cross-signing signatures for user %q key %q", userID, keyID) + logrus.WithError(err).Errorf("Failed to get cross-signing signatures for user %q key %q", targetUserID, keyID) continue } @@ -491,7 +498,7 @@ func (a *KeyInternalAPI) crossSigningKeysFromDatabase( case req.UserID != "" && originUserID == req.UserID: // Include signatures that we created appendSignature(originUserID, originKeyID, signature) - case originUserID == userID: + case originUserID == targetUserID: // Include signatures that were created by the person whose key // we are processing appendSignature(originUserID, originKeyID, signature) @@ -501,13 +508,13 @@ func (a *KeyInternalAPI) crossSigningKeysFromDatabase( switch keyType { case gomatrixserverlib.CrossSigningKeyPurposeMaster: - res.MasterKeys[userID] = key + res.MasterKeys[targetUserID] = key case gomatrixserverlib.CrossSigningKeyPurposeSelfSigning: - res.SelfSigningKeys[userID] = key + res.SelfSigningKeys[targetUserID] = key case gomatrixserverlib.CrossSigningKeyPurposeUserSigning: - res.UserSigningKeys[userID] = key + res.UserSigningKeys[targetUserID] = key } } } @@ -546,7 +553,8 @@ func (a *KeyInternalAPI) QuerySignatures(ctx context.Context, req *api.QuerySign } for _, targetKeyID := range forTargetUser { - sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, targetUserID, targetKeyID) + // Get own signatures only. + sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, targetUserID, targetUserID, targetKeyID) if err != nil && err != sql.ErrNoRows { res.Error = &api.KeyError{ Err: fmt.Sprintf("a.DB.CrossSigningSigsForTarget: %s", err), diff --git a/keyserver/internal/device_list_update.go b/keyserver/internal/device_list_update.go index 561c9a163..23f3e1a67 100644 --- a/keyserver/internal/device_list_update.go +++ b/keyserver/internal/device_list_update.go @@ -84,7 +84,7 @@ type DeviceListUpdater struct { db DeviceListUpdaterDatabase api DeviceListUpdaterAPI producer KeyChangeProducer - fedClient fedsenderapi.FederationClient + fedClient fedsenderapi.KeyserverFederationAPI workerChans []chan gomatrixserverlib.ServerName // When device lists are stale for a user, they get inserted into this map with a channel which `Update` will @@ -127,7 +127,7 @@ type KeyChangeProducer interface { // NewDeviceListUpdater creates a new updater which fetches fresh device lists when they go stale. func NewDeviceListUpdater( db DeviceListUpdaterDatabase, api DeviceListUpdaterAPI, producer KeyChangeProducer, - fedClient fedsenderapi.FederationClient, numWorkers int, + fedClient fedsenderapi.KeyserverFederationAPI, numWorkers int, ) *DeviceListUpdater { return &DeviceListUpdater{ userIDToMutex: make(map[string]*sync.Mutex), diff --git a/keyserver/internal/internal.go b/keyserver/internal/internal.go index a05476f5f..f8d0d69c3 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -37,13 +37,13 @@ import ( type KeyInternalAPI struct { DB storage.Database ThisServer gomatrixserverlib.ServerName - FedClient fedsenderapi.FederationClient - UserAPI userapi.UserInternalAPI + FedClient fedsenderapi.KeyserverFederationAPI + UserAPI userapi.KeyserverUserAPI Producer *producers.KeyChange Updater *DeviceListUpdater } -func (a *KeyInternalAPI) SetUserAPI(i userapi.UserInternalAPI) { +func (a *KeyInternalAPI) SetUserAPI(i userapi.KeyserverUserAPI) { a.UserAPI = i } @@ -71,8 +71,12 @@ func (a *KeyInternalAPI) QueryKeyChanges(ctx context.Context, req *api.QueryKeyC func (a *KeyInternalAPI) PerformUploadKeys(ctx context.Context, req *api.PerformUploadKeysRequest, res *api.PerformUploadKeysResponse) { res.KeyErrors = make(map[string]map[string]*api.KeyError) - a.uploadLocalDeviceKeys(ctx, req, res) - a.uploadOneTimeKeys(ctx, req, res) + if len(req.DeviceKeys) > 0 { + a.uploadLocalDeviceKeys(ctx, req, res) + } + if len(req.OneTimeKeys) > 0 { + a.uploadOneTimeKeys(ctx, req, res) + } } func (a *KeyInternalAPI) PerformClaimKeys(ctx context.Context, req *api.PerformClaimKeysRequest, res *api.PerformClaimKeysResponse) { @@ -313,9 +317,34 @@ func (a *KeyInternalAPI) QueryKeys(ctx context.Context, req *api.QueryKeysReques // Finally, append signatures that we know about // TODO: This is horrible because we need to round-trip the signature from // JSON, add the signatures and marshal it again, for some reason? - for userID, forUserID := range res.DeviceKeys { - for keyID, key := range forUserID { - sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, userID, gomatrixserverlib.KeyID(keyID)) + + for targetUserID, masterKey := range res.MasterKeys { + if masterKey.Signatures == nil { + masterKey.Signatures = map[string]map[gomatrixserverlib.KeyID]gomatrixserverlib.Base64Bytes{} + } + for targetKeyID := range masterKey.Keys { + sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, req.UserID, targetUserID, targetKeyID) + if err != nil { + logrus.WithError(err).Errorf("a.DB.CrossSigningSigsForTarget failed") + continue + } + if len(sigMap) == 0 { + continue + } + for sourceUserID, forSourceUser := range sigMap { + for sourceKeyID, sourceSig := range forSourceUser { + if _, ok := masterKey.Signatures[sourceUserID]; !ok { + masterKey.Signatures[sourceUserID] = map[gomatrixserverlib.KeyID]gomatrixserverlib.Base64Bytes{} + } + masterKey.Signatures[sourceUserID][sourceKeyID] = sourceSig + } + } + } + } + + for targetUserID, forUserID := range res.DeviceKeys { + for targetKeyID, key := range forUserID { + sigMap, err := a.DB.CrossSigningSigsForTarget(ctx, req.UserID, targetUserID, gomatrixserverlib.KeyID(targetKeyID)) if err != nil { logrus.WithError(err).Errorf("a.DB.CrossSigningSigsForTarget failed") continue @@ -339,7 +368,7 @@ func (a *KeyInternalAPI) QueryKeys(ctx context.Context, req *api.QueryKeysReques } } if js, err := json.Marshal(deviceKey); err == nil { - res.DeviceKeys[userID][keyID] = js + res.DeviceKeys[targetUserID][targetKeyID] = js } } } @@ -603,44 +632,57 @@ func (a *KeyInternalAPI) uploadLocalDeviceKeys(ctx context.Context, req *api.Per } var keysToStore []api.DeviceMessage - // assert that the user ID / device ID are not lying for each key - for _, key := range req.DeviceKeys { - var serverName gomatrixserverlib.ServerName - _, serverName, err = gomatrixserverlib.SplitID('@', key.UserID) - if err != nil { - continue // ignore invalid users - } - if serverName != a.ThisServer { - continue // ignore remote users - } - if len(key.KeyJSON) == 0 { - keysToStore = append(keysToStore, key.WithStreamID(0)) - continue // deleted keys don't need sanity checking - } - // check that the device in question actually exists in the user - // API before we try and store a key for it - if _, ok := existingDeviceMap[key.DeviceID]; !ok { - continue - } - gotUserID := gjson.GetBytes(key.KeyJSON, "user_id").Str - gotDeviceID := gjson.GetBytes(key.KeyJSON, "device_id").Str - if gotUserID == key.UserID && gotDeviceID == key.DeviceID { - keysToStore = append(keysToStore, key.WithStreamID(0)) - continue - } - - res.KeyError(key.UserID, key.DeviceID, &api.KeyError{ - Err: fmt.Sprintf( - "user_id or device_id mismatch: users: %s - %s, devices: %s - %s", - gotUserID, key.UserID, gotDeviceID, key.DeviceID, - ), - }) - } if req.OnlyDisplayNameUpdates { - // add the display name field from keysToStore into existingKeys - keysToStore = appendDisplayNames(existingKeys, keysToStore) + for _, existingKey := range existingKeys { + for _, newKey := range req.DeviceKeys { + switch { + case existingKey.UserID != newKey.UserID: + continue + case existingKey.DeviceID != newKey.DeviceID: + continue + case existingKey.DisplayName != newKey.DisplayName: + existingKey.DisplayName = newKey.DisplayName + } + } + keysToStore = append(keysToStore, existingKey) + } + } else { + // assert that the user ID / device ID are not lying for each key + for _, key := range req.DeviceKeys { + var serverName gomatrixserverlib.ServerName + _, serverName, err = gomatrixserverlib.SplitID('@', key.UserID) + if err != nil { + continue // ignore invalid users + } + if serverName != a.ThisServer { + continue // ignore remote users + } + if len(key.KeyJSON) == 0 { + keysToStore = append(keysToStore, key.WithStreamID(0)) + continue // deleted keys don't need sanity checking + } + // check that the device in question actually exists in the user + // API before we try and store a key for it + if _, ok := existingDeviceMap[key.DeviceID]; !ok { + continue + } + gotUserID := gjson.GetBytes(key.KeyJSON, "user_id").Str + gotDeviceID := gjson.GetBytes(key.KeyJSON, "device_id").Str + if gotUserID == key.UserID && gotDeviceID == key.DeviceID { + keysToStore = append(keysToStore, key.WithStreamID(0)) + continue + } + + res.KeyError(key.UserID, key.DeviceID, &api.KeyError{ + Err: fmt.Sprintf( + "user_id or device_id mismatch: users: %s - %s, devices: %s - %s", + gotUserID, key.UserID, gotDeviceID, key.DeviceID, + ), + }) + } } + // store the device keys and emit changes err = a.DB.StoreLocalDeviceKeys(ctx, keysToStore) if err != nil { @@ -734,16 +776,3 @@ func emitDeviceKeyChanges(producer KeyChangeProducer, existing, new []api.Device } return producer.ProduceKeyChanges(keysAdded) } - -func appendDisplayNames(existing, new []api.DeviceMessage) []api.DeviceMessage { - for i, existingDevice := range existing { - for _, newDevice := range new { - if existingDevice.DeviceID != newDevice.DeviceID { - continue - } - existingDevice.DisplayName = newDevice.DisplayName - existing[i] = existingDevice - } - } - return existing -} diff --git a/keyserver/inthttp/client.go b/keyserver/inthttp/client.go index f50789b82..abce81582 100644 --- a/keyserver/inthttp/client.go +++ b/keyserver/inthttp/client.go @@ -60,7 +60,7 @@ type httpKeyInternalAPI struct { httpClient *http.Client } -func (h *httpKeyInternalAPI) SetUserAPI(i userapi.UserInternalAPI) { +func (h *httpKeyInternalAPI) SetUserAPI(i userapi.KeyserverUserAPI) { // no-op: doesn't need it } func (h *httpKeyInternalAPI) InputDeviceListUpdate( diff --git a/keyserver/keyserver.go b/keyserver/keyserver.go index c557dfbaa..3ffd3ba1e 100644 --- a/keyserver/keyserver.go +++ b/keyserver/keyserver.go @@ -37,11 +37,11 @@ func AddInternalRoutes(router *mux.Router, intAPI api.KeyInternalAPI) { // NewInternalAPI returns a concerete implementation of the internal API. Callers // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( - base *base.BaseDendrite, cfg *config.KeyServer, fedClient fedsenderapi.FederationClient, + base *base.BaseDendrite, cfg *config.KeyServer, fedClient fedsenderapi.KeyserverFederationAPI, ) api.KeyInternalAPI { - js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) - db, err := storage.NewDatabase(&cfg.Database) + db, err := storage.NewDatabase(base, &cfg.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to key server database") } diff --git a/keyserver/storage/interface.go b/keyserver/storage/interface.go index 16e034776..242e16a06 100644 --- a/keyserver/storage/interface.go +++ b/keyserver/storage/interface.go @@ -81,7 +81,7 @@ type Database interface { CrossSigningKeysForUser(ctx context.Context, userID string) (map[gomatrixserverlib.CrossSigningKeyPurpose]gomatrixserverlib.CrossSigningKey, error) CrossSigningKeysDataForUser(ctx context.Context, userID string) (types.CrossSigningKeyMap, error) - CrossSigningSigsForTarget(ctx context.Context, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) + CrossSigningSigsForTarget(ctx context.Context, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) StoreCrossSigningKeysForUser(ctx context.Context, userID string, keyMap types.CrossSigningKeyMap) error StoreCrossSigningSigsForTarget(ctx context.Context, originUserID string, originKeyID gomatrixserverlib.KeyID, targetUserID string, targetKeyID gomatrixserverlib.KeyID, signature gomatrixserverlib.Base64Bytes) error diff --git a/keyserver/storage/postgres/cross_signing_sigs_table.go b/keyserver/storage/postgres/cross_signing_sigs_table.go index e11853957..5ab1a46f5 100644 --- a/keyserver/storage/postgres/cross_signing_sigs_table.go +++ b/keyserver/storage/postgres/cross_signing_sigs_table.go @@ -21,6 +21,7 @@ import ( "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/keyserver/storage/postgres/deltas" "github.com/matrix-org/dendrite/keyserver/storage/tables" "github.com/matrix-org/dendrite/keyserver/types" "github.com/matrix-org/gomatrixserverlib" @@ -33,18 +34,20 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs ( target_user_id TEXT NOT NULL, target_key_id TEXT NOT NULL, signature TEXT NOT NULL, - PRIMARY KEY (origin_user_id, target_user_id, target_key_id) + PRIMARY KEY (origin_user_id, origin_key_id, target_user_id, target_key_id) ); + +CREATE INDEX IF NOT EXISTS keyserver_cross_signing_sigs_idx ON keyserver_cross_signing_sigs (origin_user_id, target_user_id, target_key_id); ` const selectCrossSigningSigsForTargetSQL = "" + "SELECT origin_user_id, origin_key_id, signature FROM keyserver_cross_signing_sigs" + - " WHERE target_user_id = $1 AND target_key_id = $2" + " WHERE (origin_user_id = $1 OR origin_user_id = target_user_id) AND target_user_id = $2 AND target_key_id = $3" const upsertCrossSigningSigsForTargetSQL = "" + "INSERT INTO keyserver_cross_signing_sigs (origin_user_id, origin_key_id, target_user_id, target_key_id, signature)" + " VALUES($1, $2, $3, $4, $5)" + - " ON CONFLICT (origin_user_id, target_user_id, target_key_id) DO UPDATE SET (origin_key_id, signature) = ($2, $5)" + " ON CONFLICT (origin_user_id, origin_key_id, target_user_id, target_key_id) DO UPDATE SET signature = $5" const deleteCrossSigningSigsForTargetSQL = "" + "DELETE FROM keyserver_cross_signing_sigs WHERE target_user_id=$1 AND target_key_id=$2" @@ -64,6 +67,16 @@ func NewPostgresCrossSigningSigsTable(db *sql.DB) (tables.CrossSigningSigs, erro if err != nil { return nil, err } + + m := sqlutil.NewMigrator(db) + m.AddMigrations(sqlutil.Migration{ + Version: "", + Up: deltas.UpFixCrossSigningSignatureIndexes, + }) + if err = m.Up(context.Background()); err != nil { + return nil, err + } + return s, sqlutil.StatementList{ {&s.selectCrossSigningSigsForTargetStmt, selectCrossSigningSigsForTargetSQL}, {&s.upsertCrossSigningSigsForTargetStmt, upsertCrossSigningSigsForTargetSQL}, @@ -72,9 +85,9 @@ func NewPostgresCrossSigningSigsTable(db *sql.DB) (tables.CrossSigningSigs, erro } func (s *crossSigningSigsStatements) SelectCrossSigningSigsForTarget( - ctx context.Context, txn *sql.Tx, targetUserID string, targetKeyID gomatrixserverlib.KeyID, + ctx context.Context, txn *sql.Tx, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID, ) (r types.CrossSigningSigMap, err error) { - rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningSigsForTargetStmt).QueryContext(ctx, targetUserID, targetKeyID) + rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningSigsForTargetStmt).QueryContext(ctx, originUserID, targetUserID, targetKeyID) if err != nil { return nil, err } diff --git a/keyserver/storage/postgres/deltas/2022042612000000_xsigning_idx.go b/keyserver/storage/postgres/deltas/2022042612000000_xsigning_idx.go new file mode 100644 index 000000000..cb6f69766 --- /dev/null +++ b/keyserver/storage/postgres/deltas/2022042612000000_xsigning_idx.go @@ -0,0 +1,47 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deltas + +import ( + "context" + "database/sql" + "fmt" +) + +func UpFixCrossSigningSignatureIndexes(ctx context.Context, tx *sql.Tx) error { + _, err := tx.Exec(` + ALTER TABLE keyserver_cross_signing_sigs DROP CONSTRAINT keyserver_cross_signing_sigs_pkey; + ALTER TABLE keyserver_cross_signing_sigs ADD PRIMARY KEY (origin_user_id, origin_key_id, target_user_id, target_key_id); + + CREATE INDEX IF NOT EXISTS keyserver_cross_signing_sigs_idx ON keyserver_cross_signing_sigs (origin_user_id, target_user_id, target_key_id); + `) + if err != nil { + return fmt.Errorf("failed to execute upgrade: %w", err) + } + return nil +} + +func DownFixCrossSigningSignatureIndexes(ctx context.Context, tx *sql.Tx) error { + _, err := tx.Exec(` + ALTER TABLE keyserver_cross_signing_sigs DROP CONSTRAINT keyserver_cross_signing_sigs_pkey; + ALTER TABLE keyserver_cross_signing_sigs ADD PRIMARY KEY (origin_user_id, target_user_id, target_key_id); + + DROP INDEX IF EXISTS keyserver_cross_signing_sigs_idx; + `) + if err != nil { + return fmt.Errorf("failed to execute downgrade: %w", err) + } + return nil +} diff --git a/keyserver/storage/postgres/one_time_keys_table.go b/keyserver/storage/postgres/one_time_keys_table.go index 0b143a1aa..2117efcae 100644 --- a/keyserver/storage/postgres/one_time_keys_table.go +++ b/keyserver/storage/postgres/one_time_keys_table.go @@ -39,6 +39,8 @@ CREATE TABLE IF NOT EXISTS keyserver_one_time_keys ( -- Clobber based on 4-uple of user/device/key/algorithm. CONSTRAINT keyserver_one_time_keys_unique UNIQUE (user_id, device_id, key_id, algorithm) ); + +CREATE INDEX IF NOT EXISTS keyserver_one_time_keys_idx ON keyserver_one_time_keys (user_id, device_id); ` const upsertKeysSQL = "" + @@ -51,7 +53,9 @@ const selectKeysSQL = "" + "SELECT concat(algorithm, ':', key_id) as algorithmwithid, key_json FROM keyserver_one_time_keys WHERE user_id=$1 AND device_id=$2 AND concat(algorithm, ':', key_id) = ANY($3);" const selectKeysCountSQL = "" + - "SELECT algorithm, COUNT(key_id) FROM keyserver_one_time_keys WHERE user_id=$1 AND device_id=$2 GROUP BY algorithm" + "SELECT algorithm, COUNT(key_id) FROM " + + " (SELECT algorithm, key_id FROM keyserver_one_time_keys WHERE user_id = $1 AND device_id = $2 LIMIT 100)" + + " x GROUP BY algorithm" const deleteOneTimeKeySQL = "" + "DELETE FROM keyserver_one_time_keys WHERE user_id = $1 AND device_id = $2 AND algorithm = $3 AND key_id = $4" diff --git a/keyserver/storage/postgres/storage.go b/keyserver/storage/postgres/storage.go index 41b8c7b46..35e630559 100644 --- a/keyserver/storage/postgres/storage.go +++ b/keyserver/storage/postgres/storage.go @@ -17,13 +17,14 @@ package postgres import ( "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/keyserver/storage/shared" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewDatabase creates a new sync server database -func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) { var err error - db, err := sqlutil.Open(dbProperties) + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()) if err != nil { return nil, err } @@ -56,7 +57,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) } d := &shared.Database{ DB: db, - Writer: sqlutil.NewDummyWriter(), + Writer: writer, OneTimeKeysTable: otk, DeviceKeysTable: dk, KeyChangesTable: kc, diff --git a/keyserver/storage/shared/storage.go b/keyserver/storage/shared/storage.go index 7ba0b3ea1..0e587b5a8 100644 --- a/keyserver/storage/shared/storage.go +++ b/keyserver/storage/shared/storage.go @@ -190,7 +190,7 @@ func (d *Database) CrossSigningKeysForUser(ctx context.Context, userID string) ( keyID: key, }, } - sigMap, err := d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, userID, keyID) + sigMap, err := d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, userID, userID, keyID) if err != nil { continue } @@ -219,8 +219,8 @@ func (d *Database) CrossSigningKeysDataForUser(ctx context.Context, userID strin } // CrossSigningSigsForTarget returns the signatures for a given user's key ID, if any. -func (d *Database) CrossSigningSigsForTarget(ctx context.Context, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) { - return d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, targetUserID, targetKeyID) +func (d *Database) CrossSigningSigsForTarget(ctx context.Context, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (types.CrossSigningSigMap, error) { + return d.CrossSigningSigsTable.SelectCrossSigningSigsForTarget(ctx, nil, originUserID, targetUserID, targetKeyID) } // StoreCrossSigningKeysForUser stores the latest known cross-signing keys for a user. diff --git a/keyserver/storage/sqlite3/cross_signing_sigs_table.go b/keyserver/storage/sqlite3/cross_signing_sigs_table.go index 9abf54363..f129b9149 100644 --- a/keyserver/storage/sqlite3/cross_signing_sigs_table.go +++ b/keyserver/storage/sqlite3/cross_signing_sigs_table.go @@ -21,6 +21,7 @@ import ( "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/keyserver/storage/sqlite3/deltas" "github.com/matrix-org/dendrite/keyserver/storage/tables" "github.com/matrix-org/dendrite/keyserver/types" "github.com/matrix-org/gomatrixserverlib" @@ -33,13 +34,15 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs ( target_user_id TEXT NOT NULL, target_key_id TEXT NOT NULL, signature TEXT NOT NULL, - PRIMARY KEY (origin_user_id, target_user_id, target_key_id) + PRIMARY KEY (origin_user_id, origin_key_id, target_user_id, target_key_id) ); + +CREATE INDEX IF NOT EXISTS keyserver_cross_signing_sigs_idx ON keyserver_cross_signing_sigs (origin_user_id, target_user_id, target_key_id); ` const selectCrossSigningSigsForTargetSQL = "" + "SELECT origin_user_id, origin_key_id, signature FROM keyserver_cross_signing_sigs" + - " WHERE target_user_id = $1 AND target_key_id = $2" + " WHERE (origin_user_id = $1 OR origin_user_id = target_user_id) AND target_user_id = $2 AND target_key_id = $3" const upsertCrossSigningSigsForTargetSQL = "" + "INSERT OR REPLACE INTO keyserver_cross_signing_sigs (origin_user_id, origin_key_id, target_user_id, target_key_id, signature)" + @@ -63,6 +66,15 @@ func NewSqliteCrossSigningSigsTable(db *sql.DB) (tables.CrossSigningSigs, error) if err != nil { return nil, err } + m := sqlutil.NewMigrator(db) + m.AddMigrations(sqlutil.Migration{ + Version: "", + Up: deltas.UpFixCrossSigningSignatureIndexes, + }) + if err = m.Up(context.Background()); err != nil { + return nil, err + } + return s, sqlutil.StatementList{ {&s.selectCrossSigningSigsForTargetStmt, selectCrossSigningSigsForTargetSQL}, {&s.upsertCrossSigningSigsForTargetStmt, upsertCrossSigningSigsForTargetSQL}, @@ -71,13 +83,13 @@ func NewSqliteCrossSigningSigsTable(db *sql.DB) (tables.CrossSigningSigs, error) } func (s *crossSigningSigsStatements) SelectCrossSigningSigsForTarget( - ctx context.Context, txn *sql.Tx, targetUserID string, targetKeyID gomatrixserverlib.KeyID, + ctx context.Context, txn *sql.Tx, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID, ) (r types.CrossSigningSigMap, err error) { - rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningSigsForTargetStmt).QueryContext(ctx, targetUserID, targetKeyID) + rows, err := sqlutil.TxStmt(txn, s.selectCrossSigningSigsForTargetStmt).QueryContext(ctx, originUserID, targetUserID, targetKeyID) if err != nil { return nil, err } - defer internal.CloseAndLogIfError(ctx, rows, "selectCrossSigningSigsForTargetStmt: rows.close() failed") + defer internal.CloseAndLogIfError(ctx, rows, "selectCrossSigningSigsForOriginTargetStmt: rows.close() failed") r = types.CrossSigningSigMap{} for rows.Next() { var userID string diff --git a/keyserver/storage/sqlite3/deltas/2022042612000000_xsigning_idx.go b/keyserver/storage/sqlite3/deltas/2022042612000000_xsigning_idx.go new file mode 100644 index 000000000..d4e38dea5 --- /dev/null +++ b/keyserver/storage/sqlite3/deltas/2022042612000000_xsigning_idx.go @@ -0,0 +1,71 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package deltas + +import ( + "context" + "database/sql" + "fmt" +) + +func UpFixCrossSigningSignatureIndexes(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs_tmp ( + origin_user_id TEXT NOT NULL, + origin_key_id TEXT NOT NULL, + target_user_id TEXT NOT NULL, + target_key_id TEXT NOT NULL, + signature TEXT NOT NULL, + PRIMARY KEY (origin_user_id, origin_key_id, target_user_id, target_key_id) + ); + + INSERT INTO keyserver_cross_signing_sigs_tmp (origin_user_id, origin_key_id, target_user_id, target_key_id, signature) + SELECT origin_user_id, origin_key_id, target_user_id, target_key_id, signature FROM keyserver_cross_signing_sigs; + + DROP TABLE keyserver_cross_signing_sigs; + ALTER TABLE keyserver_cross_signing_sigs_tmp RENAME TO keyserver_cross_signing_sigs; + + CREATE INDEX IF NOT EXISTS keyserver_cross_signing_sigs_idx ON keyserver_cross_signing_sigs (origin_user_id, target_user_id, target_key_id); + `) + if err != nil { + return fmt.Errorf("failed to execute upgrade: %w", err) + } + return nil +} + +func DownFixCrossSigningSignatureIndexes(ctx context.Context, tx *sql.Tx) error { + _, err := tx.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs_tmp ( + origin_user_id TEXT NOT NULL, + origin_key_id TEXT NOT NULL, + target_user_id TEXT NOT NULL, + target_key_id TEXT NOT NULL, + signature TEXT NOT NULL, + PRIMARY KEY (origin_user_id, target_user_id, target_key_id) + ); + + INSERT INTO keyserver_cross_signing_sigs_tmp (origin_user_id, origin_key_id, target_user_id, target_key_id, signature) + SELECT origin_user_id, origin_key_id, target_user_id, target_key_id, signature FROM keyserver_cross_signing_sigs; + + DROP TABLE keyserver_cross_signing_sigs; + ALTER TABLE keyserver_cross_signing_sigs_tmp RENAME TO keyserver_cross_signing_sigs; + + DELETE INDEX IF EXISTS keyserver_cross_signing_sigs_idx; + `) + if err != nil { + return fmt.Errorf("failed to execute downgrade: %w", err) + } + return nil +} diff --git a/keyserver/storage/sqlite3/one_time_keys_table.go b/keyserver/storage/sqlite3/one_time_keys_table.go index 897839aca..7a923d0e5 100644 --- a/keyserver/storage/sqlite3/one_time_keys_table.go +++ b/keyserver/storage/sqlite3/one_time_keys_table.go @@ -38,6 +38,8 @@ CREATE TABLE IF NOT EXISTS keyserver_one_time_keys ( -- Clobber based on 4-uple of user/device/key/algorithm. UNIQUE (user_id, device_id, key_id, algorithm) ); + +CREATE INDEX IF NOT EXISTS keyserver_one_time_keys_idx ON keyserver_one_time_keys (user_id, device_id); ` const upsertKeysSQL = "" + @@ -50,7 +52,9 @@ const selectKeysSQL = "" + "SELECT key_id, algorithm, key_json FROM keyserver_one_time_keys WHERE user_id=$1 AND device_id=$2" const selectKeysCountSQL = "" + - "SELECT algorithm, COUNT(key_id) FROM keyserver_one_time_keys WHERE user_id=$1 AND device_id=$2 GROUP BY algorithm" + "SELECT algorithm, COUNT(key_id) FROM " + + " (SELECT algorithm, key_id FROM keyserver_one_time_keys WHERE user_id = $1 AND device_id = $2 LIMIT 100)" + + " x GROUP BY algorithm" const deleteOneTimeKeySQL = "" + "DELETE FROM keyserver_one_time_keys WHERE user_id = $1 AND device_id = $2 AND algorithm = $3 AND key_id = $4" diff --git a/keyserver/storage/sqlite3/storage.go b/keyserver/storage/sqlite3/storage.go index 4f2f1b56d..873fe3e24 100644 --- a/keyserver/storage/sqlite3/storage.go +++ b/keyserver/storage/sqlite3/storage.go @@ -17,11 +17,12 @@ package sqlite3 import ( "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/keyserver/storage/shared" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) -func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) { - db, err := sqlutil.Open(dbProperties) +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()) if err != nil { return nil, err } @@ -55,7 +56,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) } d := &shared.Database{ DB: db, - Writer: sqlutil.NewExclusiveWriter(), + Writer: writer, OneTimeKeysTable: otk, DeviceKeysTable: dk, KeyChangesTable: kc, diff --git a/keyserver/storage/storage.go b/keyserver/storage/storage.go index 742e8463a..ab6a35401 100644 --- a/keyserver/storage/storage.go +++ b/keyserver/storage/storage.go @@ -22,17 +22,18 @@ import ( "github.com/matrix-org/dendrite/keyserver/storage/postgres" "github.com/matrix-org/dendrite/keyserver/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme) // and sets postgres connection parameters -func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties) + return postgres.NewDatabase(base, dbProperties) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/keyserver/storage/storage_test.go b/keyserver/storage/storage_test.go index 84d2098ad..9940eac60 100644 --- a/keyserver/storage/storage_test.go +++ b/keyserver/storage/storage_test.go @@ -22,7 +22,7 @@ func MustCreateDatabase(t *testing.T) (Database, func()) { log.Fatal(err) } t.Logf("Database %s", tmpfile.Name()) - db, err := NewDatabase(&config.DatabaseOptions{ + db, err := NewDatabase(nil, &config.DatabaseOptions{ ConnectionString: config.DataSource(fmt.Sprintf("file://%s", tmpfile.Name())), }) if err != nil { diff --git a/keyserver/storage/storage_wasm.go b/keyserver/storage/storage_wasm.go index 8b31bfd01..75c9053e8 100644 --- a/keyserver/storage/storage_wasm.go +++ b/keyserver/storage/storage_wasm.go @@ -18,13 +18,14 @@ import ( "fmt" "github.com/matrix-org/dendrite/keyserver/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) -func NewDatabase(dbProperties *config.DatabaseOptions) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/keyserver/storage/tables/interface.go b/keyserver/storage/tables/interface.go index f840cd1f3..37a010a7c 100644 --- a/keyserver/storage/tables/interface.go +++ b/keyserver/storage/tables/interface.go @@ -64,7 +64,7 @@ type CrossSigningKeys interface { } type CrossSigningSigs interface { - SelectCrossSigningSigsForTarget(ctx context.Context, txn *sql.Tx, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (r types.CrossSigningSigMap, err error) + SelectCrossSigningSigsForTarget(ctx context.Context, txn *sql.Tx, originUserID, targetUserID string, targetKeyID gomatrixserverlib.KeyID) (r types.CrossSigningSigMap, err error) UpsertCrossSigningSigsForTarget(ctx context.Context, txn *sql.Tx, originUserID string, originKeyID gomatrixserverlib.KeyID, targetUserID string, targetKeyID gomatrixserverlib.KeyID, signature gomatrixserverlib.Base64Bytes) error DeleteCrossSigningSigsForTarget(ctx context.Context, txn *sql.Tx, targetUserID string, targetKeyID gomatrixserverlib.KeyID) error } diff --git a/mediaapi/mediaapi.go b/mediaapi/mediaapi.go index e5daf480d..4792c996d 100644 --- a/mediaapi/mediaapi.go +++ b/mediaapi/mediaapi.go @@ -15,10 +15,9 @@ package mediaapi import ( - "github.com/gorilla/mux" "github.com/matrix-org/dendrite/mediaapi/routing" "github.com/matrix-org/dendrite/mediaapi/storage" - "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/setup/base" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/sirupsen/logrus" @@ -26,18 +25,19 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component. func AddPublicRoutes( - router *mux.Router, - cfg *config.MediaAPI, - rateLimit *config.RateLimiting, - userAPI userapi.UserInternalAPI, + base *base.BaseDendrite, + userAPI userapi.MediaUserAPI, client *gomatrixserverlib.Client, ) { - mediaDB, err := storage.NewMediaAPIDatasource(&cfg.Database) + cfg := &base.Cfg.MediaAPI + rateCfg := &base.Cfg.ClientAPI.RateLimiting + + mediaDB, err := storage.NewMediaAPIDatasource(base, &cfg.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to media db") } routing.Setup( - router, cfg, rateLimit, mediaDB, userAPI, client, + base.PublicMediaAPIMux, cfg, rateCfg, mediaDB, userAPI, client, ) } diff --git a/mediaapi/routing/download.go b/mediaapi/routing/download.go index 5f22a9461..10b25a5cd 100644 --- a/mediaapi/routing/download.go +++ b/mediaapi/routing/download.go @@ -551,7 +551,7 @@ func (r *downloadRequest) getRemoteFile( // If we do not have a record, we need to fetch the remote file first and then respond from the local file err := r.fetchRemoteFileAndStoreMetadata( ctx, client, - cfg.AbsBasePath, *cfg.MaxFileSizeBytes, db, + cfg.AbsBasePath, cfg.MaxFileSizeBytes, db, cfg.ThumbnailSizes, activeThumbnailGeneration, cfg.MaxThumbnailGenerators, ) diff --git a/mediaapi/routing/routing.go b/mediaapi/routing/routing.go index 0e1583991..76f07415b 100644 --- a/mediaapi/routing/routing.go +++ b/mediaapi/routing/routing.go @@ -35,7 +35,7 @@ import ( // configResponse is the response to GET /_matrix/media/r0/config // https://matrix.org/docs/spec/client_server/latest#get-matrix-media-r0-config type configResponse struct { - UploadSize config.FileSizeBytes `json:"m.upload.size"` + UploadSize *config.FileSizeBytes `json:"m.upload.size"` } // Setup registers the media API HTTP handlers @@ -48,7 +48,7 @@ func Setup( cfg *config.MediaAPI, rateLimit *config.RateLimiting, db storage.Database, - userAPI userapi.UserInternalAPI, + userAPI userapi.MediaUserAPI, client *gomatrixserverlib.Client, ) { rateLimits := httputil.NewRateLimits(rateLimit) @@ -73,9 +73,13 @@ func Setup( if r := rateLimits.Limit(req); r != nil { return *r } + respondSize := &cfg.MaxFileSizeBytes + if cfg.MaxFileSizeBytes == 0 { + respondSize = nil + } return util.JSONResponse{ Code: http.StatusOK, - JSON: configResponse{UploadSize: *cfg.MaxFileSizeBytes}, + JSON: configResponse{UploadSize: respondSize}, } }) diff --git a/mediaapi/routing/upload.go b/mediaapi/routing/upload.go index 972c52af0..2175648ea 100644 --- a/mediaapi/routing/upload.go +++ b/mediaapi/routing/upload.go @@ -90,7 +90,7 @@ func parseAndValidateRequest(req *http.Request, cfg *config.MediaAPI, dev *usera Logger: util.GetLogger(req.Context()).WithField("Origin", cfg.Matrix.ServerName), } - if resErr := r.Validate(*cfg.MaxFileSizeBytes); resErr != nil { + if resErr := r.Validate(cfg.MaxFileSizeBytes); resErr != nil { return nil, resErr } @@ -148,20 +148,20 @@ func (r *uploadRequest) doUpload( // r.storeFileAndMetadata(ctx, tmpDir, ...) // before you return from doUpload else we will leak a temp file. We could make this nicer with a `WithTransaction` style of // nested function to guarantee either storage or cleanup. - if *cfg.MaxFileSizeBytes > 0 { - if *cfg.MaxFileSizeBytes+1 <= 0 { + if cfg.MaxFileSizeBytes > 0 { + if cfg.MaxFileSizeBytes+1 <= 0 { r.Logger.WithFields(log.Fields{ - "MaxFileSizeBytes": *cfg.MaxFileSizeBytes, + "MaxFileSizeBytes": cfg.MaxFileSizeBytes, }).Warnf("Configured MaxFileSizeBytes overflows int64, defaulting to %d bytes", config.DefaultMaxFileSizeBytes) - cfg.MaxFileSizeBytes = &config.DefaultMaxFileSizeBytes + cfg.MaxFileSizeBytes = config.DefaultMaxFileSizeBytes } - reqReader = io.LimitReader(reqReader, int64(*cfg.MaxFileSizeBytes)+1) + reqReader = io.LimitReader(reqReader, int64(cfg.MaxFileSizeBytes)+1) } hash, bytesWritten, tmpDir, err := fileutils.WriteTempFile(ctx, reqReader, cfg.AbsBasePath) if err != nil { r.Logger.WithError(err).WithFields(log.Fields{ - "MaxFileSizeBytes": *cfg.MaxFileSizeBytes, + "MaxFileSizeBytes": cfg.MaxFileSizeBytes, }).Warn("Error while transferring file") return &util.JSONResponse{ Code: http.StatusBadRequest, @@ -170,9 +170,9 @@ func (r *uploadRequest) doUpload( } // Check if temp file size exceeds max file size configuration - if *cfg.MaxFileSizeBytes > 0 && bytesWritten > types.FileSizeBytes(*cfg.MaxFileSizeBytes) { + if cfg.MaxFileSizeBytes > 0 && bytesWritten > types.FileSizeBytes(cfg.MaxFileSizeBytes) { fileutils.RemoveDir(tmpDir, r.Logger) // delete temp file - return requestEntityTooLargeJSONResponse(*cfg.MaxFileSizeBytes) + return requestEntityTooLargeJSONResponse(cfg.MaxFileSizeBytes) } // Look up the media by the file hash. If we already have the file but under a diff --git a/mediaapi/routing/upload_test.go b/mediaapi/routing/upload_test.go index b2c2f5a44..420d0eba9 100644 --- a/mediaapi/routing/upload_test.go +++ b/mediaapi/routing/upload_test.go @@ -36,12 +36,11 @@ func Test_uploadRequest_doUpload(t *testing.T) { } maxSize := config.FileSizeBytes(8) - unlimitedSize := config.FileSizeBytes(0) logger := log.New().WithField("mediaapi", "test") testdataPath := filepath.Join(wd, "./testdata") cfg := &config.MediaAPI{ - MaxFileSizeBytes: &maxSize, + MaxFileSizeBytes: maxSize, BasePath: config.Path(testdataPath), AbsBasePath: config.Path(testdataPath), DynamicThumbnails: false, @@ -51,7 +50,7 @@ func Test_uploadRequest_doUpload(t *testing.T) { _ = os.Mkdir(testdataPath, os.ModePerm) defer fileutils.RemoveDir(types.Path(testdataPath), nil) - db, err := storage.NewMediaAPIDatasource(&config.DatabaseOptions{ + db, err := storage.NewMediaAPIDatasource(nil, &config.DatabaseOptions{ ConnectionString: "file::memory:?cache=shared", MaxOpenConnections: 100, MaxIdleConnections: 2, @@ -124,7 +123,7 @@ func Test_uploadRequest_doUpload(t *testing.T) { ctx: context.Background(), reqReader: strings.NewReader("test test test"), cfg: &config.MediaAPI{ - MaxFileSizeBytes: &unlimitedSize, + MaxFileSizeBytes: config.FileSizeBytes(0), BasePath: config.Path(testdataPath), AbsBasePath: config.Path(testdataPath), DynamicThumbnails: false, diff --git a/mediaapi/storage/postgres/mediaapi.go b/mediaapi/storage/postgres/mediaapi.go index ea70e575b..30ec64f84 100644 --- a/mediaapi/storage/postgres/mediaapi.go +++ b/mediaapi/storage/postgres/mediaapi.go @@ -20,12 +20,13 @@ import ( _ "github.com/lib/pq" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/mediaapi/storage/shared" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewDatabase opens a postgres database. -func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) { - db, err := sqlutil.Open(dbProperties) +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()) if err != nil { return nil, err } @@ -41,6 +42,6 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) MediaRepository: mediaRepo, Thumbnails: thumbnails, DB: db, - Writer: sqlutil.NewExclusiveWriter(), + Writer: writer, }, nil } diff --git a/mediaapi/storage/sqlite3/mediaapi.go b/mediaapi/storage/sqlite3/mediaapi.go index abf329367..c0ab10e9f 100644 --- a/mediaapi/storage/sqlite3/mediaapi.go +++ b/mediaapi/storage/sqlite3/mediaapi.go @@ -19,12 +19,13 @@ import ( // Import the postgres database driver. "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/mediaapi/storage/shared" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewDatabase opens a SQLIte database. -func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) { - db, err := sqlutil.Open(dbProperties) +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*shared.Database, error) { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()) if err != nil { return nil, err } @@ -40,6 +41,6 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) MediaRepository: mediaRepo, Thumbnails: thumbnails, DB: db, - Writer: sqlutil.NewExclusiveWriter(), + Writer: writer, }, nil } diff --git a/mediaapi/storage/storage.go b/mediaapi/storage/storage.go index baa242e57..f673ae7e6 100644 --- a/mediaapi/storage/storage.go +++ b/mediaapi/storage/storage.go @@ -22,16 +22,17 @@ import ( "github.com/matrix-org/dendrite/mediaapi/storage/postgres" "github.com/matrix-org/dendrite/mediaapi/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewMediaAPIDatasource opens a database connection. -func NewMediaAPIDatasource(dbProperties *config.DatabaseOptions) (Database, error) { +func NewMediaAPIDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties) + return postgres.NewDatabase(base, dbProperties) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/mediaapi/storage/storage_test.go b/mediaapi/storage/storage_test.go index 8d3403045..81f0a5d24 100644 --- a/mediaapi/storage/storage_test.go +++ b/mediaapi/storage/storage_test.go @@ -13,7 +13,7 @@ import ( func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) { connStr, close := test.PrepareDBConnectionString(t, dbType) - db, err := storage.NewMediaAPIDatasource(&config.DatabaseOptions{ + db, err := storage.NewMediaAPIDatasource(nil, &config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), }) if err != nil { @@ -123,11 +123,19 @@ func TestThumbnailsStorage(t *testing.T) { t.Fatalf("expected %d stored thumbnail metadata, got %d", len(thumbnails), len(gotMediadatas)) } for i := range gotMediadatas { - if !reflect.DeepEqual(thumbnails[i].MediaMetadata, gotMediadatas[i].MediaMetadata) { - t.Fatalf("expected metadata %+v, got %v", thumbnails[i].MediaMetadata, gotMediadatas[i].MediaMetadata) + // metadata may be returned in a different order than it was stored, perform a search + metaDataMatches := func() bool { + for _, t := range thumbnails { + if reflect.DeepEqual(t.MediaMetadata, gotMediadatas[i].MediaMetadata) && reflect.DeepEqual(t.ThumbnailSize, gotMediadatas[i].ThumbnailSize) { + return true + } + } + return false } - if !reflect.DeepEqual(thumbnails[i].ThumbnailSize, gotMediadatas[i].ThumbnailSize) { - t.Fatalf("expected metadata %+v, got %v", thumbnails[i].ThumbnailSize, gotMediadatas[i].ThumbnailSize) + + if !metaDataMatches() { + t.Fatalf("expected metadata %+v, got %+v", thumbnails[i].MediaMetadata, gotMediadatas[i].MediaMetadata) + } } }) diff --git a/mediaapi/storage/storage_wasm.go b/mediaapi/storage/storage_wasm.go index f67f9d5e1..41e4a28c0 100644 --- a/mediaapi/storage/storage_wasm.go +++ b/mediaapi/storage/storage_wasm.go @@ -18,14 +18,15 @@ import ( "fmt" "github.com/matrix-org/dendrite/mediaapi/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // Open opens a postgres database. -func NewMediaAPIDatasource(dbProperties *config.DatabaseOptions) (Database, error) { +func NewMediaAPIDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/mediaapi/thumbnailer/thumbnailer_bimg.go b/mediaapi/thumbnailer/thumbnailer_bimg.go index 6ca533176..fa1acbf08 100644 --- a/mediaapi/thumbnailer/thumbnailer_bimg.go +++ b/mediaapi/thumbnailer/thumbnailer_bimg.go @@ -37,7 +37,7 @@ func GenerateThumbnails( mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, - db *storage.Database, + db storage.Database, logger *log.Entry, ) (busy bool, errorReturn error) { buffer, err := bimg.Read(string(src)) @@ -49,7 +49,7 @@ func GenerateThumbnails( for _, config := range configs { // Note: createThumbnail does locking based on activeThumbnailGeneration busy, err = createThumbnail( - ctx, src, img, config, mediaMetadata, activeThumbnailGeneration, + ctx, src, img, types.ThumbnailSize(config), mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger, ) if err != nil { @@ -71,7 +71,7 @@ func GenerateThumbnail( mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, - db *storage.Database, + db storage.Database, logger *log.Entry, ) (busy bool, errorReturn error) { buffer, err := bimg.Read(string(src)) @@ -109,7 +109,7 @@ func createThumbnail( mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, - db *storage.Database, + db storage.Database, logger *log.Entry, ) (busy bool, errorReturn error) { logger = logger.WithFields(log.Fields{ diff --git a/roomserver/api/alias.go b/roomserver/api/alias.go index baab27751..37892a44a 100644 --- a/roomserver/api/alias.go +++ b/roomserver/api/alias.go @@ -59,18 +59,6 @@ type GetAliasesForRoomIDResponse struct { Aliases []string `json:"aliases"` } -// GetCreatorIDForAliasRequest is a request to GetCreatorIDForAlias -type GetCreatorIDForAliasRequest struct { - // The alias we want to find the creator of - Alias string `json:"alias"` -} - -// GetCreatorIDForAliasResponse is a response to GetCreatorIDForAlias -type GetCreatorIDForAliasResponse struct { - // The user ID of the alias creator - UserID string `json:"user_id"` -} - // RemoveRoomAliasRequest is a request to RemoveRoomAlias type RemoveRoomAliasRequest struct { // ID of the user removing the alias diff --git a/roomserver/api/api.go b/roomserver/api/api.go index fb77423f8..80e7aed64 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -12,213 +12,180 @@ import ( // RoomserverInputAPI is used to write events to the room server. type RoomserverInternalAPI interface { + SyncRoomserverAPI + AppserviceRoomserverAPI + ClientRoomserverAPI + UserRoomserverAPI + FederationRoomserverAPI + // needed to avoid chicken and egg scenario when setting up the // interdependencies between the roomserver and other input APIs - SetFederationAPI(fsAPI fsAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing) - SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) - SetUserAPI(userAPI userapi.UserInternalAPI) + SetFederationAPI(fsAPI fsAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) + SetAppserviceAPI(asAPI asAPI.AppServiceInternalAPI) + SetUserAPI(userAPI userapi.RoomserverUserAPI) + // QueryAuthChain returns the entire auth chain for the event IDs given. + // The response includes the events in the request. + // Omits without error for any missing auth events. There will be no duplicates. + // Used in MSC2836. + QueryAuthChain( + ctx context.Context, + req *QueryAuthChainRequest, + res *QueryAuthChainResponse, + ) error +} + +type InputRoomEventsAPI interface { InputRoomEvents( ctx context.Context, - request *InputRoomEventsRequest, - response *InputRoomEventsResponse, + req *InputRoomEventsRequest, + res *InputRoomEventsResponse, ) +} - PerformInvite( +// Query the latest events and state for a room from the room server. +type QueryLatestEventsAndStateAPI interface { + QueryLatestEventsAndState(ctx context.Context, req *QueryLatestEventsAndStateRequest, res *QueryLatestEventsAndStateResponse) error +} + +// QueryBulkStateContent does a bulk query for state event content in the given rooms. +type QueryBulkStateContentAPI interface { + QueryBulkStateContent(ctx context.Context, req *QueryBulkStateContentRequest, res *QueryBulkStateContentResponse) error +} + +type QueryEventsAPI interface { + // Query a list of events by event ID. + QueryEventsByID( ctx context.Context, - req *PerformInviteRequest, - res *PerformInviteResponse, + req *QueryEventsByIDRequest, + res *QueryEventsByIDResponse, ) error + // QueryCurrentState retrieves the requested state events. If state events are not found, they will be missing from + // the response. + QueryCurrentState(ctx context.Context, req *QueryCurrentStateRequest, res *QueryCurrentStateResponse) error +} - PerformJoin( +// API functions required by the syncapi +type SyncRoomserverAPI interface { + QueryLatestEventsAndStateAPI + QueryBulkStateContentAPI + // QuerySharedUsers returns a list of users who share at least 1 room in common with the given user. + QuerySharedUsers(ctx context.Context, req *QuerySharedUsersRequest, res *QuerySharedUsersResponse) error + // Query a list of events by event ID. + QueryEventsByID( ctx context.Context, - req *PerformJoinRequest, - res *PerformJoinResponse, - ) - - PerformLeave( - ctx context.Context, - req *PerformLeaveRequest, - res *PerformLeaveResponse, + req *QueryEventsByIDRequest, + res *QueryEventsByIDResponse, ) error - - PerformPeek( + // Query the membership event for an user for a room. + QueryMembershipForUser( ctx context.Context, - req *PerformPeekRequest, - res *PerformPeekResponse, - ) - - PerformUnpeek( - ctx context.Context, - req *PerformUnpeekRequest, - res *PerformUnpeekResponse, - ) - - PerformPublish( - ctx context.Context, - req *PerformPublishRequest, - res *PerformPublishResponse, - ) - - PerformInboundPeek( - ctx context.Context, - req *PerformInboundPeekRequest, - res *PerformInboundPeekResponse, - ) error - - QueryPublishedRooms( - ctx context.Context, - req *QueryPublishedRoomsRequest, - res *QueryPublishedRoomsResponse, - ) error - - // Query the latest events and state for a room from the room server. - QueryLatestEventsAndState( - ctx context.Context, - request *QueryLatestEventsAndStateRequest, - response *QueryLatestEventsAndStateResponse, + req *QueryMembershipForUserRequest, + res *QueryMembershipForUserResponse, ) error // Query the state after a list of events in a room from the room server. QueryStateAfterEvents( ctx context.Context, - request *QueryStateAfterEventsRequest, - response *QueryStateAfterEventsResponse, + req *QueryStateAfterEventsRequest, + res *QueryStateAfterEventsResponse, ) error - // Query a list of events by event ID. - QueryEventsByID( - ctx context.Context, - request *QueryEventsByIDRequest, - response *QueryEventsByIDResponse, - ) error - - // Query the membership event for an user for a room. - QueryMembershipForUser( - ctx context.Context, - request *QueryMembershipForUserRequest, - response *QueryMembershipForUserResponse, - ) error - - // Query a list of membership events for a room - QueryMembershipsForRoom( - ctx context.Context, - request *QueryMembershipsForRoomRequest, - response *QueryMembershipsForRoomResponse, - ) error - - // Query if we think we're still in a room. - QueryServerJoinedToRoom( - ctx context.Context, - request *QueryServerJoinedToRoomRequest, - response *QueryServerJoinedToRoomResponse, - ) error - - // Query whether a server is allowed to see an event - QueryServerAllowedToSeeEvent( - ctx context.Context, - request *QueryServerAllowedToSeeEventRequest, - response *QueryServerAllowedToSeeEventResponse, - ) error - - // Query missing events for a room from roomserver - QueryMissingEvents( - ctx context.Context, - request *QueryMissingEventsRequest, - response *QueryMissingEventsResponse, - ) error - - // 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. - QueryStateAndAuthChain( - ctx context.Context, - request *QueryStateAndAuthChainRequest, - response *QueryStateAndAuthChainResponse, - ) error - - // QueryAuthChain returns the entire auth chain for the event IDs given. - // The response includes the events in the request. - // Omits without error for any missing auth events. There will be no duplicates. - QueryAuthChain( - ctx context.Context, - request *QueryAuthChainRequest, - response *QueryAuthChainResponse, - ) error - - // QueryCurrentState retrieves the requested state events. If state events are not found, they will be missing from - // the response. - QueryCurrentState(ctx context.Context, req *QueryCurrentStateRequest, res *QueryCurrentStateResponse) error - // QueryRoomsForUser retrieves a list of room IDs matching the given query. - QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) error - // QueryBulkStateContent does a bulk query for state event content in the given rooms. - QueryBulkStateContent(ctx context.Context, req *QueryBulkStateContentRequest, res *QueryBulkStateContentResponse) error - // QuerySharedUsers returns a list of users who share at least 1 room in common with the given user. - QuerySharedUsers(ctx context.Context, req *QuerySharedUsersRequest, res *QuerySharedUsersResponse) error - // QueryKnownUsers returns a list of users that we know about from our joined rooms. - QueryKnownUsers(ctx context.Context, req *QueryKnownUsersRequest, res *QueryKnownUsersResponse) error - // QueryServerBannedFromRoom returns whether a server is banned from a room by server ACLs. - QueryServerBannedFromRoom(ctx context.Context, req *QueryServerBannedFromRoomRequest, res *QueryServerBannedFromRoomResponse) error - // Query a given amount (or less) of events prior to a given set of events. PerformBackfill( ctx context.Context, - request *PerformBackfillRequest, - response *PerformBackfillResponse, + req *PerformBackfillRequest, + res *PerformBackfillResponse, ) error +} - // PerformForget forgets a rooms history for a specific user - PerformForget(ctx context.Context, req *PerformForgetRequest, resp *PerformForgetResponse) error - - // PerformRoomUpgrade upgrades a room to a newer version - PerformRoomUpgrade(ctx context.Context, req *PerformRoomUpgradeRequest, resp *PerformRoomUpgradeResponse) - - // Asks for the default room version as preferred by the server. - QueryRoomVersionCapabilities( +type AppserviceRoomserverAPI interface { + // Query a list of events by event ID. + QueryEventsByID( ctx context.Context, - request *QueryRoomVersionCapabilitiesRequest, - response *QueryRoomVersionCapabilitiesResponse, + req *QueryEventsByIDRequest, + res *QueryEventsByIDResponse, ) error - - // Asks for the room version for a given room. - QueryRoomVersionForRoom( + // Query a list of membership events for a room + QueryMembershipsForRoom( ctx context.Context, - request *QueryRoomVersionForRoomRequest, - response *QueryRoomVersionForRoomResponse, + req *QueryMembershipsForRoomRequest, + res *QueryMembershipsForRoomResponse, ) error - - // Set a room alias - SetRoomAlias( - ctx context.Context, - req *SetRoomAliasRequest, - response *SetRoomAliasResponse, - ) error - - // Get the room ID for an alias - GetRoomIDForAlias( - ctx context.Context, - req *GetRoomIDForAliasRequest, - response *GetRoomIDForAliasResponse, - ) error - // Get all known aliases for a room ID GetAliasesForRoomID( ctx context.Context, req *GetAliasesForRoomIDRequest, - response *GetAliasesForRoomIDResponse, - ) error - - // Get the user ID of the creator of an alias - GetCreatorIDForAlias( - ctx context.Context, - req *GetCreatorIDForAliasRequest, - response *GetCreatorIDForAliasResponse, - ) error - - // Remove a room alias - RemoveRoomAlias( - ctx context.Context, - req *RemoveRoomAliasRequest, - response *RemoveRoomAliasResponse, + res *GetAliasesForRoomIDResponse, ) error } + +type ClientRoomserverAPI interface { + InputRoomEventsAPI + QueryLatestEventsAndStateAPI + QueryBulkStateContentAPI + QueryEventsAPI + QueryMembershipForUser(ctx context.Context, req *QueryMembershipForUserRequest, res *QueryMembershipForUserResponse) error + QueryMembershipsForRoom(ctx context.Context, req *QueryMembershipsForRoomRequest, res *QueryMembershipsForRoomResponse) error + QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) error + QueryStateAfterEvents(ctx context.Context, req *QueryStateAfterEventsRequest, res *QueryStateAfterEventsResponse) error + // QueryKnownUsers returns a list of users that we know about from our joined rooms. + QueryKnownUsers(ctx context.Context, req *QueryKnownUsersRequest, res *QueryKnownUsersResponse) error + QueryRoomVersionForRoom(ctx context.Context, req *QueryRoomVersionForRoomRequest, res *QueryRoomVersionForRoomResponse) error + QueryPublishedRooms(ctx context.Context, req *QueryPublishedRoomsRequest, res *QueryPublishedRoomsResponse) error + QueryRoomVersionCapabilities(ctx context.Context, req *QueryRoomVersionCapabilitiesRequest, res *QueryRoomVersionCapabilitiesResponse) error + + GetRoomIDForAlias(ctx context.Context, req *GetRoomIDForAliasRequest, res *GetRoomIDForAliasResponse) error + GetAliasesForRoomID(ctx context.Context, req *GetAliasesForRoomIDRequest, res *GetAliasesForRoomIDResponse) error + + // PerformRoomUpgrade upgrades a room to a newer version + PerformRoomUpgrade(ctx context.Context, req *PerformRoomUpgradeRequest, resp *PerformRoomUpgradeResponse) + PerformAdminEvacuateRoom( + ctx context.Context, + req *PerformAdminEvacuateRoomRequest, + res *PerformAdminEvacuateRoomResponse, + ) + PerformPeek(ctx context.Context, req *PerformPeekRequest, res *PerformPeekResponse) + PerformUnpeek(ctx context.Context, req *PerformUnpeekRequest, res *PerformUnpeekResponse) + PerformInvite(ctx context.Context, req *PerformInviteRequest, res *PerformInviteResponse) error + PerformJoin(ctx context.Context, req *PerformJoinRequest, res *PerformJoinResponse) + PerformLeave(ctx context.Context, req *PerformLeaveRequest, res *PerformLeaveResponse) error + PerformPublish(ctx context.Context, req *PerformPublishRequest, res *PerformPublishResponse) + // PerformForget forgets a rooms history for a specific user + PerformForget(ctx context.Context, req *PerformForgetRequest, resp *PerformForgetResponse) error + SetRoomAlias(ctx context.Context, req *SetRoomAliasRequest, res *SetRoomAliasResponse) error + RemoveRoomAlias(ctx context.Context, req *RemoveRoomAliasRequest, res *RemoveRoomAliasResponse) error +} + +type UserRoomserverAPI interface { + QueryLatestEventsAndStateAPI + QueryCurrentState(ctx context.Context, req *QueryCurrentStateRequest, res *QueryCurrentStateResponse) error + QueryMembershipsForRoom(ctx context.Context, req *QueryMembershipsForRoomRequest, res *QueryMembershipsForRoomResponse) error +} + +type FederationRoomserverAPI interface { + InputRoomEventsAPI + QueryLatestEventsAndStateAPI + QueryBulkStateContentAPI + // QueryServerBannedFromRoom returns whether a server is banned from a room by server ACLs. + QueryServerBannedFromRoom(ctx context.Context, req *QueryServerBannedFromRoomRequest, res *QueryServerBannedFromRoomResponse) error + QueryRoomVersionForRoom(ctx context.Context, req *QueryRoomVersionForRoomRequest, res *QueryRoomVersionForRoomResponse) error + GetRoomIDForAlias(ctx context.Context, req *GetRoomIDForAliasRequest, res *GetRoomIDForAliasResponse) error + QueryEventsByID(ctx context.Context, req *QueryEventsByIDRequest, res *QueryEventsByIDResponse) error + // 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. + QueryStateAndAuthChain(ctx context.Context, req *QueryStateAndAuthChainRequest, res *QueryStateAndAuthChainResponse) error + // Query if we think we're still in a room. + QueryServerJoinedToRoom(ctx context.Context, req *QueryServerJoinedToRoomRequest, res *QueryServerJoinedToRoomResponse) error + QueryPublishedRooms(ctx context.Context, req *QueryPublishedRoomsRequest, res *QueryPublishedRoomsResponse) error + // Query missing events for a room from roomserver + QueryMissingEvents(ctx context.Context, req *QueryMissingEventsRequest, res *QueryMissingEventsResponse) error + // Query whether a server is allowed to see an event + QueryServerAllowedToSeeEvent(ctx context.Context, req *QueryServerAllowedToSeeEventRequest, res *QueryServerAllowedToSeeEventResponse) error + QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) error + PerformInboundPeek(ctx context.Context, req *PerformInboundPeekRequest, res *PerformInboundPeekResponse) error + PerformInvite(ctx context.Context, req *PerformInviteRequest, res *PerformInviteResponse) error + // Query a given amount (or less) of events prior to a given set of events. + PerformBackfill(ctx context.Context, req *PerformBackfillRequest, res *PerformBackfillResponse) error +} diff --git a/roomserver/api/api_trace.go b/roomserver/api/api_trace.go index ec7211ef8..711324644 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -19,15 +19,15 @@ type RoomserverInternalAPITrace struct { Impl RoomserverInternalAPI } -func (t *RoomserverInternalAPITrace) SetFederationAPI(fsAPI fsAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing) { +func (t *RoomserverInternalAPITrace) SetFederationAPI(fsAPI fsAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) { t.Impl.SetFederationAPI(fsAPI, keyRing) } -func (t *RoomserverInternalAPITrace) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) { +func (t *RoomserverInternalAPITrace) SetAppserviceAPI(asAPI asAPI.AppServiceInternalAPI) { t.Impl.SetAppserviceAPI(asAPI) } -func (t *RoomserverInternalAPITrace) SetUserAPI(userAPI userapi.UserInternalAPI) { +func (t *RoomserverInternalAPITrace) SetUserAPI(userAPI userapi.RoomserverUserAPI) { t.Impl.SetUserAPI(userAPI) } @@ -104,6 +104,15 @@ func (t *RoomserverInternalAPITrace) PerformPublish( util.GetLogger(ctx).Infof("PerformPublish req=%+v res=%+v", js(req), js(res)) } +func (t *RoomserverInternalAPITrace) PerformAdminEvacuateRoom( + ctx context.Context, + req *PerformAdminEvacuateRoomRequest, + res *PerformAdminEvacuateRoomResponse, +) { + t.Impl.PerformAdminEvacuateRoom(ctx, req, res) + util.GetLogger(ctx).Infof("PerformAdminEvacuateRoom req=%+v res=%+v", js(req), js(res)) +} + func (t *RoomserverInternalAPITrace) PerformInboundPeek( ctx context.Context, req *PerformInboundPeekRequest, @@ -284,16 +293,6 @@ func (t *RoomserverInternalAPITrace) GetAliasesForRoomID( return err } -func (t *RoomserverInternalAPITrace) GetCreatorIDForAlias( - ctx context.Context, - req *GetCreatorIDForAliasRequest, - res *GetCreatorIDForAliasResponse, -) error { - err := t.Impl.GetCreatorIDForAlias(ctx, req, res) - util.GetLogger(ctx).WithError(err).Infof("GetCreatorIDForAlias req=%+v res=%+v", js(req), js(res)) - return err -} - func (t *RoomserverInternalAPITrace) RemoveRoomAlias( ctx context.Context, req *RemoveRoomAliasRequest, diff --git a/roomserver/api/output.go b/roomserver/api/output.go index 767611ec4..a82bf8701 100644 --- a/roomserver/api/output.go +++ b/roomserver/api/output.go @@ -163,6 +163,19 @@ type OutputNewRoomEvent struct { TransactionID *TransactionID `json:"transaction_id,omitempty"` } +func (o *OutputNewRoomEvent) NeededStateEventIDs() ([]*gomatrixserverlib.HeaderedEvent, []string) { + addsStateEvents := make([]*gomatrixserverlib.HeaderedEvent, 0, 1) + missingEventIDs := make([]string, 0, len(o.AddsStateEventIDs)) + for _, eventID := range o.AddsStateEventIDs { + if eventID != o.Event.EventID() { + missingEventIDs = append(missingEventIDs, eventID) + } else { + addsStateEvents = append(addsStateEvents, o.Event) + } + } + return addsStateEvents, missingEventIDs +} + // An OutputOldRoomEvent is written when the roomserver receives an old event. // This will typically happen as a result of getting either missing events // or backfilling. Downstream components may wish to send these events to diff --git a/roomserver/api/perform.go b/roomserver/api/perform.go index cda4b3ee4..30aa2cf1b 100644 --- a/roomserver/api/perform.go +++ b/roomserver/api/perform.go @@ -214,3 +214,12 @@ type PerformRoomUpgradeResponse struct { NewRoomID string Error *PerformError } + +type PerformAdminEvacuateRoomRequest struct { + RoomID string `json:"room_id"` +} + +type PerformAdminEvacuateRoomResponse struct { + Affected []string `json:"affected"` + Error *PerformError +} diff --git a/roomserver/api/query.go b/roomserver/api/query.go index 8f84edcb5..afafb87c3 100644 --- a/roomserver/api/query.go +++ b/roomserver/api/query.go @@ -122,6 +122,7 @@ type QueryMembershipForUserResponse struct { Membership string `json:"membership"` // True if the user asked to forget this room. IsRoomForgotten bool `json:"is_room_forgotten"` + RoomExists bool `json:"room_exists"` } // QueryMembershipsForRoomRequest is a request to QueryMembershipsForRoom @@ -230,6 +231,8 @@ type QueryStateAndAuthChainResponse struct { // The lists will be in an arbitrary order. StateEvents []*gomatrixserverlib.HeaderedEvent `json:"state_events"` AuthChainEvents []*gomatrixserverlib.HeaderedEvent `json:"auth_chain_events"` + // True if the queried event was rejected earlier. + IsRejected bool `json:"is_rejected"` } // QueryRoomVersionCapabilitiesRequest asks for the default room version diff --git a/roomserver/api/wrapper.go b/roomserver/api/wrapper.go index 5491d36b3..344e9b079 100644 --- a/roomserver/api/wrapper.go +++ b/roomserver/api/wrapper.go @@ -16,7 +16,6 @@ package api import ( "context" - "fmt" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -24,7 +23,7 @@ import ( // SendEvents to the roomserver The events are written with KindNew. func SendEvents( - ctx context.Context, rsAPI RoomserverInternalAPI, + ctx context.Context, rsAPI InputRoomEventsAPI, kind Kind, events []*gomatrixserverlib.HeaderedEvent, origin gomatrixserverlib.ServerName, sendAsServer gomatrixserverlib.ServerName, txnID *TransactionID, @@ -47,7 +46,7 @@ func SendEvents( // with the state at the event as KindOutlier before it. Will not send any event that is // marked as `true` in haveEventIDs. func SendEventWithState( - ctx context.Context, rsAPI RoomserverInternalAPI, kind Kind, + ctx context.Context, rsAPI InputRoomEventsAPI, kind Kind, state *gomatrixserverlib.RespState, event *gomatrixserverlib.HeaderedEvent, origin gomatrixserverlib.ServerName, haveEventIDs map[string]bool, async bool, ) error { @@ -83,7 +82,7 @@ func SendEventWithState( // SendInputRoomEvents to the roomserver. func SendInputRoomEvents( - ctx context.Context, rsAPI RoomserverInternalAPI, + ctx context.Context, rsAPI InputRoomEventsAPI, ires []InputRoomEvent, async bool, ) error { request := InputRoomEventsRequest{ @@ -95,37 +94,8 @@ func SendInputRoomEvents( return response.Err() } -// SendInvite event to the roomserver. -// This should only be needed for invite events that occur outside of a known room. -// If we are in the room then the event should be sent using the SendEvents method. -func SendInvite( - ctx context.Context, - rsAPI RoomserverInternalAPI, inviteEvent *gomatrixserverlib.HeaderedEvent, - inviteRoomState []gomatrixserverlib.InviteV2StrippedState, - sendAsServer gomatrixserverlib.ServerName, txnID *TransactionID, -) error { - // Start by sending the invite request into the roomserver. This will - // trigger the federation request amongst other things if needed. - request := &PerformInviteRequest{ - Event: inviteEvent, - InviteRoomState: inviteRoomState, - RoomVersion: inviteEvent.RoomVersion, - SendAsServer: string(sendAsServer), - TransactionID: txnID, - } - response := &PerformInviteResponse{} - if err := rsAPI.PerformInvite(ctx, request, response); err != nil { - return fmt.Errorf("rsAPI.PerformInvite: %w", err) - } - if response.Error != nil { - return response.Error - } - - return nil -} - // GetEvent returns the event or nil, even on errors. -func GetEvent(ctx context.Context, rsAPI RoomserverInternalAPI, eventID string) *gomatrixserverlib.HeaderedEvent { +func GetEvent(ctx context.Context, rsAPI QueryEventsAPI, eventID string) *gomatrixserverlib.HeaderedEvent { var res QueryEventsByIDResponse err := rsAPI.QueryEventsByID(ctx, &QueryEventsByIDRequest{ EventIDs: []string{eventID}, @@ -141,7 +111,7 @@ func GetEvent(ctx context.Context, rsAPI RoomserverInternalAPI, eventID string) } // GetStateEvent returns the current state event in the room or nil. -func GetStateEvent(ctx context.Context, rsAPI RoomserverInternalAPI, roomID string, tuple gomatrixserverlib.StateKeyTuple) *gomatrixserverlib.HeaderedEvent { +func GetStateEvent(ctx context.Context, rsAPI QueryEventsAPI, roomID string, tuple gomatrixserverlib.StateKeyTuple) *gomatrixserverlib.HeaderedEvent { var res QueryCurrentStateResponse err := rsAPI.QueryCurrentState(ctx, &QueryCurrentStateRequest{ RoomID: roomID, @@ -159,7 +129,7 @@ func GetStateEvent(ctx context.Context, rsAPI RoomserverInternalAPI, roomID stri } // IsServerBannedFromRoom returns whether the server is banned from a room by server ACLs. -func IsServerBannedFromRoom(ctx context.Context, rsAPI RoomserverInternalAPI, roomID string, serverName gomatrixserverlib.ServerName) bool { +func IsServerBannedFromRoom(ctx context.Context, rsAPI FederationRoomserverAPI, roomID string, serverName gomatrixserverlib.ServerName) bool { req := &QueryServerBannedFromRoomRequest{ ServerName: serverName, RoomID: roomID, @@ -175,7 +145,7 @@ func IsServerBannedFromRoom(ctx context.Context, rsAPI RoomserverInternalAPI, ro // PopulatePublicRooms extracts PublicRoom information for all the provided room IDs. The IDs are not checked to see if they are visible in the // published room directory. // due to lots of switches -func PopulatePublicRooms(ctx context.Context, roomIDs []string, rsAPI RoomserverInternalAPI) ([]gomatrixserverlib.PublicRoom, error) { +func PopulatePublicRooms(ctx context.Context, roomIDs []string, rsAPI QueryBulkStateContentAPI) ([]gomatrixserverlib.PublicRoom, error) { avatarTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.avatar", StateKey: ""} nameTuple := gomatrixserverlib.StateKeyTuple{EventType: "m.room.name", StateKey: ""} canonicalTuple := gomatrixserverlib.StateKeyTuple{EventType: gomatrixserverlib.MRoomCanonicalAlias, StateKey: ""} diff --git a/roomserver/internal/alias.go b/roomserver/internal/alias.go index 02fc4a5a7..f47ae47fe 100644 --- a/roomserver/internal/alias.go +++ b/roomserver/internal/alias.go @@ -41,9 +41,6 @@ type RoomserverInternalAPIDatabase interface { // Look up all aliases referring to a given room ID. // Returns an error if there was a problem talking to the database. GetAliasesForRoomID(ctx context.Context, roomID string) ([]string, error) - // Get the user ID of the creator of an alias. - // Returns an error if there was a problem talking to the database. - GetCreatorIDForAlias(ctx context.Context, alias string) (string, error) // Remove a given room alias. // Returns an error if there was a problem talking to the database. RemoveRoomAlias(ctx context.Context, alias string) error @@ -134,22 +131,6 @@ func (r *RoomserverInternalAPI) GetAliasesForRoomID( return nil } -// GetCreatorIDForAlias implements alias.RoomserverInternalAPI -func (r *RoomserverInternalAPI) GetCreatorIDForAlias( - ctx context.Context, - request *api.GetCreatorIDForAliasRequest, - response *api.GetCreatorIDForAliasResponse, -) error { - // Look up the aliases in the database for the given RoomID - creatorID, err := r.DB.GetCreatorIDForAlias(ctx, request.Alias) - if err != nil { - return err - } - - response.UserID = creatorID - return nil -} - // RemoveRoomAlias implements alias.RoomserverInternalAPI func (r *RoomserverInternalAPI) RemoveRoomAlias( ctx context.Context, diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index 59f485cf7..afef52da4 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -35,6 +35,7 @@ type RoomserverInternalAPI struct { *perform.Backfiller *perform.Forgetter *perform.Upgrader + *perform.Admin ProcessContext *process.ProcessContext DB storage.Database Cfg *config.RoomServer @@ -42,8 +43,8 @@ type RoomserverInternalAPI struct { ServerName gomatrixserverlib.ServerName KeyRing gomatrixserverlib.JSONVerifier ServerACLs *acls.ServerACLs - fsAPI fsAPI.FederationInternalAPI - asAPI asAPI.AppServiceQueryAPI + fsAPI fsAPI.RoomserverFederationAPI + asAPI asAPI.AppServiceInternalAPI NATSClient *nats.Conn JetStream nats.JetStreamContext Durable string @@ -86,7 +87,7 @@ func NewRoomserverAPI( // SetFederationInputAPI passes in a federation input API reference so that we can // avoid the chicken-and-egg problem of both the roomserver input API and the // federation input API being interdependent. -func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing) { +func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) { r.fsAPI = fsAPI r.KeyRing = keyRing @@ -164,17 +165,23 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.FederationInternalA Cfg: r.Cfg, URSAPI: r, } + r.Admin = &perform.Admin{ + DB: r.DB, + Cfg: r.Cfg, + Inputer: r.Inputer, + Queryer: r.Queryer, + } if err := r.Inputer.Start(); err != nil { logrus.WithError(err).Panic("failed to start roomserver input API") } } -func (r *RoomserverInternalAPI) SetUserAPI(userAPI userapi.UserInternalAPI) { +func (r *RoomserverInternalAPI) SetUserAPI(userAPI userapi.RoomserverUserAPI) { r.Leaver.UserAPI = userAPI } -func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) { +func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceInternalAPI) { r.asAPI = asAPI } diff --git a/roomserver/internal/input/input.go b/roomserver/internal/input/input.go index 1fea6ef06..600994c5a 100644 --- a/roomserver/internal/input/input.go +++ b/roomserver/internal/input/input.go @@ -82,7 +82,7 @@ type Inputer struct { JetStream nats.JetStreamContext Durable nats.SubOpt ServerName gomatrixserverlib.ServerName - FSAPI fedapi.FederationInternalAPI + FSAPI fedapi.RoomserverFederationAPI KeyRing gomatrixserverlib.JSONVerifier ACLs *acls.ServerACLs InputRoomEventTopic string @@ -202,7 +202,7 @@ func (w *worker) _next() { return } - case context.DeadlineExceeded: + case context.DeadlineExceeded, context.Canceled: // The context exceeded, so we've been waiting for more than a // minute for activity in this room. At this point we will shut // down the subscriber to free up resources. It'll get started diff --git a/roomserver/internal/input/input_latest_events.go b/roomserver/internal/input/input_latest_events.go index 7e58ef9d0..e4c138d58 100644 --- a/roomserver/internal/input/input_latest_events.go +++ b/roomserver/internal/input/input_latest_events.go @@ -233,12 +233,19 @@ func (u *latestEventsUpdater) latestState() error { } } - // Get a list of the current latest events. This may or may not - // include the new event from the input path, depending on whether - // it is a forward extremity or not. - latestStateAtEvents := make([]types.StateAtEvent, len(u.latest)) - for i := range u.latest { - latestStateAtEvents[i] = u.latest[i].StateAtEvent + // Take the old set of extremities and the new set of extremities and + // mash them together into a list. This may or may not include the new event + // from the input path, depending on whether it became a forward extremity + // or not. We'll then run state resolution across all of them to determine + // the new current state of the room. Including the old extremities here + // ensures that new forward extremities with bad state snapshots (from + // possible malicious actors) can't completely corrupt the room state + // away from what it was before. + combinedExtremities := types.StateAtEventAndReferences(append(u.oldLatest, u.latest...)) + combinedExtremities = combinedExtremities[:util.SortAndUnique(combinedExtremities)] + latestStateAtEvents := make([]types.StateAtEvent, len(combinedExtremities)) + for i := range combinedExtremities { + latestStateAtEvents[i] = combinedExtremities[i].StateAtEvent } // Takes the NIDs of the latest events and creates a state snapshot @@ -316,40 +323,30 @@ func (u *latestEventsUpdater) calculateLatest( // Then let's see if any of the existing forward extremities now // have entries in the previous events table. If they do then we // will no longer include them as forward extremities. - existingPrevs := make(map[string]struct{}) - for _, l := range existingRefs { + for k, l := range existingRefs { referenced, err := u.updater.IsReferenced(l.EventReference) if err != nil { return false, fmt.Errorf("u.updater.IsReferenced: %w", err) } else if referenced { - existingPrevs[l.EventID] = struct{}{} + delete(existingRefs, k) } } - // Include our new event in the extremities. - newLatest := []types.StateAtEventAndReference{newStateAndRef} + // Start off with our new unreferenced event. We're reusing the backing + // array here rather than allocating a new one. + u.latest = append(u.latest[:0], newStateAndRef) - // Then run through and see if the other extremities are still valid. - // If our new event references them then they are no longer good - // candidates. + // If our new event references any of the existing forward extremities + // then they are no longer forward extremities, so remove them. for _, prevEventID := range newEvent.PrevEventIDs() { delete(existingRefs, prevEventID) } - // Ensure that we don't add any candidate forward extremities from - // the old set that are, themselves, referenced by the old set of - // forward extremities. This shouldn't happen but guards against - // the possibility anyway. - for prevEventID := range existingPrevs { - delete(existingRefs, prevEventID) - } - // Then re-add any old extremities that are still valid after all. for _, old := range existingRefs { - newLatest = append(newLatest, *old) + u.latest = append(u.latest, *old) } - u.latest = newLatest return true, nil } diff --git a/roomserver/internal/input/input_missing.go b/roomserver/internal/input/input_missing.go index 2c958335d..9c70076c2 100644 --- a/roomserver/internal/input/input_missing.go +++ b/roomserver/internal/input/input_missing.go @@ -44,7 +44,7 @@ type missingStateReq struct { roomInfo *types.RoomInfo inputer *Inputer keys gomatrixserverlib.JSONVerifier - federation fedapi.FederationInternalAPI + federation fedapi.RoomserverFederationAPI roomsMu *internal.MutexByRoom servers []gomatrixserverlib.ServerName hadEvents map[string]bool diff --git a/roomserver/internal/input/input_test.go b/roomserver/internal/input/input_test.go index 81c86ae38..7c65f9eac 100644 --- a/roomserver/internal/input/input_test.go +++ b/roomserver/internal/input/input_test.go @@ -10,9 +10,9 @@ import ( "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/internal/input" "github.com/matrix-org/dendrite/roomserver/storage" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/dendrite/setup/jetstream" - "github.com/matrix-org/dendrite/setup/process" + "github.com/matrix-org/dendrite/test/testrig" "github.com/matrix-org/gomatrixserverlib" "github.com/nats-io/nats.go" ) @@ -21,11 +21,11 @@ var js nats.JetStreamContext var jc *nats.Conn func TestMain(m *testing.M) { - var pc *process.ProcessContext - pc, js, jc = jetstream.PrepareForTests() + var b *base.BaseDendrite + b, js, jc = testrig.Base(nil) code := m.Run() - pc.ShutdownDendrite() - pc.WaitForComponentsToFinish() + b.ShutdownDendrite() + b.WaitForComponentsToFinish() os.Exit(code) } @@ -53,6 +53,7 @@ func TestSingleTransactionOnInput(t *testing.T) { t.Fatal(err) } db, err := storage.Open( + nil, &config.DatabaseOptions{ ConnectionString: "", MaxOpenConnections: 1, diff --git a/roomserver/internal/perform/perform_admin.go b/roomserver/internal/perform/perform_admin.go new file mode 100644 index 000000000..2de6477cc --- /dev/null +++ b/roomserver/internal/perform/perform_admin.go @@ -0,0 +1,162 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package perform + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/roomserver/internal/input" + "github.com/matrix-org/dendrite/roomserver/internal/query" + "github.com/matrix-org/dendrite/roomserver/storage" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/gomatrixserverlib" +) + +type Admin struct { + DB storage.Database + Cfg *config.RoomServer + Queryer *query.Queryer + Inputer *input.Inputer +} + +// PerformEvacuateRoom will remove all local users from the given room. +func (r *Admin) PerformAdminEvacuateRoom( + ctx context.Context, + req *api.PerformAdminEvacuateRoomRequest, + res *api.PerformAdminEvacuateRoomResponse, +) { + roomInfo, err := r.DB.RoomInfo(ctx, req.RoomID) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.DB.RoomInfo: %s", err), + } + return + } + if roomInfo == nil || roomInfo.IsStub { + res.Error = &api.PerformError{ + Code: api.PerformErrorNoRoom, + Msg: fmt.Sprintf("Room %s not found", req.RoomID), + } + return + } + + memberNIDs, err := r.DB.GetMembershipEventNIDsForRoom(ctx, roomInfo.RoomNID, true, true) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.DB.GetMembershipEventNIDsForRoom: %s", err), + } + return + } + + memberEvents, err := r.DB.Events(ctx, memberNIDs) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.DB.Events: %s", err), + } + return + } + + inputEvents := make([]api.InputRoomEvent, 0, len(memberEvents)) + res.Affected = make([]string, 0, len(memberEvents)) + latestReq := &api.QueryLatestEventsAndStateRequest{ + RoomID: req.RoomID, + } + latestRes := &api.QueryLatestEventsAndStateResponse{} + if err = r.Queryer.QueryLatestEventsAndState(ctx, latestReq, latestRes); err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("r.Queryer.QueryLatestEventsAndState: %s", err), + } + return + } + + prevEvents := latestRes.LatestEvents + for _, memberEvent := range memberEvents { + if memberEvent.StateKey() == nil { + continue + } + + var memberContent gomatrixserverlib.MemberContent + if err = json.Unmarshal(memberEvent.Content(), &memberContent); err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("json.Unmarshal: %s", err), + } + return + } + memberContent.Membership = gomatrixserverlib.Leave + + stateKey := *memberEvent.StateKey() + fledglingEvent := &gomatrixserverlib.EventBuilder{ + RoomID: req.RoomID, + Type: gomatrixserverlib.MRoomMember, + StateKey: &stateKey, + Sender: stateKey, + PrevEvents: prevEvents, + } + + if fledglingEvent.Content, err = json.Marshal(memberContent); err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("json.Marshal: %s", err), + } + return + } + + eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(fledglingEvent) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("gomatrixserverlib.StateNeededForEventBuilder: %s", err), + } + return + } + + event, err := eventutil.BuildEvent(ctx, fledglingEvent, r.Cfg.Matrix, time.Now(), &eventsNeeded, latestRes) + if err != nil { + res.Error = &api.PerformError{ + Code: api.PerformErrorBadRequest, + Msg: fmt.Sprintf("eventutil.BuildEvent: %s", err), + } + return + } + + inputEvents = append(inputEvents, api.InputRoomEvent{ + Kind: api.KindNew, + Event: event, + Origin: r.Cfg.Matrix.ServerName, + SendAsServer: string(r.Cfg.Matrix.ServerName), + }) + res.Affected = append(res.Affected, stateKey) + prevEvents = []gomatrixserverlib.EventReference{ + event.EventReference(), + } + } + + inputReq := &api.InputRoomEventsRequest{ + InputRoomEvents: inputEvents, + Asynchronous: true, + } + inputRes := &api.InputRoomEventsResponse{} + r.Inputer.InputRoomEvents(ctx, inputReq, inputRes) +} diff --git a/roomserver/internal/perform/perform_backfill.go b/roomserver/internal/perform/perform_backfill.go index 081f694a1..1bc4c75ce 100644 --- a/roomserver/internal/perform/perform_backfill.go +++ b/roomserver/internal/perform/perform_backfill.go @@ -38,7 +38,7 @@ const maxBackfillServers = 5 type Backfiller struct { ServerName gomatrixserverlib.ServerName DB storage.Database - FSAPI federationAPI.FederationInternalAPI + FSAPI federationAPI.RoomserverFederationAPI KeyRing gomatrixserverlib.JSONVerifier // The servers which should be preferred above other servers when backfilling @@ -228,7 +228,7 @@ func (r *Backfiller) fetchAndStoreMissingEvents(ctx context.Context, roomVer gom // backfillRequester implements gomatrixserverlib.BackfillRequester type backfillRequester struct { db storage.Database - fsAPI federationAPI.FederationInternalAPI + fsAPI federationAPI.RoomserverFederationAPI thisServer gomatrixserverlib.ServerName preferServer map[gomatrixserverlib.ServerName]bool bwExtrems map[string][]string @@ -240,7 +240,7 @@ type backfillRequester struct { } func newBackfillRequester( - db storage.Database, fsAPI federationAPI.FederationInternalAPI, thisServer gomatrixserverlib.ServerName, + db storage.Database, fsAPI federationAPI.RoomserverFederationAPI, thisServer gomatrixserverlib.ServerName, bwExtrems map[string][]string, preferServers []gomatrixserverlib.ServerName, ) *backfillRequester { preferServer := make(map[gomatrixserverlib.ServerName]bool) diff --git a/roomserver/internal/perform/perform_invite.go b/roomserver/internal/perform/perform_invite.go index 6111372d8..b0148a314 100644 --- a/roomserver/internal/perform/perform_invite.go +++ b/roomserver/internal/perform/perform_invite.go @@ -35,7 +35,7 @@ import ( type Inviter struct { DB storage.Database Cfg *config.RoomServer - FSAPI federationAPI.FederationInternalAPI + FSAPI federationAPI.RoomserverFederationAPI Inputer *input.Inputer } diff --git a/roomserver/internal/perform/perform_join.go b/roomserver/internal/perform/perform_join.go index a40f66d21..61a0206ef 100644 --- a/roomserver/internal/perform/perform_join.go +++ b/roomserver/internal/perform/perform_join.go @@ -38,7 +38,7 @@ import ( type Joiner struct { ServerName gomatrixserverlib.ServerName Cfg *config.RoomServer - FSAPI fsAPI.FederationInternalAPI + FSAPI fsAPI.RoomserverFederationAPI RSAPI rsAPI.RoomserverInternalAPI DB storage.Database diff --git a/roomserver/internal/perform/perform_leave.go b/roomserver/internal/perform/perform_leave.go index 5b4cd3c6f..c5b62ac00 100644 --- a/roomserver/internal/perform/perform_leave.go +++ b/roomserver/internal/perform/perform_leave.go @@ -37,8 +37,8 @@ import ( type Leaver struct { Cfg *config.RoomServer DB storage.Database - FSAPI fsAPI.FederationInternalAPI - UserAPI userapi.UserInternalAPI + FSAPI fsAPI.RoomserverFederationAPI + UserAPI userapi.RoomserverUserAPI Inputer *input.Inputer } diff --git a/roomserver/internal/perform/perform_peek.go b/roomserver/internal/perform/perform_peek.go index 6a2c329b9..45e63888d 100644 --- a/roomserver/internal/perform/perform_peek.go +++ b/roomserver/internal/perform/perform_peek.go @@ -33,7 +33,7 @@ import ( type Peeker struct { ServerName gomatrixserverlib.ServerName Cfg *config.RoomServer - FSAPI fsAPI.FederationInternalAPI + FSAPI fsAPI.RoomserverFederationAPI DB storage.Database Inputer *input.Inputer diff --git a/roomserver/internal/perform/perform_unpeek.go b/roomserver/internal/perform/perform_unpeek.go index 16b4eeaed..1057499cb 100644 --- a/roomserver/internal/perform/perform_unpeek.go +++ b/roomserver/internal/perform/perform_unpeek.go @@ -30,7 +30,7 @@ import ( type Unpeeker struct { ServerName gomatrixserverlib.ServerName Cfg *config.RoomServer - FSAPI fsAPI.FederationInternalAPI + FSAPI fsAPI.RoomserverFederationAPI DB storage.Database Inputer *input.Inputer diff --git a/roomserver/internal/query/query.go b/roomserver/internal/query/query.go index 7e4d56684..d25bdc378 100644 --- a/roomserver/internal/query/query.go +++ b/roomserver/internal/query/query.go @@ -169,8 +169,10 @@ func (r *Queryer) QueryMembershipForUser( return err } if info == nil { - return fmt.Errorf("QueryMembershipForUser: unknown room %s", request.RoomID) + response.RoomExists = false + return nil } + response.RoomExists = true membershipEventNID, stillInRoom, isRoomforgotten, err := r.DB.GetMembership(ctx, info.RoomNID, request.UserID) if err != nil { @@ -441,11 +443,11 @@ func (r *Queryer) QueryStateAndAuthChain( } var stateEvents []*gomatrixserverlib.Event - stateEvents, err = r.loadStateAtEventIDs(ctx, info, request.PrevEventIDs) + stateEvents, rejected, err := r.loadStateAtEventIDs(ctx, info, request.PrevEventIDs) if err != nil { return err } - + response.IsRejected = rejected response.PrevEventsExist = true // add the auth event IDs for the current state events too @@ -480,15 +482,23 @@ func (r *Queryer) QueryStateAndAuthChain( return err } -func (r *Queryer) loadStateAtEventIDs(ctx context.Context, roomInfo *types.RoomInfo, eventIDs []string) ([]*gomatrixserverlib.Event, error) { +func (r *Queryer) loadStateAtEventIDs(ctx context.Context, roomInfo *types.RoomInfo, eventIDs []string) ([]*gomatrixserverlib.Event, bool, error) { roomState := state.NewStateResolution(r.DB, roomInfo) prevStates, err := r.DB.StateAtEventIDs(ctx, eventIDs) if err != nil { switch err.(type) { case types.MissingEventError: - return nil, nil + return nil, false, nil default: - return nil, err + return nil, false, err + } + } + // Currently only used on /state and /state_ids + rejected := false + for i := range prevStates { + if prevStates[i].IsRejected { + rejected = true + break } } @@ -497,10 +507,12 @@ func (r *Queryer) loadStateAtEventIDs(ctx context.Context, roomInfo *types.RoomI ctx, prevStates, ) if err != nil { - return nil, err + return nil, rejected, err } - return helpers.LoadStateEvents(ctx, r.DB, stateEntries) + events, err := helpers.LoadStateEvents(ctx, r.DB, stateEntries) + + return events, rejected, err } type eventsFromIDs func(context.Context, []string) ([]types.Event, error) diff --git a/roomserver/internal/query/query_test.go b/roomserver/internal/query/query_test.go index ba5bb9f55..03627ea97 100644 --- a/roomserver/internal/query/query_test.go +++ b/roomserver/internal/query/query_test.go @@ -19,8 +19,8 @@ import ( "encoding/json" "testing" - "github.com/matrix-org/dendrite/internal/test" "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/test" "github.com/matrix-org/gomatrixserverlib" ) diff --git a/roomserver/inthttp/client.go b/roomserver/inthttp/client.go index d55805a91..09358001b 100644 --- a/roomserver/inthttp/client.go +++ b/roomserver/inthttp/client.go @@ -29,16 +29,17 @@ const ( RoomserverInputRoomEventsPath = "/roomserver/inputRoomEvents" // Perform operations - RoomserverPerformInvitePath = "/roomserver/performInvite" - RoomserverPerformPeekPath = "/roomserver/performPeek" - RoomserverPerformUnpeekPath = "/roomserver/performUnpeek" - RoomserverPerformRoomUpgradePath = "/roomserver/performRoomUpgrade" - RoomserverPerformJoinPath = "/roomserver/performJoin" - RoomserverPerformLeavePath = "/roomserver/performLeave" - RoomserverPerformBackfillPath = "/roomserver/performBackfill" - RoomserverPerformPublishPath = "/roomserver/performPublish" - RoomserverPerformInboundPeekPath = "/roomserver/performInboundPeek" - RoomserverPerformForgetPath = "/roomserver/performForget" + RoomserverPerformInvitePath = "/roomserver/performInvite" + RoomserverPerformPeekPath = "/roomserver/performPeek" + RoomserverPerformUnpeekPath = "/roomserver/performUnpeek" + RoomserverPerformRoomUpgradePath = "/roomserver/performRoomUpgrade" + RoomserverPerformJoinPath = "/roomserver/performJoin" + RoomserverPerformLeavePath = "/roomserver/performLeave" + RoomserverPerformBackfillPath = "/roomserver/performBackfill" + RoomserverPerformPublishPath = "/roomserver/performPublish" + RoomserverPerformInboundPeekPath = "/roomserver/performInboundPeek" + RoomserverPerformForgetPath = "/roomserver/performForget" + RoomserverPerformAdminEvacuateRoomPath = "/roomserver/performAdminEvacuateRoom" // Query operations RoomserverQueryLatestEventsAndStatePath = "/roomserver/queryLatestEventsAndState" @@ -86,15 +87,15 @@ func NewRoomserverClient( } // SetFederationInputAPI no-ops in HTTP client mode as there is no chicken/egg scenario -func (h *httpRoomserverInternalAPI) SetFederationAPI(fsAPI fsInputAPI.FederationInternalAPI, keyRing *gomatrixserverlib.KeyRing) { +func (h *httpRoomserverInternalAPI) SetFederationAPI(fsAPI fsInputAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) { } // SetAppserviceAPI no-ops in HTTP client mode as there is no chicken/egg scenario -func (h *httpRoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) { +func (h *httpRoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceInternalAPI) { } // SetUserAPI no-ops in HTTP client mode as there is no chicken/egg scenario -func (h *httpRoomserverInternalAPI) SetUserAPI(userAPI userapi.UserInternalAPI) { +func (h *httpRoomserverInternalAPI) SetUserAPI(userAPI userapi.RoomserverUserAPI) { } // SetRoomAlias implements RoomserverAliasAPI @@ -136,19 +137,6 @@ func (h *httpRoomserverInternalAPI) GetAliasesForRoomID( return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response) } -// GetCreatorIDForAlias implements RoomserverAliasAPI -func (h *httpRoomserverInternalAPI) GetCreatorIDForAlias( - ctx context.Context, - request *api.GetCreatorIDForAliasRequest, - response *api.GetCreatorIDForAliasResponse, -) error { - span, ctx := opentracing.StartSpanFromContext(ctx, "GetCreatorIDForAlias") - defer span.Finish() - - apiURL := h.roomserverURL + RoomserverGetCreatorIDForAliasPath - return httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response) -} - // RemoveRoomAlias implements RoomserverAliasAPI func (h *httpRoomserverInternalAPI) RemoveRoomAlias( ctx context.Context, @@ -299,6 +287,23 @@ func (h *httpRoomserverInternalAPI) PerformPublish( } } +func (h *httpRoomserverInternalAPI) PerformAdminEvacuateRoom( + ctx context.Context, + req *api.PerformAdminEvacuateRoomRequest, + res *api.PerformAdminEvacuateRoomResponse, +) { + span, ctx := opentracing.StartSpanFromContext(ctx, "PerformAdminEvacuateRoom") + defer span.Finish() + + apiURL := h.roomserverURL + RoomserverPerformAdminEvacuateRoomPath + err := httputil.PostJSON(ctx, span, h.httpClient, apiURL, req, res) + if err != nil { + res.Error = &api.PerformError{ + Msg: fmt.Sprintf("failed to communicate with roomserver: %s", err), + } + } +} + // QueryLatestEventsAndState implements RoomserverQueryAPI func (h *httpRoomserverInternalAPI) QueryLatestEventsAndState( ctx context.Context, diff --git a/roomserver/inthttp/server.go b/roomserver/inthttp/server.go index 0b27b5a8d..9042e341b 100644 --- a/roomserver/inthttp/server.go +++ b/roomserver/inthttp/server.go @@ -118,6 +118,17 @@ func AddRoutes(r api.RoomserverInternalAPI, internalAPIMux *mux.Router) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + internalAPIMux.Handle(RoomserverPerformAdminEvacuateRoomPath, + httputil.MakeInternalAPI("performAdminEvacuateRoom", func(req *http.Request) util.JSONResponse { + var request api.PerformAdminEvacuateRoomRequest + var response api.PerformAdminEvacuateRoomResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + r.PerformAdminEvacuateRoom(req.Context(), &request, &response) + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) internalAPIMux.Handle( RoomserverQueryPublishedRoomsPath, httputil.MakeInternalAPI("queryPublishedRooms", func(req *http.Request) util.JSONResponse { @@ -342,20 +353,6 @@ func AddRoutes(r api.RoomserverInternalAPI, internalAPIMux *mux.Router) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) - internalAPIMux.Handle( - RoomserverGetCreatorIDForAliasPath, - httputil.MakeInternalAPI("GetCreatorIDForAlias", func(req *http.Request) util.JSONResponse { - var request api.GetCreatorIDForAliasRequest - var response api.GetCreatorIDForAliasResponse - if err := json.NewDecoder(req.Body).Decode(&request); err != nil { - return util.ErrorResponse(err) - } - if err := r.GetCreatorIDForAlias(req.Context(), &request, &response); err != nil { - return util.ErrorResponse(err) - } - return util.JSONResponse{Code: http.StatusOK, JSON: &response} - }), - ) internalAPIMux.Handle( RoomserverGetAliasesForRoomIDPath, httputil.MakeInternalAPI("getAliasesForRoomID", func(req *http.Request) util.JSONResponse { diff --git a/roomserver/roomserver.go b/roomserver/roomserver.go index 36e3c5269..1480e8942 100644 --- a/roomserver/roomserver.go +++ b/roomserver/roomserver.go @@ -45,12 +45,12 @@ func NewInternalAPI( perspectiveServerNames = append(perspectiveServerNames, kp.ServerName) } - roomserverDB, err := storage.Open(&cfg.Database, base.Caches) + roomserverDB, err := storage.Open(base, &cfg.Database, base.Caches) if err != nil { logrus.WithError(err).Panicf("failed to connect to room server db") } - js, nc := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, nc := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) return internal.NewRoomserverAPI( base.ProcessContext, cfg, roomserverDB, js, nc, diff --git a/roomserver/storage/postgres/event_json_table.go b/roomserver/storage/postgres/event_json_table.go index b3220effd..5f069ca10 100644 --- a/roomserver/storage/postgres/event_json_table.go +++ b/roomserver/storage/postgres/event_json_table.go @@ -59,12 +59,12 @@ type eventJSONStatements struct { bulkSelectEventJSONStmt *sql.Stmt } -func createEventJSONTable(db *sql.DB) error { +func CreateEventJSONTable(db *sql.DB) error { _, err := db.Exec(eventJSONSchema) return err } -func prepareEventJSONTable(db *sql.DB) (tables.EventJSON, error) { +func PrepareEventJSONTable(db *sql.DB) (tables.EventJSON, error) { s := &eventJSONStatements{} return s, sqlutil.StatementList{ @@ -97,9 +97,9 @@ func (s *eventJSONStatements) BulkSelectEventJSON( // We might get fewer results than NIDs so we adjust the length of the slice before returning it. results := make([]tables.EventJSONPair, len(eventNIDs)) i := 0 + var eventNID int64 for ; rows.Next(); i++ { result := &results[i] - var eventNID int64 if err := rows.Scan(&eventNID, &result.EventJSON); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/event_state_keys_table.go b/roomserver/storage/postgres/event_state_keys_table.go index 762b3a1fc..338e11b82 100644 --- a/roomserver/storage/postgres/event_state_keys_table.go +++ b/roomserver/storage/postgres/event_state_keys_table.go @@ -76,12 +76,12 @@ type eventStateKeyStatements struct { bulkSelectEventStateKeyStmt *sql.Stmt } -func createEventStateKeysTable(db *sql.DB) error { +func CreateEventStateKeysTable(db *sql.DB) error { _, err := db.Exec(eventStateKeysSchema) return err } -func prepareEventStateKeysTable(db *sql.DB) (tables.EventStateKeys, error) { +func PrepareEventStateKeysTable(db *sql.DB) (tables.EventStateKeys, error) { s := &eventStateKeyStatements{} return s, sqlutil.StatementList{ @@ -123,9 +123,9 @@ func (s *eventStateKeyStatements) BulkSelectEventStateKeyNID( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventStateKeyNID: rows.close() failed") result := make(map[string]types.EventStateKeyNID, len(eventStateKeys)) + var stateKey string + var stateKeyNID int64 for rows.Next() { - var stateKey string - var stateKeyNID int64 if err := rows.Scan(&stateKey, &stateKeyNID); err != nil { return nil, err } @@ -149,9 +149,9 @@ func (s *eventStateKeyStatements) BulkSelectEventStateKey( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventStateKey: rows.close() failed") result := make(map[types.EventStateKeyNID]string, len(eventStateKeyNIDs)) + var stateKey string + var stateKeyNID int64 for rows.Next() { - var stateKey string - var stateKeyNID int64 if err := rows.Scan(&stateKey, &stateKeyNID); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/event_types_table.go b/roomserver/storage/postgres/event_types_table.go index 1d5de5822..15ab7fd8e 100644 --- a/roomserver/storage/postgres/event_types_table.go +++ b/roomserver/storage/postgres/event_types_table.go @@ -99,12 +99,12 @@ type eventTypeStatements struct { bulkSelectEventTypeNIDStmt *sql.Stmt } -func createEventTypesTable(db *sql.DB) error { +func CreateEventTypesTable(db *sql.DB) error { _, err := db.Exec(eventTypesSchema) return err } -func prepareEventTypesTable(db *sql.DB) (tables.EventTypes, error) { +func PrepareEventTypesTable(db *sql.DB) (tables.EventTypes, error) { s := &eventTypeStatements{} return s, sqlutil.StatementList{ @@ -143,9 +143,9 @@ func (s *eventTypeStatements) BulkSelectEventTypeNID( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventTypeNID: rows.close() failed") result := make(map[string]types.EventTypeNID, len(eventTypes)) + var eventType string + var eventTypeNID int64 for rows.Next() { - var eventType string - var eventTypeNID int64 if err := rows.Scan(&eventType, &eventTypeNID); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/events_table.go b/roomserver/storage/postgres/events_table.go index 8012174a0..a4d05756d 100644 --- a/roomserver/storage/postgres/events_table.go +++ b/roomserver/storage/postgres/events_table.go @@ -155,12 +155,12 @@ type eventStatements struct { selectRoomNIDsForEventNIDsStmt *sql.Stmt } -func createEventsTable(db *sql.DB) error { +func CreateEventsTable(db *sql.DB) error { _, err := db.Exec(eventsSchema) return err } -func prepareEventsTable(db *sql.DB) (tables.Events, error) { +func PrepareEventsTable(db *sql.DB) (tables.Events, error) { s := &eventStatements{} return s, sqlutil.StatementList{ @@ -264,11 +264,11 @@ func (s *eventStatements) BulkSelectStateEventByNID( ctx context.Context, txn *sql.Tx, eventNIDs []types.EventNID, stateKeyTuples []types.StateKeyTuple, ) ([]types.StateEntry, error) { - tuples := stateKeyTupleSorter(stateKeyTuples) + tuples := types.StateKeyTupleSorter(stateKeyTuples) sort.Sort(tuples) - eventTypeNIDArray, eventStateKeyNIDArray := tuples.typesAndStateKeysAsArrays() + eventTypeNIDArray, eventStateKeyNIDArray := tuples.TypesAndStateKeysAsArrays() stmt := sqlutil.TxStmt(txn, s.bulkSelectStateEventByNIDStmt) - rows, err := stmt.QueryContext(ctx, eventNIDsAsArray(eventNIDs), eventTypeNIDArray, eventStateKeyNIDArray) + rows, err := stmt.QueryContext(ctx, eventNIDsAsArray(eventNIDs), pq.Int64Array(eventTypeNIDArray), pq.Int64Array(eventStateKeyNIDArray)) if err != nil { return nil, err } @@ -380,15 +380,15 @@ func (s *eventStatements) BulkSelectStateAtEventAndReference( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectStateAtEventAndReference: rows.close() failed") results := make([]types.StateAtEventAndReference, len(eventNIDs)) i := 0 + var ( + eventTypeNID int64 + eventStateKeyNID int64 + eventNID int64 + stateSnapshotNID int64 + eventID string + eventSHA256 []byte + ) for ; rows.Next(); i++ { - var ( - eventTypeNID int64 - eventStateKeyNID int64 - eventNID int64 - stateSnapshotNID int64 - eventID string - eventSHA256 []byte - ) if err = rows.Scan( &eventTypeNID, &eventStateKeyNID, &eventNID, &stateSnapshotNID, &eventID, &eventSHA256, ); err != nil { @@ -446,9 +446,9 @@ func (s *eventStatements) BulkSelectEventID(ctx context.Context, txn *sql.Tx, ev defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventID: rows.close() failed") results := make(map[types.EventNID]string, len(eventNIDs)) i := 0 + var eventNID int64 + var eventID string for ; rows.Next(); i++ { - var eventNID int64 - var eventID string if err = rows.Scan(&eventNID, &eventID); err != nil { return nil, err } @@ -491,9 +491,9 @@ func (s *eventStatements) bulkSelectEventNID(ctx context.Context, txn *sql.Tx, e } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventNID: rows.close() failed") results := make(map[string]types.EventNID, len(eventIDs)) + var eventID string + var eventNID int64 for rows.Next() { - var eventID string - var eventNID int64 if err = rows.Scan(&eventID, &eventNID); err != nil { return nil, err } @@ -522,9 +522,9 @@ func (s *eventStatements) SelectRoomNIDsForEventNIDs( } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomNIDsForEventNIDsStmt: rows.close() failed") result := make(map[types.EventNID]types.RoomNID) + var eventNID types.EventNID + var roomNID types.RoomNID for rows.Next() { - var eventNID types.EventNID - var roomNID types.RoomNID if err = rows.Scan(&eventNID, &roomNID); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/invite_table.go b/roomserver/storage/postgres/invite_table.go index 176c16e48..4cddfe2e9 100644 --- a/roomserver/storage/postgres/invite_table.go +++ b/roomserver/storage/postgres/invite_table.go @@ -81,12 +81,12 @@ type inviteStatements struct { updateInviteRetiredStmt *sql.Stmt } -func createInvitesTable(db *sql.DB) error { +func CreateInvitesTable(db *sql.DB) error { _, err := db.Exec(inviteSchema) return err } -func prepareInvitesTable(db *sql.DB) (tables.Invites, error) { +func PrepareInvitesTable(db *sql.DB) (tables.Invites, error) { s := &inviteStatements{} return s, sqlutil.StatementList{ @@ -127,8 +127,8 @@ func (s *inviteStatements) UpdateInviteRetired( defer internal.CloseAndLogIfError(ctx, rows, "updateInviteRetired: rows.close() failed") var eventIDs []string + var inviteEventID string for rows.Next() { - var inviteEventID string if err = rows.Scan(&inviteEventID); err != nil { return nil, err } @@ -152,9 +152,9 @@ func (s *inviteStatements) SelectInviteActiveForUserInRoom( defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed") var result []types.EventStateKeyNID var eventIDs []string + var inviteEventID string + var senderUserNID int64 for rows.Next() { - var inviteEventID string - var senderUserNID int64 if err := rows.Scan(&inviteEventID, &senderUserNID); err != nil { return nil, nil, err } diff --git a/roomserver/storage/postgres/membership_table.go b/roomserver/storage/postgres/membership_table.go index 8d0b818b7..4cb675613 100644 --- a/roomserver/storage/postgres/membership_table.go +++ b/roomserver/storage/postgres/membership_table.go @@ -161,7 +161,7 @@ type membershipStatements struct { selectServerInRoomStmt *sql.Stmt } -func createMembershipTable(db *sql.DB) error { +func CreateMembershipTable(db *sql.DB) error { _, err := db.Exec(membershipSchema) if err != nil { return err @@ -174,7 +174,7 @@ func createMembershipTable(db *sql.DB) error { return m.Up(context.Background()) } -func prepareMembershipTable(db *sql.DB) (tables.Membership, error) { +func PrepareMembershipTable(db *sql.DB) (tables.Membership, error) { s := &membershipStatements{} return s, sqlutil.StatementList{ @@ -243,8 +243,8 @@ func (s *membershipStatements) SelectMembershipsFromRoom( } defer internal.CloseAndLogIfError(ctx, rows, "selectMembershipsFromRoom: rows.close() failed") + var eNID types.EventNID for rows.Next() { - var eNID types.EventNID if err = rows.Scan(&eNID); err != nil { return } @@ -271,8 +271,8 @@ func (s *membershipStatements) SelectMembershipsFromRoomAndMembership( } defer internal.CloseAndLogIfError(ctx, rows, "selectMembershipsFromRoomAndMembership: rows.close() failed") + var eNID types.EventNID for rows.Next() { - var eNID types.EventNID if err = rows.Scan(&eNID); err != nil { return } @@ -307,8 +307,8 @@ func (s *membershipStatements) SelectRoomsWithMembership( } defer internal.CloseAndLogIfError(ctx, rows, "SelectRoomsWithMembership: rows.close() failed") var roomNIDs []types.RoomNID + var roomNID types.RoomNID for rows.Next() { - var roomNID types.RoomNID if err := rows.Scan(&roomNID); err != nil { return nil, err } @@ -329,9 +329,9 @@ func (s *membershipStatements) SelectJoinedUsersSetForRooms( } defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsersSetForRooms: rows.close() failed") result := make(map[types.EventStateKeyNID]int) + var userID types.EventStateKeyNID + var count int for rows.Next() { - var userID types.EventStateKeyNID - var count int if err := rows.Scan(&userID, &count); err != nil { return nil, err } @@ -351,12 +351,12 @@ func (s *membershipStatements) SelectKnownUsers( } result := []string{} defer internal.CloseAndLogIfError(ctx, rows, "SelectKnownUsers: rows.close() failed") + var resUserID string for rows.Next() { - var userID string - if err := rows.Scan(&userID); err != nil { + if err := rows.Scan(&resUserID); err != nil { return nil, err } - result = append(result, userID) + result = append(result, resUserID) } return result, rows.Err() } diff --git a/roomserver/storage/postgres/previous_events_table.go b/roomserver/storage/postgres/previous_events_table.go index bd4e853eb..26999a290 100644 --- a/roomserver/storage/postgres/previous_events_table.go +++ b/roomserver/storage/postgres/previous_events_table.go @@ -64,12 +64,12 @@ type previousEventStatements struct { selectPreviousEventExistsStmt *sql.Stmt } -func createPrevEventsTable(db *sql.DB) error { +func CreatePrevEventsTable(db *sql.DB) error { _, err := db.Exec(previousEventSchema) return err } -func preparePrevEventsTable(db *sql.DB) (tables.PreviousEvents, error) { +func PreparePrevEventsTable(db *sql.DB) (tables.PreviousEvents, error) { s := &previousEventStatements{} return s, sqlutil.StatementList{ diff --git a/roomserver/storage/postgres/published_table.go b/roomserver/storage/postgres/published_table.go index 15985fcd6..56fa02f7b 100644 --- a/roomserver/storage/postgres/published_table.go +++ b/roomserver/storage/postgres/published_table.go @@ -49,12 +49,12 @@ type publishedStatements struct { selectPublishedStmt *sql.Stmt } -func createPublishedTable(db *sql.DB) error { +func CreatePublishedTable(db *sql.DB) error { _, err := db.Exec(publishedSchema) return err } -func preparePublishedTable(db *sql.DB) (tables.Published, error) { +func PreparePublishedTable(db *sql.DB) (tables.Published, error) { s := &publishedStatements{} return s, sqlutil.StatementList{ @@ -94,8 +94,8 @@ func (s *publishedStatements) SelectAllPublishedRooms( defer internal.CloseAndLogIfError(ctx, rows, "selectAllPublishedStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/redactions_table.go b/roomserver/storage/postgres/redactions_table.go index 5614f2bd8..6e2f6712d 100644 --- a/roomserver/storage/postgres/redactions_table.go +++ b/roomserver/storage/postgres/redactions_table.go @@ -59,12 +59,12 @@ type redactionStatements struct { markRedactionValidatedStmt *sql.Stmt } -func createRedactionsTable(db *sql.DB) error { +func CreateRedactionsTable(db *sql.DB) error { _, err := db.Exec(redactionsSchema) return err } -func prepareRedactionsTable(db *sql.DB) (tables.Redactions, error) { +func PrepareRedactionsTable(db *sql.DB) (tables.Redactions, error) { s := &redactionStatements{} return s, sqlutil.StatementList{ diff --git a/roomserver/storage/postgres/room_aliases_table.go b/roomserver/storage/postgres/room_aliases_table.go index d13df8e7f..a84929f61 100644 --- a/roomserver/storage/postgres/room_aliases_table.go +++ b/roomserver/storage/postgres/room_aliases_table.go @@ -61,12 +61,12 @@ type roomAliasesStatements struct { deleteRoomAliasStmt *sql.Stmt } -func createRoomAliasesTable(db *sql.DB) error { +func CreateRoomAliasesTable(db *sql.DB) error { _, err := db.Exec(roomAliasesSchema) return err } -func prepareRoomAliasesTable(db *sql.DB) (tables.RoomAliases, error) { +func PrepareRoomAliasesTable(db *sql.DB) (tables.RoomAliases, error) { s := &roomAliasesStatements{} return s, sqlutil.StatementList{ @@ -108,8 +108,8 @@ func (s *roomAliasesStatements) SelectAliasesFromRoomID( defer internal.CloseAndLogIfError(ctx, rows, "selectAliasesFromRoomID: rows.close() failed") var aliases []string + var alias string for rows.Next() { - var alias string if err = rows.Scan(&alias); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/rooms_table.go b/roomserver/storage/postgres/rooms_table.go index b2685084d..24362af74 100644 --- a/roomserver/storage/postgres/rooms_table.go +++ b/roomserver/storage/postgres/rooms_table.go @@ -95,12 +95,12 @@ type roomStatements struct { bulkSelectRoomNIDsStmt *sql.Stmt } -func createRoomsTable(db *sql.DB) error { +func CreateRoomsTable(db *sql.DB) error { _, err := db.Exec(roomsSchema) return err } -func prepareRoomsTable(db *sql.DB) (tables.Rooms, error) { +func PrepareRoomsTable(db *sql.DB) (tables.Rooms, error) { s := &roomStatements{} return s, sqlutil.StatementList{ @@ -117,7 +117,7 @@ func prepareRoomsTable(db *sql.DB) (tables.Rooms, error) { }.Prepare(db) } -func (s *roomStatements) SelectRoomIDs(ctx context.Context, txn *sql.Tx) ([]string, error) { +func (s *roomStatements) SelectRoomIDsWithEvents(ctx context.Context, txn *sql.Tx) ([]string, error) { stmt := sqlutil.TxStmt(txn, s.selectRoomIDsStmt) rows, err := stmt.QueryContext(ctx) if err != nil { @@ -125,8 +125,8 @@ func (s *roomStatements) SelectRoomIDs(ctx context.Context, txn *sql.Tx) ([]stri } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomIDsStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } @@ -231,9 +231,9 @@ func (s *roomStatements) SelectRoomVersionsForRoomNIDs( } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomVersionsForRoomNIDsStmt: rows.close() failed") result := make(map[types.RoomNID]gomatrixserverlib.RoomVersion) + var roomNID types.RoomNID + var roomVersion gomatrixserverlib.RoomVersion for rows.Next() { - var roomNID types.RoomNID - var roomVersion gomatrixserverlib.RoomVersion if err = rows.Scan(&roomNID, &roomVersion); err != nil { return nil, err } @@ -254,8 +254,8 @@ func (s *roomStatements) BulkSelectRoomIDs(ctx context.Context, txn *sql.Tx, roo } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectRoomIDsStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } @@ -276,8 +276,8 @@ func (s *roomStatements) BulkSelectRoomNIDs(ctx context.Context, txn *sql.Tx, ro } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectRoomNIDsStmt: rows.close() failed") var roomNIDs []types.RoomNID + var roomNID types.RoomNID for rows.Next() { - var roomNID types.RoomNID if err = rows.Scan(&roomNID); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/state_block_table.go b/roomserver/storage/postgres/state_block_table.go index 6f8f9e1b5..5af48f031 100644 --- a/roomserver/storage/postgres/state_block_table.go +++ b/roomserver/storage/postgres/state_block_table.go @@ -19,7 +19,6 @@ import ( "context" "database/sql" "fmt" - "sort" "github.com/lib/pq" "github.com/matrix-org/dendrite/internal" @@ -71,12 +70,12 @@ type stateBlockStatements struct { bulkSelectStateBlockEntriesStmt *sql.Stmt } -func createStateBlockTable(db *sql.DB) error { +func CreateStateBlockTable(db *sql.DB) error { _, err := db.Exec(stateDataSchema) return err } -func prepareStateBlockTable(db *sql.DB) (tables.StateBlock, error) { +func PrepareStateBlockTable(db *sql.DB) (tables.StateBlock, error) { s := &stateBlockStatements{} return s, sqlutil.StatementList{ @@ -90,9 +89,9 @@ func (s *stateBlockStatements) BulkInsertStateData( entries types.StateEntries, ) (id types.StateBlockNID, err error) { entries = entries[:util.SortAndUnique(entries)] - var nids types.EventNIDs - for _, e := range entries { - nids = append(nids, e.EventNID) + nids := make(types.EventNIDs, entries.Len()) + for i := range entries { + nids[i] = entries[i].EventNID } stmt := sqlutil.TxStmt(txn, s.insertStateDataStmt) err = stmt.QueryRowContext( @@ -113,15 +112,15 @@ func (s *stateBlockStatements) BulkSelectStateBlockEntries( results := make([][]types.EventNID, len(stateBlockNIDs)) i := 0 + var stateBlockNID types.StateBlockNID + var result pq.Int64Array for ; rows.Next(); i++ { - var stateBlockNID types.StateBlockNID - var result pq.Int64Array if err = rows.Scan(&stateBlockNID, &result); err != nil { return nil, err } - r := []types.EventNID{} - for _, e := range result { - r = append(r, types.EventNID(e)) + r := make([]types.EventNID, len(result)) + for x := range result { + r[x] = types.EventNID(result[x]) } results[i] = r } @@ -141,35 +140,3 @@ func stateBlockNIDsAsArray(stateBlockNIDs []types.StateBlockNID) pq.Int64Array { } return pq.Int64Array(nids) } - -type stateKeyTupleSorter []types.StateKeyTuple - -func (s stateKeyTupleSorter) Len() int { return len(s) } -func (s stateKeyTupleSorter) Less(i, j int) bool { return s[i].LessThan(s[j]) } -func (s stateKeyTupleSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Check whether a tuple is in the list. Assumes that the list is sorted. -func (s stateKeyTupleSorter) contains(value types.StateKeyTuple) bool { - i := sort.Search(len(s), func(i int) bool { return !s[i].LessThan(value) }) - return i < len(s) && s[i] == value -} - -// List the unique eventTypeNIDs and eventStateKeyNIDs. -// Assumes that the list is sorted. -func (s stateKeyTupleSorter) typesAndStateKeysAsArrays() (eventTypeNIDs pq.Int64Array, eventStateKeyNIDs pq.Int64Array) { - eventTypeNIDs = make(pq.Int64Array, len(s)) - eventStateKeyNIDs = make(pq.Int64Array, len(s)) - for i := range s { - eventTypeNIDs[i] = int64(s[i].EventTypeNID) - eventStateKeyNIDs[i] = int64(s[i].EventStateKeyNID) - } - eventTypeNIDs = eventTypeNIDs[:util.SortAndUnique(int64Sorter(eventTypeNIDs))] - eventStateKeyNIDs = eventStateKeyNIDs[:util.SortAndUnique(int64Sorter(eventStateKeyNIDs))] - return -} - -type int64Sorter []int64 - -func (s int64Sorter) Len() int { return len(s) } -func (s int64Sorter) Less(i, j int) bool { return s[i] < s[j] } -func (s int64Sorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/roomserver/storage/postgres/state_block_table_test.go b/roomserver/storage/postgres/state_block_table_test.go deleted file mode 100644 index a0e2ec952..000000000 --- a/roomserver/storage/postgres/state_block_table_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2017-2018 New Vector Ltd -// Copyright 2019-2020 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package postgres - -import ( - "sort" - "testing" - - "github.com/matrix-org/dendrite/roomserver/types" -) - -func TestStateKeyTupleSorter(t *testing.T) { - input := stateKeyTupleSorter{ - {EventTypeNID: 1, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 4}, - {EventTypeNID: 2, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 1}, - } - want := []types.StateKeyTuple{ - {EventTypeNID: 1, EventStateKeyNID: 1}, - {EventTypeNID: 1, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 4}, - {EventTypeNID: 2, EventStateKeyNID: 2}, - } - doNotWant := []types.StateKeyTuple{ - {EventTypeNID: 0, EventStateKeyNID: 0}, - {EventTypeNID: 1, EventStateKeyNID: 3}, - {EventTypeNID: 2, EventStateKeyNID: 1}, - {EventTypeNID: 3, EventStateKeyNID: 1}, - } - wantTypeNIDs := []int64{1, 2} - wantStateKeyNIDs := []int64{1, 2, 4} - - // Sort the input and check it's in the right order. - sort.Sort(input) - gotTypeNIDs, gotStateKeyNIDs := input.typesAndStateKeysAsArrays() - - for i := range want { - if input[i] != want[i] { - t.Errorf("Wanted %#v at index %d got %#v", want[i], i, input[i]) - } - - if !input.contains(want[i]) { - t.Errorf("Wanted %#v.contains(%#v) to be true but got false", input, want[i]) - } - } - - for i := range doNotWant { - if input.contains(doNotWant[i]) { - t.Errorf("Wanted %#v.contains(%#v) to be false but got true", input, doNotWant[i]) - } - } - - if len(wantTypeNIDs) != len(gotTypeNIDs) { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - - for i := range wantTypeNIDs { - if wantTypeNIDs[i] != gotTypeNIDs[i] { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - } - - if len(wantStateKeyNIDs) != len(gotStateKeyNIDs) { - t.Fatalf("Wanted state key NIDs %#v got %#v", wantStateKeyNIDs, gotStateKeyNIDs) - } - - for i := range wantStateKeyNIDs { - if wantStateKeyNIDs[i] != gotStateKeyNIDs[i] { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - } -} diff --git a/roomserver/storage/postgres/state_snapshot_table.go b/roomserver/storage/postgres/state_snapshot_table.go index 8ed886030..a24b7f3f0 100644 --- a/roomserver/storage/postgres/state_snapshot_table.go +++ b/roomserver/storage/postgres/state_snapshot_table.go @@ -77,12 +77,12 @@ type stateSnapshotStatements struct { bulkSelectStateBlockNIDsStmt *sql.Stmt } -func createStateSnapshotTable(db *sql.DB) error { +func CreateStateSnapshotTable(db *sql.DB) error { _, err := db.Exec(stateSnapshotSchema) return err } -func prepareStateSnapshotTable(db *sql.DB) (tables.StateSnapshot, error) { +func PrepareStateSnapshotTable(db *sql.DB) (tables.StateSnapshot, error) { s := &stateSnapshotStatements{} return s, sqlutil.StatementList{ @@ -95,12 +95,10 @@ func (s *stateSnapshotStatements) InsertState( ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, nids types.StateBlockNIDs, ) (stateNID types.StateSnapshotNID, err error) { nids = nids[:util.SortAndUnique(nids)] - var id int64 - err = sqlutil.TxStmt(txn, s.insertStateStmt).QueryRowContext(ctx, nids.Hash(), int64(roomNID), stateBlockNIDsAsArray(nids)).Scan(&id) + err = sqlutil.TxStmt(txn, s.insertStateStmt).QueryRowContext(ctx, nids.Hash(), int64(roomNID), stateBlockNIDsAsArray(nids)).Scan(&stateNID) if err != nil { return 0, err } - stateNID = types.StateSnapshotNID(id) return } @@ -119,9 +117,9 @@ func (s *stateSnapshotStatements) BulkSelectStateBlockNIDs( defer rows.Close() // nolint: errcheck results := make([]types.StateBlockNIDList, len(stateNIDs)) i := 0 + var stateBlockNIDs pq.Int64Array for ; rows.Next(); i++ { result := &results[i] - var stateBlockNIDs pq.Int64Array if err = rows.Scan(&result.StateSnapshotNID, &stateBlockNIDs); err != nil { return nil, err } diff --git a/roomserver/storage/postgres/storage.go b/roomserver/storage/postgres/storage.go index 5a113c229..5331eb848 100644 --- a/roomserver/storage/postgres/storage.go +++ b/roomserver/storage/postgres/storage.go @@ -26,6 +26,7 @@ import ( "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/roomserver/storage/postgres/deltas" "github.com/matrix-org/dendrite/roomserver/storage/shared" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) @@ -35,11 +36,11 @@ type Database struct { } // Open a postgres database. -func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) { +func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) { var d Database - var db *sql.DB var err error - if db, err = sqlutil.Open(dbProperties); err != nil { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()) + if err != nil { return nil, fmt.Errorf("sqlutil.Open: %w", err) } @@ -68,7 +69,7 @@ func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) // Then prepare the statements. Now that the migrations have run, any columns referred // to in the database code should now exist. - if err := d.prepare(db, cache); err != nil { + if err := d.prepare(db, writer, cache); err != nil { return nil, err } @@ -76,106 +77,106 @@ func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) } func (d *Database) create(db *sql.DB) error { - if err := createEventStateKeysTable(db); err != nil { + if err := CreateEventStateKeysTable(db); err != nil { return err } - if err := createEventTypesTable(db); err != nil { + if err := CreateEventTypesTable(db); err != nil { return err } - if err := createEventJSONTable(db); err != nil { + if err := CreateEventJSONTable(db); err != nil { return err } - if err := createEventsTable(db); err != nil { + if err := CreateEventsTable(db); err != nil { return err } - if err := createRoomsTable(db); err != nil { + if err := CreateRoomsTable(db); err != nil { return err } - if err := createStateBlockTable(db); err != nil { + if err := CreateStateBlockTable(db); err != nil { return err } - if err := createStateSnapshotTable(db); err != nil { + if err := CreateStateSnapshotTable(db); err != nil { return err } - if err := createPrevEventsTable(db); err != nil { + if err := CreatePrevEventsTable(db); err != nil { return err } - if err := createRoomAliasesTable(db); err != nil { + if err := CreateRoomAliasesTable(db); err != nil { return err } - if err := createInvitesTable(db); err != nil { + if err := CreateInvitesTable(db); err != nil { return err } - if err := createMembershipTable(db); err != nil { + if err := CreateMembershipTable(db); err != nil { return err } - if err := createPublishedTable(db); err != nil { + if err := CreatePublishedTable(db); err != nil { return err } - if err := createRedactionsTable(db); err != nil { + if err := CreateRedactionsTable(db); err != nil { return err } return nil } -func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { - eventStateKeys, err := prepareEventStateKeysTable(db) +func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { + eventStateKeys, err := PrepareEventStateKeysTable(db) if err != nil { return err } - eventTypes, err := prepareEventTypesTable(db) + eventTypes, err := PrepareEventTypesTable(db) if err != nil { return err } - eventJSON, err := prepareEventJSONTable(db) + eventJSON, err := PrepareEventJSONTable(db) if err != nil { return err } - events, err := prepareEventsTable(db) + events, err := PrepareEventsTable(db) if err != nil { return err } - rooms, err := prepareRoomsTable(db) + rooms, err := PrepareRoomsTable(db) if err != nil { return err } - stateBlock, err := prepareStateBlockTable(db) + stateBlock, err := PrepareStateBlockTable(db) if err != nil { return err } - stateSnapshot, err := prepareStateSnapshotTable(db) + stateSnapshot, err := PrepareStateSnapshotTable(db) if err != nil { return err } - prevEvents, err := preparePrevEventsTable(db) + prevEvents, err := PreparePrevEventsTable(db) if err != nil { return err } - roomAliases, err := prepareRoomAliasesTable(db) + roomAliases, err := PrepareRoomAliasesTable(db) if err != nil { return err } - invites, err := prepareInvitesTable(db) + invites, err := PrepareInvitesTable(db) if err != nil { return err } - membership, err := prepareMembershipTable(db) + membership, err := PrepareMembershipTable(db) if err != nil { return err } - published, err := preparePublishedTable(db) + published, err := PreparePublishedTable(db) if err != nil { return err } - redactions, err := prepareRedactionsTable(db) + redactions, err := PrepareRedactionsTable(db) if err != nil { return err } d.Database = shared.Database{ DB: db, Cache: cache, - Writer: sqlutil.NewDummyWriter(), + Writer: writer, EventTypesTable: eventTypes, EventStateKeysTable: eventStateKeys, EventJSONTable: eventJSON, diff --git a/roomserver/storage/shared/storage.go b/roomserver/storage/shared/storage.go index 252e94c7e..cc4a9fff5 100644 --- a/roomserver/storage/shared/storage.go +++ b/roomserver/storage/shared/storage.go @@ -1216,7 +1216,7 @@ func (d *Database) GetKnownUsers(ctx context.Context, userID, searchString strin // GetKnownRooms returns a list of all rooms we know about. func (d *Database) GetKnownRooms(ctx context.Context) ([]string, error) { - return d.RoomsTable.SelectRoomIDs(ctx, nil) + return d.RoomsTable.SelectRoomIDsWithEvents(ctx, nil) } // ForgetRoom sets a users room to forgotten diff --git a/roomserver/storage/sqlite3/event_json_table.go b/roomserver/storage/sqlite3/event_json_table.go index f470ea326..dc26885bb 100644 --- a/roomserver/storage/sqlite3/event_json_table.go +++ b/roomserver/storage/sqlite3/event_json_table.go @@ -52,12 +52,12 @@ type eventJSONStatements struct { bulkSelectEventJSONStmt *sql.Stmt } -func createEventJSONTable(db *sql.DB) error { +func CreateEventJSONTable(db *sql.DB) error { _, err := db.Exec(eventJSONSchema) return err } -func prepareEventJSONTable(db *sql.DB) (tables.EventJSON, error) { +func PrepareEventJSONTable(db *sql.DB) (tables.EventJSON, error) { s := &eventJSONStatements{ db: db, } @@ -101,9 +101,9 @@ func (s *eventJSONStatements) BulkSelectEventJSON( // We might get fewer results than NIDs so we adjust the length of the slice before returning it. results := make([]tables.EventJSONPair, len(eventNIDs)) i := 0 + var eventNID int64 for ; rows.Next(); i++ { result := &results[i] - var eventNID int64 if err := rows.Scan(&eventNID, &result.EventJSON); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/event_state_keys_table.go b/roomserver/storage/sqlite3/event_state_keys_table.go index f97541f4a..347524a81 100644 --- a/roomserver/storage/sqlite3/event_state_keys_table.go +++ b/roomserver/storage/sqlite3/event_state_keys_table.go @@ -71,12 +71,12 @@ type eventStateKeyStatements struct { bulkSelectEventStateKeyStmt *sql.Stmt } -func createEventStateKeysTable(db *sql.DB) error { +func CreateEventStateKeysTable(db *sql.DB) error { _, err := db.Exec(eventStateKeysSchema) return err } -func prepareEventStateKeysTable(db *sql.DB) (tables.EventStateKeys, error) { +func PrepareEventStateKeysTable(db *sql.DB) (tables.EventStateKeys, error) { s := &eventStateKeyStatements{ db: db, } @@ -128,9 +128,9 @@ func (s *eventStateKeyStatements) BulkSelectEventStateKeyNID( } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventStateKeyNID: rows.close() failed") result := make(map[string]types.EventStateKeyNID, len(eventStateKeys)) + var stateKey string + var stateKeyNID int64 for rows.Next() { - var stateKey string - var stateKeyNID int64 if err := rows.Scan(&stateKey, &stateKeyNID); err != nil { return nil, err } @@ -159,9 +159,9 @@ func (s *eventStateKeyStatements) BulkSelectEventStateKey( } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventStateKey: rows.close() failed") result := make(map[types.EventStateKeyNID]string, len(eventStateKeyNIDs)) + var stateKey string + var stateKeyNID int64 for rows.Next() { - var stateKey string - var stateKeyNID int64 if err := rows.Scan(&stateKey, &stateKeyNID); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/event_types_table.go b/roomserver/storage/sqlite3/event_types_table.go index c49cc509a..0581ec194 100644 --- a/roomserver/storage/sqlite3/event_types_table.go +++ b/roomserver/storage/sqlite3/event_types_table.go @@ -79,12 +79,12 @@ type eventTypeStatements struct { bulkSelectEventTypeNIDStmt *sql.Stmt } -func createEventTypesTable(db *sql.DB) error { +func CreateEventTypesTable(db *sql.DB) error { _, err := db.Exec(eventTypesSchema) return err } -func prepareEventTypesTable(db *sql.DB) (tables.EventTypes, error) { +func PrepareEventTypesTable(db *sql.DB) (tables.EventTypes, error) { s := &eventTypeStatements{ db: db, } @@ -139,9 +139,9 @@ func (s *eventTypeStatements) BulkSelectEventTypeNID( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventTypeNID: rows.close() failed") result := make(map[string]types.EventTypeNID, len(eventTypes)) + var eventType string + var eventTypeNID int64 for rows.Next() { - var eventType string - var eventTypeNID int64 if err := rows.Scan(&eventType, &eventTypeNID); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/events_table.go b/roomserver/storage/sqlite3/events_table.go index 45b49e5cb..1dda34c36 100644 --- a/roomserver/storage/sqlite3/events_table.go +++ b/roomserver/storage/sqlite3/events_table.go @@ -68,7 +68,8 @@ const bulkSelectStateEventByIDSQL = "" + const bulkSelectStateEventByNIDSQL = "" + "SELECT event_type_nid, event_state_key_nid, event_nid FROM roomserver_events" + " WHERE event_nid IN ($1)" - // Rest of query is built by BulkSelectStateEventByNID + +// Rest of query is built by BulkSelectStateEventByNID const bulkSelectStateAtEventByIDSQL = "" + "SELECT event_type_nid, event_state_key_nid, event_nid, state_snapshot_nid, is_rejected FROM roomserver_events" + @@ -126,12 +127,12 @@ type eventStatements struct { //selectRoomNIDsForEventNIDsStmt *sql.Stmt } -func createEventsTable(db *sql.DB) error { +func CreateEventsTable(db *sql.DB) error { _, err := db.Exec(eventsSchema) return err } -func prepareEventsTable(db *sql.DB) (tables.Events, error) { +func PrepareEventsTable(db *sql.DB) (tables.Events, error) { s := &eventStatements{ db: db, } @@ -246,9 +247,9 @@ func (s *eventStatements) BulkSelectStateEventByNID( ctx context.Context, txn *sql.Tx, eventNIDs []types.EventNID, stateKeyTuples []types.StateKeyTuple, ) ([]types.StateEntry, error) { - tuples := stateKeyTupleSorter(stateKeyTuples) + tuples := types.StateKeyTupleSorter(stateKeyTuples) sort.Sort(tuples) - eventTypeNIDArray, eventStateKeyNIDArray := tuples.typesAndStateKeysAsArrays() + eventTypeNIDArray, eventStateKeyNIDArray := tuples.TypesAndStateKeysAsArrays() params := make([]interface{}, 0, len(eventNIDs)+len(eventTypeNIDArray)+len(eventStateKeyNIDArray)) selectOrig := strings.Replace(bulkSelectStateEventByNIDSQL, "($1)", sqlutil.QueryVariadic(len(eventNIDs)), 1) for _, v := range eventNIDs { @@ -404,15 +405,15 @@ func (s *eventStatements) BulkSelectStateAtEventAndReference( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectStateAtEventAndReference: rows.close() failed") results := make([]types.StateAtEventAndReference, len(eventNIDs)) i := 0 + var ( + eventTypeNID int64 + eventStateKeyNID int64 + eventNID int64 + stateSnapshotNID int64 + eventID string + eventSHA256 []byte + ) for ; rows.Next(); i++ { - var ( - eventTypeNID int64 - eventStateKeyNID int64 - eventNID int64 - stateSnapshotNID int64 - eventID string - eventSHA256 []byte - ) if err = rows.Scan( &eventTypeNID, &eventStateKeyNID, &eventNID, &stateSnapshotNID, &eventID, &eventSHA256, ); err != nil { @@ -491,9 +492,9 @@ func (s *eventStatements) BulkSelectEventID(ctx context.Context, txn *sql.Tx, ev defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventID: rows.close() failed") results := make(map[types.EventNID]string, len(eventNIDs)) i := 0 + var eventNID int64 + var eventID string for ; rows.Next(); i++ { - var eventNID int64 - var eventID string if err = rows.Scan(&eventNID, &eventID); err != nil { return nil, err } @@ -545,9 +546,9 @@ func (s *eventStatements) bulkSelectEventNID(ctx context.Context, txn *sql.Tx, e } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectEventNID: rows.close() failed") results := make(map[string]types.EventNID, len(eventIDs)) + var eventID string + var eventNID int64 for rows.Next() { - var eventID string - var eventNID int64 if err = rows.Scan(&eventID, &eventNID); err != nil { return nil, err } @@ -595,9 +596,9 @@ func (s *eventStatements) SelectRoomNIDsForEventNIDs( } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomNIDsForEventNIDsStmt: rows.close() failed") result := make(map[types.EventNID]types.RoomNID) + var eventNID types.EventNID + var roomNID types.RoomNID for rows.Next() { - var eventNID types.EventNID - var roomNID types.RoomNID if err = rows.Scan(&eventNID, &roomNID); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/invite_table.go b/roomserver/storage/sqlite3/invite_table.go index d54d313a9..e051d63af 100644 --- a/roomserver/storage/sqlite3/invite_table.go +++ b/roomserver/storage/sqlite3/invite_table.go @@ -69,12 +69,12 @@ type inviteStatements struct { selectInvitesAboutToRetireStmt *sql.Stmt } -func createInvitesTable(db *sql.DB) error { +func CreateInvitesTable(db *sql.DB) error { _, err := db.Exec(inviteSchema) return err } -func prepareInvitesTable(db *sql.DB) (tables.Invites, error) { +func PrepareInvitesTable(db *sql.DB) (tables.Invites, error) { s := &inviteStatements{ db: db, } @@ -119,8 +119,8 @@ func (s *inviteStatements) UpdateInviteRetired( return } defer internal.CloseAndLogIfError(ctx, rows, "UpdateInviteRetired: rows.close() failed") + var inviteEventID string for rows.Next() { - var inviteEventID string if err = rows.Scan(&inviteEventID); err != nil { return } @@ -147,9 +147,9 @@ func (s *inviteStatements) SelectInviteActiveForUserInRoom( defer internal.CloseAndLogIfError(ctx, rows, "selectInviteActiveForUserInRoom: rows.close() failed") var result []types.EventStateKeyNID var eventIDs []string + var eventID string + var senderUserNID int64 for rows.Next() { - var eventID string - var senderUserNID int64 if err := rows.Scan(&eventID, &senderUserNID); err != nil { return nil, nil, err } diff --git a/roomserver/storage/sqlite3/membership_table.go b/roomserver/storage/sqlite3/membership_table.go index a57559484..7eebe4ee6 100644 --- a/roomserver/storage/sqlite3/membership_table.go +++ b/roomserver/storage/sqlite3/membership_table.go @@ -137,7 +137,7 @@ type membershipStatements struct { selectServerInRoomStmt *sql.Stmt } -func createMembershipTable(db *sql.DB) error { +func CreateMembershipTable(db *sql.DB) error { _, err := db.Exec(membershipSchema) if err != nil { return err @@ -150,7 +150,7 @@ func createMembershipTable(db *sql.DB) error { return m.Up(context.Background()) } -func prepareMembershipTable(db *sql.DB) (tables.Membership, error) { +func PrepareMembershipTable(db *sql.DB) (tables.Membership, error) { s := &membershipStatements{ db: db, } @@ -221,8 +221,8 @@ func (s *membershipStatements) SelectMembershipsFromRoom( } defer internal.CloseAndLogIfError(ctx, rows, "selectMembershipsFromRoom: rows.close() failed") + var eNID types.EventNID for rows.Next() { - var eNID types.EventNID if err = rows.Scan(&eNID); err != nil { return } @@ -248,8 +248,8 @@ func (s *membershipStatements) SelectMembershipsFromRoomAndMembership( } defer internal.CloseAndLogIfError(ctx, rows, "selectMembershipsFromRoomAndMembership: rows.close() failed") + var eNID types.EventNID for rows.Next() { - var eNID types.EventNID if err = rows.Scan(&eNID); err != nil { return } @@ -284,8 +284,8 @@ func (s *membershipStatements) SelectRoomsWithMembership( } defer internal.CloseAndLogIfError(ctx, rows, "SelectRoomsWithMembership: rows.close() failed") var roomNIDs []types.RoomNID + var roomNID types.RoomNID for rows.Next() { - var roomNID types.RoomNID if err := rows.Scan(&roomNID); err != nil { return nil, err } @@ -316,9 +316,9 @@ func (s *membershipStatements) SelectJoinedUsersSetForRooms(ctx context.Context, } defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsersSetForRooms: rows.close() failed") result := make(map[types.EventStateKeyNID]int) + var userID types.EventStateKeyNID + var count int for rows.Next() { - var userID types.EventStateKeyNID - var count int if err := rows.Scan(&userID, &count); err != nil { return nil, err } @@ -335,12 +335,12 @@ func (s *membershipStatements) SelectKnownUsers(ctx context.Context, txn *sql.Tx } result := []string{} defer internal.CloseAndLogIfError(ctx, rows, "SelectKnownUsers: rows.close() failed") + var resUserID string for rows.Next() { - var userID string - if err := rows.Scan(&userID); err != nil { + if err := rows.Scan(&resUserID); err != nil { return nil, err } - result = append(result, userID) + result = append(result, resUserID) } return result, rows.Err() } diff --git a/roomserver/storage/sqlite3/previous_events_table.go b/roomserver/storage/sqlite3/previous_events_table.go index 7304bf0d5..2a146ef64 100644 --- a/roomserver/storage/sqlite3/previous_events_table.go +++ b/roomserver/storage/sqlite3/previous_events_table.go @@ -70,12 +70,12 @@ type previousEventStatements struct { selectPreviousEventExistsStmt *sql.Stmt } -func createPrevEventsTable(db *sql.DB) error { +func CreatePrevEventsTable(db *sql.DB) error { _, err := db.Exec(previousEventSchema) return err } -func preparePrevEventsTable(db *sql.DB) (tables.PreviousEvents, error) { +func PreparePrevEventsTable(db *sql.DB) (tables.PreviousEvents, error) { s := &previousEventStatements{ db: db, } diff --git a/roomserver/storage/sqlite3/published_table.go b/roomserver/storage/sqlite3/published_table.go index 9e416ace3..50dfa5492 100644 --- a/roomserver/storage/sqlite3/published_table.go +++ b/roomserver/storage/sqlite3/published_table.go @@ -49,12 +49,12 @@ type publishedStatements struct { selectPublishedStmt *sql.Stmt } -func createPublishedTable(db *sql.DB) error { +func CreatePublishedTable(db *sql.DB) error { _, err := db.Exec(publishedSchema) return err } -func preparePublishedTable(db *sql.DB) (tables.Published, error) { +func PreparePublishedTable(db *sql.DB) (tables.Published, error) { s := &publishedStatements{ db: db, } @@ -96,8 +96,8 @@ func (s *publishedStatements) SelectAllPublishedRooms( defer internal.CloseAndLogIfError(ctx, rows, "selectAllPublishedStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/redactions_table.go b/roomserver/storage/sqlite3/redactions_table.go index aed190b1e..db6f57a1b 100644 --- a/roomserver/storage/sqlite3/redactions_table.go +++ b/roomserver/storage/sqlite3/redactions_table.go @@ -48,7 +48,7 @@ const selectRedactionInfoByEventBeingRedactedSQL = "" + " WHERE redacts_event_id = $1" const markRedactionValidatedSQL = "" + - " UPDATE roomserver_redactions SET validated = $2 WHERE redaction_event_id = $1" + " UPDATE roomserver_redactions SET validated = $1 WHERE redaction_event_id = $2" type redactionStatements struct { db *sql.DB @@ -58,12 +58,12 @@ type redactionStatements struct { markRedactionValidatedStmt *sql.Stmt } -func createRedactionsTable(db *sql.DB) error { +func CreateRedactionsTable(db *sql.DB) error { _, err := db.Exec(redactionsSchema) return err } -func prepareRedactionsTable(db *sql.DB) (tables.Redactions, error) { +func PrepareRedactionsTable(db *sql.DB) (tables.Redactions, error) { s := &redactionStatements{ db: db, } @@ -118,6 +118,6 @@ func (s *redactionStatements) MarkRedactionValidated( ctx context.Context, txn *sql.Tx, redactionEventID string, validated bool, ) error { stmt := sqlutil.TxStmt(txn, s.markRedactionValidatedStmt) - _, err := stmt.ExecContext(ctx, redactionEventID, validated) + _, err := stmt.ExecContext(ctx, validated, redactionEventID) return err } diff --git a/roomserver/storage/sqlite3/room_aliases_table.go b/roomserver/storage/sqlite3/room_aliases_table.go index 7c7bead95..3bdbbaa35 100644 --- a/roomserver/storage/sqlite3/room_aliases_table.go +++ b/roomserver/storage/sqlite3/room_aliases_table.go @@ -63,12 +63,12 @@ type roomAliasesStatements struct { deleteRoomAliasStmt *sql.Stmt } -func createRoomAliasesTable(db *sql.DB) error { +func CreateRoomAliasesTable(db *sql.DB) error { _, err := db.Exec(roomAliasesSchema) return err } -func prepareRoomAliasesTable(db *sql.DB) (tables.RoomAliases, error) { +func PrepareRoomAliasesTable(db *sql.DB) (tables.RoomAliases, error) { s := &roomAliasesStatements{ db: db, } @@ -113,8 +113,8 @@ func (s *roomAliasesStatements) SelectAliasesFromRoomID( defer internal.CloseAndLogIfError(ctx, rows, "selectAliasesFromRoomID: rows.close() failed") + var alias string for rows.Next() { - var alias string if err = rows.Scan(&alias); err != nil { return } diff --git a/roomserver/storage/sqlite3/rooms_table.go b/roomserver/storage/sqlite3/rooms_table.go index cd60c6785..03ad4b3d0 100644 --- a/roomserver/storage/sqlite3/rooms_table.go +++ b/roomserver/storage/sqlite3/rooms_table.go @@ -86,12 +86,12 @@ type roomStatements struct { selectRoomIDsStmt *sql.Stmt } -func createRoomsTable(db *sql.DB) error { +func CreateRoomsTable(db *sql.DB) error { _, err := db.Exec(roomsSchema) return err } -func prepareRoomsTable(db *sql.DB) (tables.Rooms, error) { +func PrepareRoomsTable(db *sql.DB) (tables.Rooms, error) { s := &roomStatements{ db: db, } @@ -108,7 +108,7 @@ func prepareRoomsTable(db *sql.DB) (tables.Rooms, error) { }.Prepare(db) } -func (s *roomStatements) SelectRoomIDs(ctx context.Context, txn *sql.Tx) ([]string, error) { +func (s *roomStatements) SelectRoomIDsWithEvents(ctx context.Context, txn *sql.Tx) ([]string, error) { stmt := sqlutil.TxStmt(txn, s.selectRoomIDsStmt) rows, err := stmt.QueryContext(ctx) if err != nil { @@ -116,8 +116,8 @@ func (s *roomStatements) SelectRoomIDs(ctx context.Context, txn *sql.Tx) ([]stri } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomIDsStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } @@ -241,9 +241,9 @@ func (s *roomStatements) SelectRoomVersionsForRoomNIDs( } defer internal.CloseAndLogIfError(ctx, rows, "selectRoomVersionsForRoomNIDsStmt: rows.close() failed") result := make(map[types.RoomNID]gomatrixserverlib.RoomVersion) + var roomNID types.RoomNID + var roomVersion gomatrixserverlib.RoomVersion for rows.Next() { - var roomNID types.RoomNID - var roomVersion gomatrixserverlib.RoomVersion if err = rows.Scan(&roomNID, &roomVersion); err != nil { return nil, err } @@ -270,8 +270,8 @@ func (s *roomStatements) BulkSelectRoomIDs(ctx context.Context, txn *sql.Tx, roo } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectRoomIDsStmt: rows.close() failed") var roomIDs []string + var roomID string for rows.Next() { - var roomID string if err = rows.Scan(&roomID); err != nil { return nil, err } @@ -298,8 +298,8 @@ func (s *roomStatements) BulkSelectRoomNIDs(ctx context.Context, txn *sql.Tx, ro } defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectRoomNIDsStmt: rows.close() failed") var roomNIDs []types.RoomNID + var roomNID types.RoomNID for rows.Next() { - var roomNID types.RoomNID if err = rows.Scan(&roomNID); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/state_block_table.go b/roomserver/storage/sqlite3/state_block_table.go index 3c829cdcd..4e67d4da1 100644 --- a/roomserver/storage/sqlite3/state_block_table.go +++ b/roomserver/storage/sqlite3/state_block_table.go @@ -20,7 +20,6 @@ import ( "database/sql" "encoding/json" "fmt" - "sort" "strings" "github.com/matrix-org/dendrite/internal" @@ -64,12 +63,12 @@ type stateBlockStatements struct { bulkSelectStateBlockEntriesStmt *sql.Stmt } -func createStateBlockTable(db *sql.DB) error { +func CreateStateBlockTable(db *sql.DB) error { _, err := db.Exec(stateDataSchema) return err } -func prepareStateBlockTable(db *sql.DB) (tables.StateBlock, error) { +func PrepareStateBlockTable(db *sql.DB) (tables.StateBlock, error) { s := &stateBlockStatements{ db: db, } @@ -85,9 +84,9 @@ func (s *stateBlockStatements) BulkInsertStateData( entries types.StateEntries, ) (id types.StateBlockNID, err error) { entries = entries[:util.SortAndUnique(entries)] - nids := types.EventNIDs{} // zero slice to not store 'null' in the DB - for _, e := range entries { - nids = append(nids, e.EventNID) + nids := make(types.EventNIDs, entries.Len()) + for i := range entries { + nids[i] = entries[i].EventNID } js, err := json.Marshal(nids) if err != nil { @@ -122,13 +121,13 @@ func (s *stateBlockStatements) BulkSelectStateBlockEntries( results := make([][]types.EventNID, len(stateBlockNIDs)) i := 0 + var stateBlockNID types.StateBlockNID + var result json.RawMessage for ; rows.Next(); i++ { - var stateBlockNID types.StateBlockNID - var result json.RawMessage if err = rows.Scan(&stateBlockNID, &result); err != nil { return nil, err } - r := []types.EventNID{} + var r []types.EventNID if err = json.Unmarshal(result, &r); err != nil { return nil, fmt.Errorf("json.Unmarshal: %w", err) } @@ -142,35 +141,3 @@ func (s *stateBlockStatements) BulkSelectStateBlockEntries( } return results, err } - -type stateKeyTupleSorter []types.StateKeyTuple - -func (s stateKeyTupleSorter) Len() int { return len(s) } -func (s stateKeyTupleSorter) Less(i, j int) bool { return s[i].LessThan(s[j]) } -func (s stateKeyTupleSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// Check whether a tuple is in the list. Assumes that the list is sorted. -func (s stateKeyTupleSorter) contains(value types.StateKeyTuple) bool { - i := sort.Search(len(s), func(i int) bool { return !s[i].LessThan(value) }) - return i < len(s) && s[i] == value -} - -// List the unique eventTypeNIDs and eventStateKeyNIDs. -// Assumes that the list is sorted. -func (s stateKeyTupleSorter) typesAndStateKeysAsArrays() (eventTypeNIDs []int64, eventStateKeyNIDs []int64) { - eventTypeNIDs = make([]int64, len(s)) - eventStateKeyNIDs = make([]int64, len(s)) - for i := range s { - eventTypeNIDs[i] = int64(s[i].EventTypeNID) - eventStateKeyNIDs[i] = int64(s[i].EventStateKeyNID) - } - eventTypeNIDs = eventTypeNIDs[:util.SortAndUnique(int64Sorter(eventTypeNIDs))] - eventStateKeyNIDs = eventStateKeyNIDs[:util.SortAndUnique(int64Sorter(eventStateKeyNIDs))] - return -} - -type int64Sorter []int64 - -func (s int64Sorter) Len() int { return len(s) } -func (s int64Sorter) Less(i, j int) bool { return s[i] < s[j] } -func (s int64Sorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } diff --git a/roomserver/storage/sqlite3/state_block_table_test.go b/roomserver/storage/sqlite3/state_block_table_test.go deleted file mode 100644 index 98439f5c0..000000000 --- a/roomserver/storage/sqlite3/state_block_table_test.go +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2017-2018 New Vector Ltd -// Copyright 2019-2020 The Matrix.org Foundation C.I.C. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package sqlite3 - -import ( - "sort" - "testing" - - "github.com/matrix-org/dendrite/roomserver/types" -) - -func TestStateKeyTupleSorter(t *testing.T) { - input := stateKeyTupleSorter{ - {EventTypeNID: 1, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 4}, - {EventTypeNID: 2, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 1}, - } - want := []types.StateKeyTuple{ - {EventTypeNID: 1, EventStateKeyNID: 1}, - {EventTypeNID: 1, EventStateKeyNID: 2}, - {EventTypeNID: 1, EventStateKeyNID: 4}, - {EventTypeNID: 2, EventStateKeyNID: 2}, - } - doNotWant := []types.StateKeyTuple{ - {EventTypeNID: 0, EventStateKeyNID: 0}, - {EventTypeNID: 1, EventStateKeyNID: 3}, - {EventTypeNID: 2, EventStateKeyNID: 1}, - {EventTypeNID: 3, EventStateKeyNID: 1}, - } - wantTypeNIDs := []int64{1, 2} - wantStateKeyNIDs := []int64{1, 2, 4} - - // Sort the input and check it's in the right order. - sort.Sort(input) - gotTypeNIDs, gotStateKeyNIDs := input.typesAndStateKeysAsArrays() - - for i := range want { - if input[i] != want[i] { - t.Errorf("Wanted %#v at index %d got %#v", want[i], i, input[i]) - } - - if !input.contains(want[i]) { - t.Errorf("Wanted %#v.contains(%#v) to be true but got false", input, want[i]) - } - } - - for i := range doNotWant { - if input.contains(doNotWant[i]) { - t.Errorf("Wanted %#v.contains(%#v) to be false but got true", input, doNotWant[i]) - } - } - - if len(wantTypeNIDs) != len(gotTypeNIDs) { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - - for i := range wantTypeNIDs { - if wantTypeNIDs[i] != gotTypeNIDs[i] { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - } - - if len(wantStateKeyNIDs) != len(gotStateKeyNIDs) { - t.Fatalf("Wanted state key NIDs %#v got %#v", wantStateKeyNIDs, gotStateKeyNIDs) - } - - for i := range wantStateKeyNIDs { - if wantStateKeyNIDs[i] != gotStateKeyNIDs[i] { - t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) - } - } -} diff --git a/roomserver/storage/sqlite3/state_snapshot_table.go b/roomserver/storage/sqlite3/state_snapshot_table.go index 1f5e9ee3b..b8136b758 100644 --- a/roomserver/storage/sqlite3/state_snapshot_table.go +++ b/roomserver/storage/sqlite3/state_snapshot_table.go @@ -68,12 +68,12 @@ type stateSnapshotStatements struct { bulkSelectStateBlockNIDsStmt *sql.Stmt } -func createStateSnapshotTable(db *sql.DB) error { +func CreateStateSnapshotTable(db *sql.DB) error { _, err := db.Exec(stateSnapshotSchema) return err } -func prepareStateSnapshotTable(db *sql.DB) (tables.StateSnapshot, error) { +func PrepareStateSnapshotTable(db *sql.DB) (tables.StateSnapshot, error) { s := &stateSnapshotStatements{ db: db, } @@ -96,12 +96,10 @@ func (s *stateSnapshotStatements) InsertState( return } insertStmt := sqlutil.TxStmt(txn, s.insertStateStmt) - var id int64 - err = insertStmt.QueryRowContext(ctx, stateBlockNIDs.Hash(), int64(roomNID), string(stateBlockNIDsJSON)).Scan(&id) + err = insertStmt.QueryRowContext(ctx, stateBlockNIDs.Hash(), int64(roomNID), string(stateBlockNIDsJSON)).Scan(&stateNID) if err != nil { return 0, err } - stateNID = types.StateSnapshotNID(id) return } @@ -127,9 +125,9 @@ func (s *stateSnapshotStatements) BulkSelectStateBlockNIDs( defer internal.CloseAndLogIfError(ctx, rows, "bulkSelectStateBlockNIDs: rows.close() failed") results := make([]types.StateBlockNIDList, len(stateNIDs)) i := 0 + var stateBlockNIDsJSON string for ; rows.Next(); i++ { result := &results[i] - var stateBlockNIDsJSON string if err := rows.Scan(&result.StateSnapshotNID, &stateBlockNIDsJSON); err != nil { return nil, err } diff --git a/roomserver/storage/sqlite3/storage.go b/roomserver/storage/sqlite3/storage.go index c60577df6..0621f50f4 100644 --- a/roomserver/storage/sqlite3/storage.go +++ b/roomserver/storage/sqlite3/storage.go @@ -18,12 +18,14 @@ package sqlite3 import ( "context" "database/sql" + "fmt" "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/roomserver/storage/shared" "github.com/matrix-org/dendrite/roomserver/storage/sqlite3/deltas" "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" ) @@ -34,12 +36,12 @@ type Database struct { } // Open a sqlite database. -func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) { +func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (*Database, error) { var d Database - var db *sql.DB var err error - if db, err = sqlutil.Open(dbProperties); err != nil { - return nil, err + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()) + if err != nil { + return nil, fmt.Errorf("sqlutil.Open: %w", err) } //db.Exec("PRAGMA journal_mode=WAL;") @@ -49,7 +51,7 @@ func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) // cause the roomserver to be unresponsive to new events because something will // acquire the global mutex and never unlock it because it is waiting for a connection // which it will never obtain. - db.SetMaxOpenConns(20) + // db.SetMaxOpenConns(20) // Create the tables. if err = d.create(db); err != nil { @@ -76,7 +78,7 @@ func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) // Then prepare the statements. Now that the migrations have run, any columns referred // to in the database code should now exist. - if err := d.prepare(db, cache); err != nil { + if err := d.prepare(db, writer, cache); err != nil { return nil, err } @@ -84,106 +86,106 @@ func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) } func (d *Database) create(db *sql.DB) error { - if err := createEventStateKeysTable(db); err != nil { + if err := CreateEventStateKeysTable(db); err != nil { return err } - if err := createEventTypesTable(db); err != nil { + if err := CreateEventTypesTable(db); err != nil { return err } - if err := createEventJSONTable(db); err != nil { + if err := CreateEventJSONTable(db); err != nil { return err } - if err := createEventsTable(db); err != nil { + if err := CreateEventsTable(db); err != nil { return err } - if err := createRoomsTable(db); err != nil { + if err := CreateRoomsTable(db); err != nil { return err } - if err := createStateBlockTable(db); err != nil { + if err := CreateStateBlockTable(db); err != nil { return err } - if err := createStateSnapshotTable(db); err != nil { + if err := CreateStateSnapshotTable(db); err != nil { return err } - if err := createPrevEventsTable(db); err != nil { + if err := CreatePrevEventsTable(db); err != nil { return err } - if err := createRoomAliasesTable(db); err != nil { + if err := CreateRoomAliasesTable(db); err != nil { return err } - if err := createInvitesTable(db); err != nil { + if err := CreateInvitesTable(db); err != nil { return err } - if err := createMembershipTable(db); err != nil { + if err := CreateMembershipTable(db); err != nil { return err } - if err := createPublishedTable(db); err != nil { + if err := CreatePublishedTable(db); err != nil { return err } - if err := createRedactionsTable(db); err != nil { + if err := CreateRedactionsTable(db); err != nil { return err } return nil } -func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { - eventStateKeys, err := prepareEventStateKeysTable(db) +func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { + eventStateKeys, err := PrepareEventStateKeysTable(db) if err != nil { return err } - eventTypes, err := prepareEventTypesTable(db) + eventTypes, err := PrepareEventTypesTable(db) if err != nil { return err } - eventJSON, err := prepareEventJSONTable(db) + eventJSON, err := PrepareEventJSONTable(db) if err != nil { return err } - events, err := prepareEventsTable(db) + events, err := PrepareEventsTable(db) if err != nil { return err } - rooms, err := prepareRoomsTable(db) + rooms, err := PrepareRoomsTable(db) if err != nil { return err } - stateBlock, err := prepareStateBlockTable(db) + stateBlock, err := PrepareStateBlockTable(db) if err != nil { return err } - stateSnapshot, err := prepareStateSnapshotTable(db) + stateSnapshot, err := PrepareStateSnapshotTable(db) if err != nil { return err } - prevEvents, err := preparePrevEventsTable(db) + prevEvents, err := PreparePrevEventsTable(db) if err != nil { return err } - roomAliases, err := prepareRoomAliasesTable(db) + roomAliases, err := PrepareRoomAliasesTable(db) if err != nil { return err } - invites, err := prepareInvitesTable(db) + invites, err := PrepareInvitesTable(db) if err != nil { return err } - membership, err := prepareMembershipTable(db) + membership, err := PrepareMembershipTable(db) if err != nil { return err } - published, err := preparePublishedTable(db) + published, err := PreparePublishedTable(db) if err != nil { return err } - redactions, err := prepareRedactionsTable(db) + redactions, err := PrepareRedactionsTable(db) if err != nil { return err } d.Database = shared.Database{ DB: db, Cache: cache, - Writer: sqlutil.NewExclusiveWriter(), + Writer: writer, EventsTable: events, EventTypesTable: eventTypes, EventStateKeysTable: eventStateKeys, diff --git a/roomserver/storage/storage.go b/roomserver/storage/storage.go index 9f98ea3ed..8a87b7d7c 100644 --- a/roomserver/storage/storage.go +++ b/roomserver/storage/storage.go @@ -23,16 +23,17 @@ import ( "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/roomserver/storage/postgres" "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // Open opens a database connection. -func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) { +func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.Open(dbProperties, cache) + return sqlite3.Open(base, dbProperties, cache) case dbProperties.ConnectionString.IsPostgres(): - return postgres.Open(dbProperties, cache) + return postgres.Open(base, dbProperties, cache) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/roomserver/storage/storage_wasm.go b/roomserver/storage/storage_wasm.go index dfc374e6e..df5a56ac3 100644 --- a/roomserver/storage/storage_wasm.go +++ b/roomserver/storage/storage_wasm.go @@ -19,14 +19,15 @@ import ( "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" ) // NewPublicRoomsServerDatabase opens a database connection. -func Open(dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) { +func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache caching.RoomServerCaches) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.Open(dbProperties, cache) + return sqlite3.Open(base, dbProperties, cache) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/roomserver/storage/tables/event_json_table_test.go b/roomserver/storage/tables/event_json_table_test.go new file mode 100644 index 000000000..b490d0fe8 --- /dev/null +++ b/roomserver/storage/tables/event_json_table_test.go @@ -0,0 +1,95 @@ +package tables_test + +import ( + "context" + "fmt" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateEventJSONTable(t *testing.T, dbType test.DBType) (tables.EventJSON, func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + var tab tables.EventJSON + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateEventJSONTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareEventJSONTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateEventJSONTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareEventJSONTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func Test_EventJSONTable(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateEventJSONTable(t, dbType) + defer close() + + // create some dummy data + for i := 0; i < 10; i++ { + err := tab.InsertEventJSON( + context.Background(), nil, types.EventNID(i), + []byte(fmt.Sprintf(`{"value":%d"}`, i)), + ) + assert.NoError(t, err) + } + + tests := []struct { + name string + args []types.EventNID + wantCount int + }{ + { + name: "select subset of existing NIDs", + args: []types.EventNID{1, 2, 3, 4, 5}, + wantCount: 5, + }, + { + name: "select subset of existing/non-existing NIDs", + args: []types.EventNID{1, 2, 12, 50}, + wantCount: 2, + }, + { + name: "select single existing NID", + args: []types.EventNID{1}, + wantCount: 1, + }, + { + name: "select single non-existing NID", + args: []types.EventNID{13}, + wantCount: 0, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + // select a subset of the data + values, err := tab.BulkSelectEventJSON(context.Background(), nil, tc.args) + assert.NoError(t, err) + assert.Equal(t, tc.wantCount, len(values)) + for i, v := range values { + assert.Equal(t, v.EventNID, types.EventNID(i+1)) + assert.Equal(t, []byte(fmt.Sprintf(`{"value":%d"}`, i+1)), v.EventJSON) + } + }) + } + }) +} diff --git a/roomserver/storage/tables/event_state_keys_table_test.go b/roomserver/storage/tables/event_state_keys_table_test.go new file mode 100644 index 000000000..a856fe551 --- /dev/null +++ b/roomserver/storage/tables/event_state_keys_table_test.go @@ -0,0 +1,79 @@ +package tables_test + +import ( + "context" + "fmt" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateEventStateKeysTable(t *testing.T, dbType test.DBType) (tables.EventStateKeys, func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + var tab tables.EventStateKeys + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateEventStateKeysTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareEventStateKeysTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateEventStateKeysTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareEventStateKeysTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func Test_EventStateKeysTable(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateEventStateKeysTable(t, dbType) + defer close() + ctx := context.Background() + var stateKeyNID, gotEventStateKey types.EventStateKeyNID + var err error + // create some dummy data + for i := 0; i < 10; i++ { + stateKey := fmt.Sprintf("@user%d:localhost", i) + stateKeyNID, err = tab.InsertEventStateKeyNID(ctx, nil, stateKey) + assert.NoError(t, err) + gotEventStateKey, err = tab.SelectEventStateKeyNID(ctx, nil, stateKey) + assert.NoError(t, err) + assert.Equal(t, stateKeyNID, gotEventStateKey) + } + // This should fail, since @user0:localhost already exists + stateKey := fmt.Sprintf("@user%d:localhost", 0) + _, err = tab.InsertEventStateKeyNID(ctx, nil, stateKey) + assert.Error(t, err) + + stateKeyNIDsMap, err := tab.BulkSelectEventStateKeyNID(ctx, nil, []string{"@user0:localhost", "@user1:localhost"}) + assert.NoError(t, err) + wantStateKeyNIDs := make([]types.EventStateKeyNID, 0, len(stateKeyNIDsMap)) + for _, nid := range stateKeyNIDsMap { + wantStateKeyNIDs = append(wantStateKeyNIDs, nid) + } + stateKeyNIDs, err := tab.BulkSelectEventStateKey(ctx, nil, wantStateKeyNIDs) + assert.NoError(t, err) + // verify that BulkSelectEventStateKeyNID and BulkSelectEventStateKey return the same values + for userID, nid := range stateKeyNIDsMap { + if v, ok := stateKeyNIDs[nid]; ok { + assert.Equal(t, v, userID) + } else { + t.Fatalf("unable to find %d in result set", nid) + } + } + }) +} diff --git a/roomserver/storage/tables/event_types_table_test.go b/roomserver/storage/tables/event_types_table_test.go new file mode 100644 index 000000000..92c57a917 --- /dev/null +++ b/roomserver/storage/tables/event_types_table_test.go @@ -0,0 +1,79 @@ +package tables_test + +import ( + "context" + "fmt" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateEventTypesTable(t *testing.T, dbType test.DBType) (tables.EventTypes, func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + var tab tables.EventTypes + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateEventTypesTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareEventTypesTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateEventTypesTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareEventTypesTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func Test_EventTypesTable(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateEventTypesTable(t, dbType) + defer close() + ctx := context.Background() + var eventTypeNID, gotEventTypeNID types.EventTypeNID + var err error + // create some dummy data + eventTypeMap := make(map[string]types.EventTypeNID) + for i := 0; i < 10; i++ { + eventType := fmt.Sprintf("dummyEventType%d", i) + eventTypeNID, err = tab.InsertEventTypeNID(ctx, nil, eventType) + assert.NoError(t, err) + eventTypeMap[eventType] = eventTypeNID + gotEventTypeNID, err = tab.SelectEventTypeNID(ctx, nil, eventType) + assert.NoError(t, err) + assert.Equal(t, eventTypeNID, gotEventTypeNID) + } + // This should fail, since the dummyEventType0 already exists + eventType := fmt.Sprintf("dummyEventType%d", 0) + _, err = tab.InsertEventTypeNID(ctx, nil, eventType) + assert.Error(t, err) + + // This should return an error, as this eventType does not exist + _, err = tab.SelectEventTypeNID(ctx, nil, "dummyEventType13") + assert.Error(t, err) + + eventTypeNIDs, err := tab.BulkSelectEventTypeNID(ctx, nil, []string{"dummyEventType0", "dummyEventType3"}) + assert.NoError(t, err) + // verify that BulkSelectEventTypeNID and InsertEventTypeNID return the same values + for eventType, nid := range eventTypeNIDs { + if v, ok := eventTypeMap[eventType]; ok { + assert.Equal(t, v, nid) + } else { + t.Fatalf("unable to find %d in result set", nid) + } + } + }) +} diff --git a/roomserver/storage/tables/events_table_test.go b/roomserver/storage/tables/events_table_test.go new file mode 100644 index 000000000..6f72a59b5 --- /dev/null +++ b/roomserver/storage/tables/events_table_test.go @@ -0,0 +1,157 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/gomatrixserverlib" + "github.com/stretchr/testify/assert" +) + +func mustCreateEventsTable(t *testing.T, dbType test.DBType) (tables.Events, func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + var tab tables.Events + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateEventsTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareEventsTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateEventsTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareEventsTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func Test_EventsTable(t *testing.T) { + alice := test.NewUser(t) + room := test.NewRoom(t, alice) + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateEventsTable(t, dbType) + defer close() + // create some dummy data + eventIDs := make([]string, 0, len(room.Events())) + wantStateAtEvent := make([]types.StateAtEvent, 0, len(room.Events())) + wantEventReferences := make([]gomatrixserverlib.EventReference, 0, len(room.Events())) + wantStateAtEventAndRefs := make([]types.StateAtEventAndReference, 0, len(room.Events())) + for _, ev := range room.Events() { + eventNID, snapNID, err := tab.InsertEvent(ctx, nil, 1, 1, 1, ev.EventID(), ev.EventReference().EventSHA256, nil, ev.Depth(), false) + assert.NoError(t, err) + gotEventNID, gotSnapNID, err := tab.SelectEvent(ctx, nil, ev.EventID()) + assert.NoError(t, err) + assert.Equal(t, eventNID, gotEventNID) + assert.Equal(t, snapNID, gotSnapNID) + eventID, err := tab.SelectEventID(ctx, nil, eventNID) + assert.NoError(t, err) + assert.Equal(t, eventID, ev.EventID()) + + // The events shouldn't be sent to output yet + sentToOutput, err := tab.SelectEventSentToOutput(ctx, nil, gotEventNID) + assert.NoError(t, err) + assert.False(t, sentToOutput) + + err = tab.UpdateEventSentToOutput(ctx, nil, gotEventNID) + assert.NoError(t, err) + + // Now they should be sent to output + sentToOutput, err = tab.SelectEventSentToOutput(ctx, nil, gotEventNID) + assert.NoError(t, err) + assert.True(t, sentToOutput) + + eventIDs = append(eventIDs, ev.EventID()) + wantEventReferences = append(wantEventReferences, ev.EventReference()) + + // Set the stateSnapshot to 2 for some events to verify they are returned later + stateSnapshot := 0 + if eventNID < 3 { + stateSnapshot = 2 + err = tab.UpdateEventState(ctx, nil, eventNID, 2) + assert.NoError(t, err) + } + stateAtEvent := types.StateAtEvent{ + Overwrite: false, + BeforeStateSnapshotNID: types.StateSnapshotNID(stateSnapshot), + IsRejected: false, + StateEntry: types.StateEntry{ + EventNID: eventNID, + StateKeyTuple: types.StateKeyTuple{ + EventTypeNID: 1, + EventStateKeyNID: 1, + }, + }, + } + wantStateAtEvent = append(wantStateAtEvent, stateAtEvent) + wantStateAtEventAndRefs = append(wantStateAtEventAndRefs, types.StateAtEventAndReference{ + StateAtEvent: stateAtEvent, + EventReference: ev.EventReference(), + }) + } + + stateEvents, err := tab.BulkSelectStateEventByID(ctx, nil, eventIDs) + assert.NoError(t, err) + assert.Equal(t, len(stateEvents), len(eventIDs)) + nids := make([]types.EventNID, 0, len(stateEvents)) + for _, ev := range stateEvents { + nids = append(nids, ev.EventNID) + } + stateEvents2, err := tab.BulkSelectStateEventByNID(ctx, nil, nids, nil) + assert.NoError(t, err) + // somehow SQLite doesn't return the values ordered as requested by the query + assert.ElementsMatch(t, stateEvents, stateEvents2) + + roomNIDs, err := tab.SelectRoomNIDsForEventNIDs(ctx, nil, nids) + assert.NoError(t, err) + // We only inserted one room, so the RoomNID should be the same for all evendNIDs + for _, roomNID := range roomNIDs { + assert.Equal(t, types.RoomNID(1), roomNID) + } + + stateAtEvent, err := tab.BulkSelectStateAtEventByID(ctx, nil, eventIDs) + assert.NoError(t, err) + assert.Equal(t, len(eventIDs), len(stateAtEvent)) + + assert.ElementsMatch(t, wantStateAtEvent, stateAtEvent) + + evendNIDMap, err := tab.BulkSelectEventID(ctx, nil, nids) + assert.NoError(t, err) + t.Logf("%+v", evendNIDMap) + assert.Equal(t, len(evendNIDMap), len(nids)) + + nidMap, err := tab.BulkSelectEventNID(ctx, nil, eventIDs) + assert.NoError(t, err) + // check that we got all expected eventNIDs + for _, eventID := range eventIDs { + _, ok := nidMap[eventID] + assert.True(t, ok) + } + + references, err := tab.BulkSelectEventReference(ctx, nil, nids) + assert.NoError(t, err) + assert.Equal(t, wantEventReferences, references) + + stateAndRefs, err := tab.BulkSelectStateAtEventAndReference(ctx, nil, nids) + assert.NoError(t, err) + assert.Equal(t, wantStateAtEventAndRefs, stateAndRefs) + + // check we get the expected event depth + maxDepth, err := tab.SelectMaxEventDepth(ctx, nil, nids) + assert.NoError(t, err) + assert.Equal(t, int64(len(room.Events())+1), maxDepth) + }) +} diff --git a/roomserver/storage/tables/interface.go b/roomserver/storage/tables/interface.go index 97e4afcff..116e11c4e 100644 --- a/roomserver/storage/tables/interface.go +++ b/roomserver/storage/tables/interface.go @@ -10,9 +10,8 @@ import ( ) type EventJSONPair struct { - EventNID types.EventNID - RoomVersion gomatrixserverlib.RoomVersion - EventJSON []byte + EventNID types.EventNID + EventJSON []byte } type EventJSON interface { @@ -36,7 +35,8 @@ type EventStateKeys interface { type Events interface { InsertEvent( - ctx context.Context, txn *sql.Tx, i types.RoomNID, j types.EventTypeNID, k types.EventStateKeyNID, eventID string, + ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, eventTypeNID types.EventTypeNID, + eventStateKeyNID types.EventStateKeyNID, eventID string, referenceSHA256 []byte, authEventNIDs []types.EventNID, depth int64, isRejected bool, ) (types.EventNID, types.StateSnapshotNID, error) SelectEvent(ctx context.Context, txn *sql.Tx, eventID string) (types.EventNID, types.StateSnapshotNID, error) @@ -72,7 +72,7 @@ type Rooms interface { UpdateLatestEventNIDs(ctx context.Context, txn *sql.Tx, roomNID types.RoomNID, eventNIDs []types.EventNID, lastEventSentNID types.EventNID, stateSnapshotNID types.StateSnapshotNID) error SelectRoomVersionsForRoomNIDs(ctx context.Context, txn *sql.Tx, roomNID []types.RoomNID) (map[types.RoomNID]gomatrixserverlib.RoomVersion, error) SelectRoomInfo(ctx context.Context, txn *sql.Tx, roomID string) (*types.RoomInfo, error) - SelectRoomIDs(ctx context.Context, txn *sql.Tx) ([]string, error) + SelectRoomIDsWithEvents(ctx context.Context, txn *sql.Tx) ([]string, error) BulkSelectRoomIDs(ctx context.Context, txn *sql.Tx, roomNIDs []types.RoomNID) ([]string, error) BulkSelectRoomNIDs(ctx context.Context, txn *sql.Tx, roomIDs []string) ([]types.RoomNID, error) } diff --git a/roomserver/storage/tables/invite_table_test.go b/roomserver/storage/tables/invite_table_test.go new file mode 100644 index 000000000..8df3faa2d --- /dev/null +++ b/roomserver/storage/tables/invite_table_test.go @@ -0,0 +1,92 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/util" + "github.com/stretchr/testify/assert" +) + +func mustCreateInviteTable(t *testing.T, dbType test.DBType) (tables.Invites, func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + var tab tables.Invites + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateInvitesTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareInvitesTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateInvitesTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareInvitesTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestInviteTable(t *testing.T) { + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateInviteTable(t, dbType) + defer close() + eventID1 := util.RandomString(16) + roomNID := types.RoomNID(1) + targetUserNID, senderUserNID := types.EventStateKeyNID(1), types.EventStateKeyNID(2) + newInvite, err := tab.InsertInviteEvent(ctx, nil, eventID1, roomNID, targetUserNID, senderUserNID, []byte("")) + assert.NoError(t, err) + assert.True(t, newInvite) + + // Try adding the same invite again + newInvite, err = tab.InsertInviteEvent(ctx, nil, eventID1, roomNID, targetUserNID, senderUserNID, []byte("")) + assert.NoError(t, err) + assert.False(t, newInvite) + + // Add another invite for this room + eventID2 := util.RandomString(16) + newInvite, err = tab.InsertInviteEvent(ctx, nil, eventID2, roomNID, targetUserNID, senderUserNID, []byte("")) + assert.NoError(t, err) + assert.True(t, newInvite) + + // Add another invite for a different user + eventID := util.RandomString(16) + newInvite, err = tab.InsertInviteEvent(ctx, nil, eventID, types.RoomNID(3), targetUserNID, senderUserNID, []byte("")) + assert.NoError(t, err) + assert.True(t, newInvite) + + stateKeyNIDs, eventIDs, err := tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID) + assert.NoError(t, err) + assert.Equal(t, []string{eventID1, eventID2}, eventIDs) + assert.Equal(t, []types.EventStateKeyNID{2, 2}, stateKeyNIDs) + + // retire the invite + retiredEventIDs, err := tab.UpdateInviteRetired(ctx, nil, roomNID, targetUserNID) + assert.NoError(t, err) + assert.Equal(t, []string{eventID1, eventID2}, retiredEventIDs) + + // This should now be empty + stateKeyNIDs, eventIDs, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, targetUserNID, roomNID) + assert.NoError(t, err) + assert.Empty(t, eventIDs) + assert.Empty(t, stateKeyNIDs) + + // Non-existent targetUserNID + stateKeyNIDs, eventIDs, err = tab.SelectInviteActiveForUserInRoom(ctx, nil, types.EventStateKeyNID(10), roomNID) + assert.NoError(t, err) + assert.Empty(t, stateKeyNIDs) + assert.Empty(t, eventIDs) + }) +} diff --git a/roomserver/storage/tables/membership_table_test.go b/roomserver/storage/tables/membership_table_test.go new file mode 100644 index 000000000..14e8ce50a --- /dev/null +++ b/roomserver/storage/tables/membership_table_test.go @@ -0,0 +1,130 @@ +package tables_test + +import ( + "context" + "fmt" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateMembershipTable(t *testing.T, dbType test.DBType) (tab tables.Membership, stateKeyTab tables.EventStateKeys, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateEventStateKeysTable(db) + assert.NoError(t, err) + err = postgres.CreateMembershipTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareMembershipTable(db) + assert.NoError(t, err) + stateKeyTab, err = postgres.PrepareEventStateKeysTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateEventStateKeysTable(db) + assert.NoError(t, err) + err = sqlite3.CreateMembershipTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareMembershipTable(db) + assert.NoError(t, err) + stateKeyTab, err = sqlite3.PrepareEventStateKeysTable(db) + } + assert.NoError(t, err) + + return tab, stateKeyTab, close +} + +func TestMembershipTable(t *testing.T) { + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, stateKeyTab, close := mustCreateMembershipTable(t, dbType) + defer close() + _ = close + + userNIDs := make([]types.EventStateKeyNID, 0, 10) + for i := 0; i < 10; i++ { + stateKeyNID, err := stateKeyTab.InsertEventStateKeyNID(ctx, nil, fmt.Sprintf("@dummy%d:localhost", i)) + assert.NoError(t, err) + userNIDs = append(userNIDs, stateKeyNID) + // This inserts a left user to the room + err = tab.InsertMembership(ctx, nil, 1, stateKeyNID, true) + assert.NoError(t, err) + } + + // ... so this should be false + inRoom, err := tab.SelectLocalServerInRoom(ctx, nil, 1) + assert.NoError(t, err) + assert.False(t, inRoom) + + changed, err := tab.UpdateMembership(ctx, nil, 1, userNIDs[0], userNIDs[0], tables.MembershipStateJoin, 1, false) + assert.NoError(t, err) + assert.True(t, changed) + + // ... should now be true + inRoom, err = tab.SelectLocalServerInRoom(ctx, nil, 1) + assert.NoError(t, err) + assert.True(t, inRoom) + + userJoinedToRooms, err := tab.SelectJoinedUsersSetForRooms(ctx, nil, []types.RoomNID{1}, userNIDs) + assert.NoError(t, err) + assert.Equal(t, 1, len(userJoinedToRooms)) + + // Get all left/banned users + eventNIDs, err := tab.SelectMembershipsFromRoomAndMembership(ctx, nil, 1, tables.MembershipStateLeaveOrBan, true) + assert.NoError(t, err) + assert.Equal(t, 9, len(eventNIDs)) + + _, membershipState, forgotten, err := tab.SelectMembershipFromRoomAndTarget(ctx, nil, 1, userNIDs[5]) + assert.NoError(t, err) + assert.False(t, forgotten) + assert.Equal(t, tables.MembershipStateLeaveOrBan, membershipState) + + // Get all members, regardless of state + members, err := tab.SelectMembershipsFromRoom(ctx, nil, 1, true) + assert.NoError(t, err) + assert.Equal(t, 10, len(members)) + + // Get correct user + roomNIDs, err := tab.SelectRoomsWithMembership(ctx, nil, userNIDs[1], tables.MembershipStateLeaveOrBan) + assert.NoError(t, err) + assert.Equal(t, []types.RoomNID{1}, roomNIDs) + + // User is not joined to room + roomNIDs, err = tab.SelectRoomsWithMembership(ctx, nil, userNIDs[5], tables.MembershipStateJoin) + assert.NoError(t, err) + assert.Equal(t, 0, len(roomNIDs)) + + // Forget room + err = tab.UpdateForgetMembership(ctx, nil, 1, userNIDs[0], true) + assert.NoError(t, err) + + // should now return true + _, _, forgotten, err = tab.SelectMembershipFromRoomAndTarget(ctx, nil, 1, userNIDs[0]) + assert.NoError(t, err) + assert.True(t, forgotten) + + serverInRoom, err := tab.SelectServerInRoom(ctx, nil, 1, "localhost") + assert.NoError(t, err) + assert.True(t, serverInRoom) + + serverInRoom, err = tab.SelectServerInRoom(ctx, nil, 1, "notJoined") + assert.NoError(t, err) + assert.False(t, serverInRoom) + + // get all users we know about; should be only one user, since no other user joined the room + knownUsers, err := tab.SelectKnownUsers(ctx, nil, userNIDs[0], "localhost", 2) + assert.NoError(t, err) + assert.Equal(t, 1, len(knownUsers)) + }) +} diff --git a/roomserver/storage/tables/previous_events_table_test.go b/roomserver/storage/tables/previous_events_table_test.go new file mode 100644 index 000000000..63d540696 --- /dev/null +++ b/roomserver/storage/tables/previous_events_table_test.go @@ -0,0 +1,61 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/util" + "github.com/stretchr/testify/assert" +) + +func mustCreatePreviousEventsTable(t *testing.T, dbType test.DBType) (tab tables.PreviousEvents, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreatePrevEventsTable(db) + assert.NoError(t, err) + tab, err = postgres.PreparePrevEventsTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreatePrevEventsTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PreparePrevEventsTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestPreviousEventsTable(t *testing.T) { + ctx := context.Background() + alice := test.NewUser(t) + room := test.NewRoom(t, alice) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreatePreviousEventsTable(t, dbType) + defer close() + + for _, x := range room.Events() { + for _, prevEvent := range x.PrevEvents() { + err := tab.InsertPreviousEvent(ctx, nil, prevEvent.EventID, prevEvent.EventSHA256, 1) + assert.NoError(t, err) + + err = tab.SelectPreviousEventExists(ctx, nil, prevEvent.EventID, prevEvent.EventSHA256) + assert.NoError(t, err) + } + } + + // RandomString with a correct EventSHA256 should fail and return sql.ErrNoRows + err := tab.SelectPreviousEventExists(ctx, nil, util.RandomString(16), room.Events()[0].EventReference().EventSHA256) + assert.Error(t, err) + }) +} diff --git a/roomserver/storage/tables/published_table_test.go b/roomserver/storage/tables/published_table_test.go new file mode 100644 index 000000000..fff6dc186 --- /dev/null +++ b/roomserver/storage/tables/published_table_test.go @@ -0,0 +1,79 @@ +package tables_test + +import ( + "context" + "sort" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreatePublishedTable(t *testing.T, dbType test.DBType) (tab tables.Published, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreatePublishedTable(db) + assert.NoError(t, err) + tab, err = postgres.PreparePublishedTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreatePublishedTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PreparePublishedTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestPublishedTable(t *testing.T) { + ctx := context.Background() + alice := test.NewUser(t) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreatePublishedTable(t, dbType) + defer close() + + // Publish some rooms + publishedRooms := []string{} + for i := 0; i < 10; i++ { + room := test.NewRoom(t, alice) + published := i%2 == 0 + err := tab.UpsertRoomPublished(ctx, nil, room.ID, published) + assert.NoError(t, err) + if published { + publishedRooms = append(publishedRooms, room.ID) + } + publishedRes, err := tab.SelectPublishedFromRoomID(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, published, publishedRes) + } + sort.Strings(publishedRooms) + + // check that we get the expected published rooms + roomIDs, err := tab.SelectAllPublishedRooms(ctx, nil, true) + assert.NoError(t, err) + assert.Equal(t, publishedRooms, roomIDs) + + // test an actual upsert + room := test.NewRoom(t, alice) + err = tab.UpsertRoomPublished(ctx, nil, room.ID, true) + assert.NoError(t, err) + err = tab.UpsertRoomPublished(ctx, nil, room.ID, false) + assert.NoError(t, err) + // should now be false, due to the upsert + publishedRes, err := tab.SelectPublishedFromRoomID(ctx, nil, room.ID) + assert.NoError(t, err) + assert.False(t, publishedRes) + }) +} diff --git a/roomserver/storage/tables/redactions_table_test.go b/roomserver/storage/tables/redactions_table_test.go new file mode 100644 index 000000000..ea48dc22f --- /dev/null +++ b/roomserver/storage/tables/redactions_table_test.go @@ -0,0 +1,89 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/util" + "github.com/stretchr/testify/assert" +) + +func mustCreateRedactionsTable(t *testing.T, dbType test.DBType) (tab tables.Redactions, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateRedactionsTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareRedactionsTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateRedactionsTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareRedactionsTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestRedactionsTable(t *testing.T) { + ctx := context.Background() + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateRedactionsTable(t, dbType) + defer close() + + // insert and verify some redactions + for i := 0; i < 10; i++ { + redactionEventID, redactsEventID := util.RandomString(16), util.RandomString(16) + wantRedactionInfo := tables.RedactionInfo{ + Validated: false, + RedactsEventID: redactsEventID, + RedactionEventID: redactionEventID, + } + err := tab.InsertRedaction(ctx, nil, wantRedactionInfo) + assert.NoError(t, err) + + // verify the redactions are inserted as expected + redactionInfo, err := tab.SelectRedactionInfoByRedactionEventID(ctx, nil, redactionEventID) + assert.NoError(t, err) + assert.Equal(t, &wantRedactionInfo, redactionInfo) + + redactionInfo, err = tab.SelectRedactionInfoByEventBeingRedacted(ctx, nil, redactsEventID) + assert.NoError(t, err) + assert.Equal(t, &wantRedactionInfo, redactionInfo) + + // redact event + err = tab.MarkRedactionValidated(ctx, nil, redactionEventID, true) + assert.NoError(t, err) + + wantRedactionInfo.Validated = true + redactionInfo, err = tab.SelectRedactionInfoByRedactionEventID(ctx, nil, redactionEventID) + assert.NoError(t, err) + assert.Equal(t, &wantRedactionInfo, redactionInfo) + } + + // Should not fail, it just updates 0 rows + err := tab.MarkRedactionValidated(ctx, nil, "iDontExist", true) + assert.NoError(t, err) + + // Should also not fail, but return a nil redactionInfo + redactionInfo, err := tab.SelectRedactionInfoByRedactionEventID(ctx, nil, "iDontExist") + assert.NoError(t, err) + assert.Nil(t, redactionInfo) + + redactionInfo, err = tab.SelectRedactionInfoByEventBeingRedacted(ctx, nil, "iDontExist") + assert.NoError(t, err) + assert.Nil(t, redactionInfo) + }) +} diff --git a/roomserver/storage/tables/room_aliases_table_test.go b/roomserver/storage/tables/room_aliases_table_test.go new file mode 100644 index 000000000..624d92ae6 --- /dev/null +++ b/roomserver/storage/tables/room_aliases_table_test.go @@ -0,0 +1,96 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateRoomAliasesTable(t *testing.T, dbType test.DBType) (tab tables.RoomAliases, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateRoomAliasesTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareRoomAliasesTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateRoomAliasesTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareRoomAliasesTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestRoomAliasesTable(t *testing.T) { + alice := test.NewUser(t) + room := test.NewRoom(t, alice) + room2 := test.NewRoom(t, alice) + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateRoomAliasesTable(t, dbType) + defer close() + alias, alias2, alias3 := "#alias:localhost", "#alias2:localhost", "#alias3:localhost" + // insert aliases + err := tab.InsertRoomAlias(ctx, nil, alias, room.ID, alice.ID) + assert.NoError(t, err) + + err = tab.InsertRoomAlias(ctx, nil, alias2, room.ID, alice.ID) + assert.NoError(t, err) + + err = tab.InsertRoomAlias(ctx, nil, alias3, room2.ID, alice.ID) + assert.NoError(t, err) + + // verify we can get the roomID for the alias + roomID, err := tab.SelectRoomIDFromAlias(ctx, nil, alias) + assert.NoError(t, err) + assert.Equal(t, room.ID, roomID) + + // .. and the creator + creator, err := tab.SelectCreatorIDFromAlias(ctx, nil, alias) + assert.NoError(t, err) + assert.Equal(t, alice.ID, creator) + + creator, err = tab.SelectCreatorIDFromAlias(ctx, nil, "#doesntexist:localhost") + assert.NoError(t, err) + assert.Equal(t, "", creator) + + roomID, err = tab.SelectRoomIDFromAlias(ctx, nil, "#doesntexist:localhost") + assert.NoError(t, err) + assert.Equal(t, "", roomID) + + // get all aliases for a room + aliases, err := tab.SelectAliasesFromRoomID(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, []string{alias, alias2}, aliases) + + // delete an alias and verify it's deleted + err = tab.DeleteRoomAlias(ctx, nil, alias2) + assert.NoError(t, err) + + aliases, err = tab.SelectAliasesFromRoomID(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, []string{alias}, aliases) + + // deleting the same alias should be a no-op + err = tab.DeleteRoomAlias(ctx, nil, alias2) + assert.NoError(t, err) + + // Delete non-existent alias should be a no-op + err = tab.DeleteRoomAlias(ctx, nil, "#doesntexist:localhost") + assert.NoError(t, err) + }) +} diff --git a/roomserver/storage/tables/rooms_table_test.go b/roomserver/storage/tables/rooms_table_test.go new file mode 100644 index 000000000..0a02369a1 --- /dev/null +++ b/roomserver/storage/tables/rooms_table_test.go @@ -0,0 +1,128 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/util" + "github.com/stretchr/testify/assert" +) + +func mustCreateRoomsTable(t *testing.T, dbType test.DBType) (tab tables.Rooms, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateRoomsTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareRoomsTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateRoomsTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareRoomsTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestRoomsTable(t *testing.T) { + alice := test.NewUser(t) + room := test.NewRoom(t, alice) + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateRoomsTable(t, dbType) + defer close() + + wantRoomNID, err := tab.InsertRoomNID(ctx, nil, room.ID, room.Version) + assert.NoError(t, err) + + // Create dummy room + _, err = tab.InsertRoomNID(ctx, nil, util.RandomString(16), room.Version) + assert.NoError(t, err) + + gotRoomNID, err := tab.SelectRoomNID(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, wantRoomNID, gotRoomNID) + + // Ensure non existent roomNID errors + roomNID, err := tab.SelectRoomNID(ctx, nil, "!doesnotexist:localhost") + assert.Error(t, err) + assert.Equal(t, types.RoomNID(0), roomNID) + + roomInfo, err := tab.SelectRoomInfo(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, &types.RoomInfo{ + RoomNID: wantRoomNID, + RoomVersion: room.Version, + StateSnapshotNID: 0, + IsStub: true, // there are no latestEventNIDs + }, roomInfo) + + roomInfo, err = tab.SelectRoomInfo(ctx, nil, "!doesnotexist:localhost") + assert.NoError(t, err) + assert.Nil(t, roomInfo) + + // There are no rooms with latestEventNIDs yet + roomIDs, err := tab.SelectRoomIDsWithEvents(ctx, nil) + assert.NoError(t, err) + assert.Equal(t, 0, len(roomIDs)) + + roomVersions, err := tab.SelectRoomVersionsForRoomNIDs(ctx, nil, []types.RoomNID{wantRoomNID, 1337}) + assert.NoError(t, err) + assert.Equal(t, roomVersions[wantRoomNID], room.Version) + // Room does not exist + _, ok := roomVersions[1337] + assert.False(t, ok) + + roomIDs, err = tab.BulkSelectRoomIDs(ctx, nil, []types.RoomNID{wantRoomNID, 1337}) + assert.NoError(t, err) + assert.Equal(t, []string{room.ID}, roomIDs) + + roomNIDs, err := tab.BulkSelectRoomNIDs(ctx, nil, []string{room.ID, "!doesnotexist:localhost"}) + assert.NoError(t, err) + assert.Equal(t, []types.RoomNID{wantRoomNID}, roomNIDs) + + wantEventNIDs := []types.EventNID{1, 2, 3} + lastEventSentNID := types.EventNID(3) + stateSnapshotNID := types.StateSnapshotNID(1) + // make the room "usable" + err = tab.UpdateLatestEventNIDs(ctx, nil, wantRoomNID, wantEventNIDs, lastEventSentNID, stateSnapshotNID) + assert.NoError(t, err) + + roomInfo, err = tab.SelectRoomInfo(ctx, nil, room.ID) + assert.NoError(t, err) + assert.Equal(t, &types.RoomInfo{ + RoomNID: wantRoomNID, + RoomVersion: room.Version, + StateSnapshotNID: 1, + IsStub: false, + }, roomInfo) + + eventNIDs, snapshotNID, err := tab.SelectLatestEventNIDs(ctx, nil, wantRoomNID) + assert.NoError(t, err) + assert.Equal(t, wantEventNIDs, eventNIDs) + assert.Equal(t, types.StateSnapshotNID(1), snapshotNID) + + // Again, doesn't exist + _, _, err = tab.SelectLatestEventNIDs(ctx, nil, 1337) + assert.Error(t, err) + + eventNIDs, eventNID, snapshotNID, err := tab.SelectLatestEventsNIDsForUpdate(ctx, nil, wantRoomNID) + assert.NoError(t, err) + assert.Equal(t, wantEventNIDs, eventNIDs) + assert.Equal(t, types.EventNID(3), eventNID) + assert.Equal(t, types.StateSnapshotNID(1), snapshotNID) + }) +} diff --git a/roomserver/storage/tables/state_block_table_test.go b/roomserver/storage/tables/state_block_table_test.go new file mode 100644 index 000000000..de0b420bc --- /dev/null +++ b/roomserver/storage/tables/state_block_table_test.go @@ -0,0 +1,92 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateStateBlockTable(t *testing.T, dbType test.DBType) (tab tables.StateBlock, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateStateBlockTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareStateBlockTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateStateBlockTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareStateBlockTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestStateBlockTable(t *testing.T) { + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateStateBlockTable(t, dbType) + defer close() + + // generate some dummy data + var entries types.StateEntries + for i := 0; i < 100; i++ { + entry := types.StateEntry{ + EventNID: types.EventNID(i), + } + entries = append(entries, entry) + } + stateBlockNID, err := tab.BulkInsertStateData(ctx, nil, entries) + assert.NoError(t, err) + assert.Equal(t, types.StateBlockNID(1), stateBlockNID) + + // generate a different hash, to get a new StateBlockNID + var entries2 types.StateEntries + for i := 100; i < 300; i++ { + entry := types.StateEntry{ + EventNID: types.EventNID(i), + } + entries2 = append(entries2, entry) + } + stateBlockNID, err = tab.BulkInsertStateData(ctx, nil, entries2) + assert.NoError(t, err) + assert.Equal(t, types.StateBlockNID(2), stateBlockNID) + + eventNIDs, err := tab.BulkSelectStateBlockEntries(ctx, nil, types.StateBlockNIDs{1, 2}) + assert.NoError(t, err) + assert.Equal(t, len(entries), len(eventNIDs[0])) + assert.Equal(t, len(entries2), len(eventNIDs[1])) + + // try to get a StateBlockNID which does not exist + _, err = tab.BulkSelectStateBlockEntries(ctx, nil, types.StateBlockNIDs{5}) + assert.Error(t, err) + + // This should return an error, since we can only retrieve 1 StateBlock + _, err = tab.BulkSelectStateBlockEntries(ctx, nil, types.StateBlockNIDs{1, 5}) + assert.Error(t, err) + + for i := 0; i < 65555; i++ { + entry := types.StateEntry{ + EventNID: types.EventNID(i), + } + entries2 = append(entries2, entry) + } + stateBlockNID, err = tab.BulkInsertStateData(ctx, nil, entries2) + assert.NoError(t, err) + assert.Equal(t, types.StateBlockNID(3), stateBlockNID) + }) +} diff --git a/roomserver/storage/tables/state_snapshot_table_test.go b/roomserver/storage/tables/state_snapshot_table_test.go new file mode 100644 index 000000000..dcdb5d8f1 --- /dev/null +++ b/roomserver/storage/tables/state_snapshot_table_test.go @@ -0,0 +1,86 @@ +package tables_test + +import ( + "context" + "testing" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/roomserver/storage/postgres" + "github.com/matrix-org/dendrite/roomserver/storage/sqlite3" + "github.com/matrix-org/dendrite/roomserver/storage/tables" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/stretchr/testify/assert" +) + +func mustCreateStateSnapshotTable(t *testing.T, dbType test.DBType) (tab tables.StateSnapshot, close func()) { + t.Helper() + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, sqlutil.NewExclusiveWriter()) + assert.NoError(t, err) + switch dbType { + case test.DBTypePostgres: + err = postgres.CreateStateSnapshotTable(db) + assert.NoError(t, err) + tab, err = postgres.PrepareStateSnapshotTable(db) + case test.DBTypeSQLite: + err = sqlite3.CreateStateSnapshotTable(db) + assert.NoError(t, err) + tab, err = sqlite3.PrepareStateSnapshotTable(db) + } + assert.NoError(t, err) + + return tab, close +} + +func TestStateSnapshotTable(t *testing.T) { + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + tab, close := mustCreateStateSnapshotTable(t, dbType) + defer close() + + // generate some dummy data + var stateBlockNIDs types.StateBlockNIDs + for i := 0; i < 100; i++ { + stateBlockNIDs = append(stateBlockNIDs, types.StateBlockNID(i)) + } + stateNID, err := tab.InsertState(ctx, nil, 1, stateBlockNIDs) + assert.NoError(t, err) + assert.Equal(t, types.StateSnapshotNID(1), stateNID) + + // verify ON CONFLICT; Note: this updates the sequence! + stateNID, err = tab.InsertState(ctx, nil, 1, stateBlockNIDs) + assert.NoError(t, err) + assert.Equal(t, types.StateSnapshotNID(1), stateNID) + + // create a second snapshot + var stateBlockNIDs2 types.StateBlockNIDs + for i := 100; i < 150; i++ { + stateBlockNIDs2 = append(stateBlockNIDs2, types.StateBlockNID(i)) + } + + stateNID, err = tab.InsertState(ctx, nil, 1, stateBlockNIDs2) + assert.NoError(t, err) + // StateSnapshotNID is now 3, since the DO UPDATE SET statement incremented the sequence + assert.Equal(t, types.StateSnapshotNID(3), stateNID) + + nidLists, err := tab.BulkSelectStateBlockNIDs(ctx, nil, []types.StateSnapshotNID{1, 3}) + assert.NoError(t, err) + assert.Equal(t, stateBlockNIDs, types.StateBlockNIDs(nidLists[0].StateBlockNIDs)) + assert.Equal(t, stateBlockNIDs2, types.StateBlockNIDs(nidLists[1].StateBlockNIDs)) + + // check we get an error if the state snapshot does not exist + _, err = tab.BulkSelectStateBlockNIDs(ctx, nil, []types.StateSnapshotNID{2}) + assert.Error(t, err) + + // create a second snapshot + for i := 0; i < 65555; i++ { + stateBlockNIDs2 = append(stateBlockNIDs2, types.StateBlockNID(i)) + } + _, err = tab.InsertState(ctx, nil, 1, stateBlockNIDs2) + assert.NoError(t, err) + }) +} diff --git a/roomserver/types/types.go b/roomserver/types/types.go index 65fbee04e..62695aaee 100644 --- a/roomserver/types/types.go +++ b/roomserver/types/types.go @@ -18,8 +18,10 @@ package types import ( "encoding/json" "sort" + "strings" "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" "golang.org/x/crypto/blake2b" ) @@ -96,6 +98,38 @@ func (a StateKeyTuple) LessThan(b StateKeyTuple) bool { return a.EventStateKeyNID < b.EventStateKeyNID } +type StateKeyTupleSorter []StateKeyTuple + +func (s StateKeyTupleSorter) Len() int { return len(s) } +func (s StateKeyTupleSorter) Less(i, j int) bool { return s[i].LessThan(s[j]) } +func (s StateKeyTupleSorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// Check whether a tuple is in the list. Assumes that the list is sorted. +func (s StateKeyTupleSorter) contains(value StateKeyTuple) bool { + i := sort.Search(len(s), func(i int) bool { return !s[i].LessThan(value) }) + return i < len(s) && s[i] == value +} + +// List the unique eventTypeNIDs and eventStateKeyNIDs. +// Assumes that the list is sorted. +func (s StateKeyTupleSorter) TypesAndStateKeysAsArrays() (eventTypeNIDs []int64, eventStateKeyNIDs []int64) { + eventTypeNIDs = make([]int64, len(s)) + eventStateKeyNIDs = make([]int64, len(s)) + for i := range s { + eventTypeNIDs[i] = int64(s[i].EventTypeNID) + eventStateKeyNIDs[i] = int64(s[i].EventStateKeyNID) + } + eventTypeNIDs = eventTypeNIDs[:util.SortAndUnique(int64Sorter(eventTypeNIDs))] + eventStateKeyNIDs = eventStateKeyNIDs[:util.SortAndUnique(int64Sorter(eventStateKeyNIDs))] + return +} + +type int64Sorter []int64 + +func (s int64Sorter) Len() int { return len(s) } +func (s int64Sorter) Less(i, j int) bool { return s[i] < s[j] } +func (s int64Sorter) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + // A StateEntry is an entry in the room state of a matrix room. type StateEntry struct { StateKeyTuple @@ -166,6 +200,20 @@ type StateAtEventAndReference struct { gomatrixserverlib.EventReference } +type StateAtEventAndReferences []StateAtEventAndReference + +func (s StateAtEventAndReferences) Less(a, b int) bool { + return strings.Compare(s[a].EventID, s[b].EventID) < 0 +} + +func (s StateAtEventAndReferences) Len() int { + return len(s) +} + +func (s StateAtEventAndReferences) Swap(a, b int) { + s[a], s[b] = s[b], s[a] +} + // An Event is a gomatrixserverlib.Event with the numeric event ID attached. // It is when performing bulk event lookup in the database. type Event struct { diff --git a/roomserver/types/types_test.go b/roomserver/types/types_test.go index b1e84b821..a26b80f74 100644 --- a/roomserver/types/types_test.go +++ b/roomserver/types/types_test.go @@ -1,6 +1,7 @@ package types import ( + "sort" "testing" ) @@ -24,3 +25,66 @@ func TestDeduplicateStateEntries(t *testing.T) { } } } + +func TestStateKeyTupleSorter(t *testing.T) { + input := StateKeyTupleSorter{ + {EventTypeNID: 1, EventStateKeyNID: 2}, + {EventTypeNID: 1, EventStateKeyNID: 4}, + {EventTypeNID: 2, EventStateKeyNID: 2}, + {EventTypeNID: 1, EventStateKeyNID: 1}, + } + want := []StateKeyTuple{ + {EventTypeNID: 1, EventStateKeyNID: 1}, + {EventTypeNID: 1, EventStateKeyNID: 2}, + {EventTypeNID: 1, EventStateKeyNID: 4}, + {EventTypeNID: 2, EventStateKeyNID: 2}, + } + doNotWant := []StateKeyTuple{ + {EventTypeNID: 0, EventStateKeyNID: 0}, + {EventTypeNID: 1, EventStateKeyNID: 3}, + {EventTypeNID: 2, EventStateKeyNID: 1}, + {EventTypeNID: 3, EventStateKeyNID: 1}, + } + wantTypeNIDs := []int64{1, 2} + wantStateKeyNIDs := []int64{1, 2, 4} + + // Sort the input and check it's in the right order. + sort.Sort(input) + gotTypeNIDs, gotStateKeyNIDs := input.TypesAndStateKeysAsArrays() + + for i := range want { + if input[i] != want[i] { + t.Errorf("Wanted %#v at index %d got %#v", want[i], i, input[i]) + } + + if !input.contains(want[i]) { + t.Errorf("Wanted %#v.contains(%#v) to be true but got false", input, want[i]) + } + } + + for i := range doNotWant { + if input.contains(doNotWant[i]) { + t.Errorf("Wanted %#v.contains(%#v) to be false but got true", input, doNotWant[i]) + } + } + + if len(wantTypeNIDs) != len(gotTypeNIDs) { + t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) + } + + for i := range wantTypeNIDs { + if wantTypeNIDs[i] != gotTypeNIDs[i] { + t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) + } + } + + if len(wantStateKeyNIDs) != len(gotStateKeyNIDs) { + t.Fatalf("Wanted state key NIDs %#v got %#v", wantStateKeyNIDs, gotStateKeyNIDs) + } + + for i := range wantStateKeyNIDs { + if wantStateKeyNIDs[i] != gotStateKeyNIDs[i] { + t.Fatalf("Wanted type NIDs %#v got %#v", wantTypeNIDs, gotTypeNIDs) + } + } +} diff --git a/setup/base/base.go b/setup/base/base.go index 43d613b0c..5cbd7da9c 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -17,10 +17,12 @@ package base import ( "context" "crypto/tls" + "database/sql" "fmt" "io" "net" "net/http" + _ "net/http/pprof" "os" "os/signal" "syscall" @@ -31,6 +33,7 @@ import ( "github.com/matrix-org/dendrite/internal/caching" "github.com/matrix-org/dendrite/internal/httputil" "github.com/matrix-org/dendrite/internal/pushgateway" + "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/gomatrixserverlib" "github.com/prometheus/client_golang/prometheus/promhttp" "go.uber.org/atomic" @@ -38,10 +41,11 @@ import ( "golang.org/x/net/http2/h2c" "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/setup/jetstream" "github.com/matrix-org/dendrite/setup/process" - userdb "github.com/matrix-org/dendrite/userapi/storage" "github.com/gorilla/mux" + "github.com/kardianos/minwinsvc" appserviceAPI "github.com/matrix-org/dendrite/appservice/api" asinthttp "github.com/matrix-org/dendrite/appservice/inthttp" @@ -55,8 +59,6 @@ import ( userapi "github.com/matrix-org/dendrite/userapi/api" userapiinthttp "github.com/matrix-org/dendrite/userapi/inthttp" "github.com/sirupsen/logrus" - - _ "net/http/pprof" ) // BaseDendrite is a base for creating new instances of dendrite. It parses @@ -76,11 +78,15 @@ type BaseDendrite struct { InternalAPIMux *mux.Router DendriteAdminMux *mux.Router SynapseAdminMux *mux.Router + NATS *jetstream.NATSInstance UseHTTPAPIs bool apiHttpClient *http.Client Cfg *config.Dendrite Caches *caching.Caches DNSCache *gomatrixserverlib.DNSCache + Database *sql.DB + DatabaseWriter sqlutil.Writer + EnableMetrics bool } const NoListener = "" @@ -91,8 +97,9 @@ const HTTPClientTimeout = time.Second * 30 type BaseDendriteOptions int const ( - NoCacheMetrics BaseDendriteOptions = iota + DisableMetrics BaseDendriteOptions = iota UseHTTPAPIs + PolylithMode ) // NewBaseDendrite creates a new instance to be used by a component. @@ -101,18 +108,22 @@ const ( func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...BaseDendriteOptions) *BaseDendrite { platformSanityChecks() useHTTPAPIs := false - cacheMetrics := true + enableMetrics := true + isMonolith := true for _, opt := range options { switch opt { - case NoCacheMetrics: - cacheMetrics = false + case DisableMetrics: + enableMetrics = false case UseHTTPAPIs: useHTTPAPIs = true + case PolylithMode: + isMonolith = false + useHTTPAPIs = true } } configErrors := &config.ConfigErrors{} - cfg.Verify(configErrors, componentName == "Monolith") // TODO: better way? + cfg.Verify(configErrors, isMonolith) if len(*configErrors) > 0 { for _, err := range *configErrors { logrus.Errorf("Configuration error: %s", err) @@ -126,6 +137,10 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base logrus.Infof("Dendrite version %s", internal.VersionString()) + if !cfg.ClientAPI.RegistrationDisabled && cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled { + logrus.Warn("Open registration is enabled") + } + closer, err := cfg.SetupTracing("Dendrite" + componentName) if err != nil { logrus.WithError(err).Panicf("failed to start opentracing") @@ -146,7 +161,7 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base } } - cache, err := caching.NewInMemoryLRUCache(cacheMetrics) + cache, err := caching.NewInMemoryLRUCache(enableMetrics) if err != nil { logrus.WithError(err).Warnf("Failed to create cache") } @@ -181,6 +196,25 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base }, } + // If we're in monolith mode, we'll set up a global pool of database + // connections. A component is welcome to use this pool if they don't + // have a separate database config of their own. + var db *sql.DB + var writer sqlutil.Writer + if cfg.Global.DatabaseOptions.ConnectionString != "" { + if !isMonolith { + logrus.Panic("Using a global database connection pool is not supported in polylith deployments") + } + if cfg.Global.DatabaseOptions.ConnectionString.IsSQLite() { + logrus.Panic("Using a global database connection pool is not supported with SQLite databases") + } + writer = sqlutil.NewDummyWriter() + if db, err = sqlutil.Open(&cfg.Global.DatabaseOptions, writer); err != nil { + logrus.WithError(err).Panic("Failed to set up global database connections") + } + logrus.Debug("Using global database connection pool") + } + // Ideally we would only use SkipClean on routes which we know can allow '/' but due to // https://github.com/gorilla/mux/issues/460 we have to attach this at the top router. // When used in conjunction with UseEncodedPath() we get the behaviour we want when parsing @@ -209,7 +243,11 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base InternalAPIMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.InternalPathPrefix).Subrouter().UseEncodedPath(), DendriteAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.DendriteAdminPathPrefix).Subrouter().UseEncodedPath(), SynapseAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.SynapseAdminPathPrefix).Subrouter().UseEncodedPath(), + NATS: &jetstream.NATSInstance{}, apiHttpClient: &apiClient, + Database: db, // set if monolith with global connection pool only + DatabaseWriter: writer, // set if monolith with global connection pool only + EnableMetrics: enableMetrics, } } @@ -218,8 +256,31 @@ func (b *BaseDendrite) Close() error { return b.tracerCloser.Close() } -// AppserviceHTTPClient returns the AppServiceQueryAPI for hitting the appservice component over HTTP. -func (b *BaseDendrite) AppserviceHTTPClient() appserviceAPI.AppServiceQueryAPI { +// DatabaseConnection assists in setting up a database connection. It accepts +// the database properties and a new writer for the given component. If we're +// running in monolith mode with a global connection pool configured then we +// will return that connection, along with the global writer, effectively +// ignoring the options provided. Otherwise we'll open a new database connection +// using the supplied options and writer. Note that it's possible for the pointer +// receiver to be nil here – that's deliberate as some of the unit tests don't +// have a BaseDendrite and just want a connection with the supplied config +// without any pooling stuff. +func (b *BaseDendrite) DatabaseConnection(dbProperties *config.DatabaseOptions, writer sqlutil.Writer) (*sql.DB, sqlutil.Writer, error) { + if dbProperties.ConnectionString != "" || b == nil { + // Open a new database connection using the supplied config. + db, err := sqlutil.Open(dbProperties, writer) + return db, writer, err + } + if b.Database != nil && b.DatabaseWriter != nil { + // Ignore the supplied config and return the global pool and + // writer. + return b.Database, b.DatabaseWriter, nil + } + return nil, nil, fmt.Errorf("no database connections configured") +} + +// AppserviceHTTPClient returns the AppServiceInternalAPI for hitting the appservice component over HTTP. +func (b *BaseDendrite) AppserviceHTTPClient() appserviceAPI.AppServiceInternalAPI { a, err := asinthttp.NewAppserviceClient(b.Cfg.AppServiceURL(), b.apiHttpClient) if err != nil { logrus.WithError(err).Panic("CreateHTTPAppServiceAPIs failed") @@ -269,24 +330,6 @@ func (b *BaseDendrite) PushGatewayHTTPClient() pushgateway.Client { return pushgateway.NewHTTPClient(b.Cfg.UserAPI.PushGatewayDisableTLSValidation) } -// CreateAccountsDB creates a new instance of the accounts database. Should only -// be called once per component. -func (b *BaseDendrite) CreateAccountsDB() userdb.Database { - db, err := userdb.NewDatabase( - &b.Cfg.UserAPI.AccountDatabase, - b.Cfg.Global.ServerName, - b.Cfg.UserAPI.BCryptCost, - b.Cfg.UserAPI.OpenIDTokenLifetimeMS, - userapi.DefaultLoginTokenLifetime, - b.Cfg.Global.ServerNotices.LocalPart, - ) - if err != nil { - logrus.WithError(err).Panicf("failed to connect to accounts db") - } - - return db -} - // CreateClient creates a new client (normally used for media fetch requests). // Should only be called once per component. func (b *BaseDendrite) CreateClient() *gomatrixserverlib.Client { @@ -297,6 +340,7 @@ func (b *BaseDendrite) CreateClient() *gomatrixserverlib.Client { } opts := []gomatrixserverlib.ClientOption{ gomatrixserverlib.WithSkipVerify(b.Cfg.FederationAPI.DisableTLSValidation), + gomatrixserverlib.WithWellKnownSRVLookups(true), } if b.Cfg.Global.DNSCache.Enabled { opts = append(opts, gomatrixserverlib.WithDNSCache(b.DNSCache)) @@ -346,6 +390,9 @@ func (b *BaseDendrite) SetupAndServeHTTP( Addr: string(externalAddr), WriteTimeout: HTTPServerTimeout, Handler: externalRouter, + BaseContext: func(_ net.Listener) context.Context { + return b.ProcessContext.Context() + }, } internalServ := externalServ @@ -361,6 +408,9 @@ func (b *BaseDendrite) SetupAndServeHTTP( internalServ = &http.Server{ Addr: string(internalAddr), Handler: h2c.NewHandler(internalRouter, internalH2S), + BaseContext: func(_ net.Listener) context.Context { + return b.ProcessContext.Context() + }, } } @@ -462,20 +512,22 @@ func (b *BaseDendrite) SetupAndServeHTTP( }() } + minwinsvc.SetOnExit(b.ProcessContext.ShutdownDendrite) <-b.ProcessContext.WaitForShutdown() - ctx, cancel := context.WithCancel(context.Background()) - cancel() - - _ = internalServ.Shutdown(ctx) - _ = externalServ.Shutdown(ctx) + logrus.Infof("Stopping HTTP listeners") + _ = internalServ.Shutdown(context.Background()) + _ = externalServ.Shutdown(context.Background()) logrus.Infof("Stopped HTTP listeners") } func (b *BaseDendrite) WaitForShutdown() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - <-sigs + select { + case <-sigs: + case <-b.ProcessContext.WaitForShutdown(): + } signal.Reset(syscall.SIGINT, syscall.SIGTERM) logrus.Warnf("Shutdown signal received") diff --git a/setup/config/config.go b/setup/config/config.go index e03518e24..9b9000a62 100644 --- a/setup/config/config.go +++ b/setup/config/config.go @@ -78,6 +78,8 @@ type Dendrite struct { // Any information derived from the configuration options for later use. Derived Derived `yaml:"-"` + + IsMonolith bool `yaml:"-"` } // TODO: Kill Derived @@ -210,6 +212,7 @@ func loadConfig( ) (*Dendrite, error) { var c Dendrite c.Defaults(false) + c.IsMonolith = monolithic var err error if err = yaml.Unmarshal(configData, &c); err != nil { diff --git a/setup/config/config_appservice.go b/setup/config/config_appservice.go index 3f4e1c917..ff3287714 100644 --- a/setup/config/config_appservice.go +++ b/setup/config/config_appservice.go @@ -50,9 +50,14 @@ func (c *AppServiceAPI) Defaults(generate bool) { } func (c *AppServiceAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "app_service_api.database.connection_string", string(c.Database.ConnectionString)) + } + if isMonolith { // polylith required configs below + return + } checkURL(configErrs, "app_service_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "app_service_api.internal_api.bind", string(c.InternalAPI.Connect)) - checkNotEmpty(configErrs, "app_service_api.database.connection_string", string(c.Database.ConnectionString)) + checkURL(configErrs, "app_service_api.internal_api.connect", string(c.InternalAPI.Connect)) } // ApplicationServiceNamespace is the namespace that a specific application diff --git a/setup/config/config_clientapi.go b/setup/config/config_clientapi.go index 4590e752b..bb786a145 100644 --- a/setup/config/config_clientapi.go +++ b/setup/config/config_clientapi.go @@ -15,6 +15,12 @@ type ClientAPI struct { // If set disables new users from registering (except via shared // secrets) RegistrationDisabled bool `yaml:"registration_disabled"` + + // Enable registration without captcha verification or shared secret. + // This option is populated by the -really-enable-open-registration + // command line parameter as it is not recommended. + OpenRegistrationWithoutVerificationEnabled bool `yaml:"-"` + // If set, allows registration by anyone who also has the shared // secret, even if registration is otherwise disabled. RegistrationSharedSecret string `yaml:"registration_shared_secret"` @@ -55,23 +61,38 @@ func (c *ClientAPI) Defaults(generate bool) { c.RecaptchaEnabled = false c.RecaptchaBypassSecret = "" c.RecaptchaSiteVerifyAPI = "" - c.RegistrationDisabled = false + c.RegistrationDisabled = true + c.OpenRegistrationWithoutVerificationEnabled = false c.RateLimiting.Defaults() } func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "client_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "client_api.internal_api.connect", string(c.InternalAPI.Connect)) - if !isMonolith { - checkURL(configErrs, "client_api.external_api.listen", string(c.ExternalAPI.Listen)) - } - if c.RecaptchaEnabled { - checkNotEmpty(configErrs, "client_api.recaptcha_public_key", string(c.RecaptchaPublicKey)) - checkNotEmpty(configErrs, "client_api.recaptcha_private_key", string(c.RecaptchaPrivateKey)) - checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", string(c.RecaptchaSiteVerifyAPI)) - } c.TURN.Verify(configErrs) c.RateLimiting.Verify(configErrs) + if c.RecaptchaEnabled { + checkNotEmpty(configErrs, "client_api.recaptcha_public_key", c.RecaptchaPublicKey) + checkNotEmpty(configErrs, "client_api.recaptcha_private_key", c.RecaptchaPrivateKey) + checkNotEmpty(configErrs, "client_api.recaptcha_siteverify_api", c.RecaptchaSiteVerifyAPI) + } + // Ensure there is any spam counter measure when enabling registration + if !c.RegistrationDisabled && !c.OpenRegistrationWithoutVerificationEnabled { + if !c.RecaptchaEnabled { + configErrs.Add( + "You have tried to enable open registration without any secondary verification methods " + + "(such as reCAPTCHA). By enabling open registration, you are SIGNIFICANTLY " + + "increasing the risk that your server will be used to send spam or abuse, and may result in " + + "your server being banned from some rooms. If you are ABSOLUTELY CERTAIN you want to do this, " + + "start Dendrite with the -really-enable-open-registration command line flag. Otherwise, you " + + "should set the registration_disabled option in your Dendrite config.", + ) + } + } + if isMonolith { // polylith required configs below + return + } + checkURL(configErrs, "client_api.internal_api.listen", string(c.InternalAPI.Listen)) + checkURL(configErrs, "client_api.internal_api.connect", string(c.InternalAPI.Connect)) + checkURL(configErrs, "client_api.external_api.listen", string(c.ExternalAPI.Listen)) } type TURN struct { diff --git a/setup/config/config_federationapi.go b/setup/config/config_federationapi.go index 176334dd8..a7a515fda 100644 --- a/setup/config/config_federationapi.go +++ b/setup/config/config_federationapi.go @@ -34,22 +34,24 @@ func (c *FederationAPI) Defaults(generate bool) { c.InternalAPI.Listen = "http://localhost:7772" c.InternalAPI.Connect = "http://localhost:7772" c.ExternalAPI.Listen = "http://[::]:8072" + c.FederationMaxRetries = 16 + c.DisableTLSValidation = false c.Database.Defaults(10) if generate { c.Database.ConnectionString = "file:federationapi.db" } - - c.FederationMaxRetries = 16 - c.DisableTLSValidation = false } func (c *FederationAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "federation_api.database.connection_string", string(c.Database.ConnectionString)) + } + if isMonolith { // polylith required configs below + return + } + checkURL(configErrs, "federation_api.external_api.listen", string(c.ExternalAPI.Listen)) checkURL(configErrs, "federation_api.internal_api.listen", string(c.InternalAPI.Listen)) checkURL(configErrs, "federation_api.internal_api.connect", string(c.InternalAPI.Connect)) - if !isMonolith { - checkURL(configErrs, "federation_api.external_api.listen", string(c.ExternalAPI.Listen)) - } - checkNotEmpty(configErrs, "federation_api.database.connection_string", string(c.Database.ConnectionString)) } // The config for setting a proxy to use for server->server requests diff --git a/setup/config/config_global.go b/setup/config/config_global.go index c1650f077..9d4c1485e 100644 --- a/setup/config/config_global.go +++ b/setup/config/config_global.go @@ -34,6 +34,13 @@ type Global struct { // Defaults to 24 hours. KeyValidityPeriod time.Duration `yaml:"key_validity_period"` + // Global pool of database connections, which is used only in monolith mode. If a + // component does not specify any database options of its own, then this pool of + // connections will be used instead. This way we don't have to manage connection + // counts on a per-component basis, but can instead do it for the entire monolith. + // In a polylith deployment, this will be ignored. + DatabaseOptions DatabaseOptions `yaml:"database"` + // The server name to delegate server-server communications to, with optional port WellKnownServerName string `yaml:"well_known_server_name"` @@ -63,6 +70,9 @@ type Global struct { // ServerNotices configuration used for sending server notices ServerNotices ServerNotices `yaml:"server_notices"` + + // ReportStats configures opt-in anonymous stats reporting. + ReportStats ReportStats `yaml:"report_stats"` } func (c *Global) Defaults(generate bool) { @@ -79,6 +89,7 @@ func (c *Global) Defaults(generate bool) { c.DNSCache.Defaults() c.Sentry.Defaults() c.ServerNotices.Defaults(generate) + c.ReportStats.Defaults() } func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) { @@ -90,6 +101,7 @@ func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) { c.Sentry.Verify(configErrs, isMonolith) c.DNSCache.Verify(configErrs, isMonolith) c.ServerNotices.Verify(configErrs, isMonolith) + c.ReportStats.Verify(configErrs, isMonolith) } type OldVerifyKeys struct { @@ -156,6 +168,26 @@ func (c *ServerNotices) Defaults(generate bool) { func (c *ServerNotices) Verify(errors *ConfigErrors, isMonolith bool) {} +// ReportStats configures opt-in anonymous stats reporting. +type ReportStats struct { + // Enabled configures anonymous usage stats of the server + Enabled bool `yaml:"enabled"` + + // Endpoint the endpoint to report stats to + Endpoint string `yaml:"endpoint"` +} + +func (c *ReportStats) Defaults() { + c.Enabled = false + c.Endpoint = "https://matrix.org/report-usage-stats/push" +} + +func (c *ReportStats) Verify(configErrs *ConfigErrors, isMonolith bool) { + if c.Enabled { + checkNotEmpty(configErrs, "global.report_stats.endpoint", c.Endpoint) + } +} + // The configuration to use for Sentry error reporting type Sentry struct { Enabled bool `yaml:"enabled"` diff --git a/setup/config/config_jetstream.go b/setup/config/config_jetstream.go index b6a93d398..e4cfd4d3b 100644 --- a/setup/config/config_jetstream.go +++ b/setup/config/config_jetstream.go @@ -36,9 +36,10 @@ func (c *JetStream) Defaults(generate bool) { } func (c *JetStream) Verify(configErrs *ConfigErrors, isMonolith bool) { + if isMonolith { // polylith required configs below + return + } // If we are running in a polylith deployment then we need at least // one NATS JetStream server to talk to. - if !isMonolith { - checkNotZero(configErrs, "global.jetstream.addresses", int64(len(c.Addresses))) - } + checkNotZero(configErrs, "global.jetstream.addresses", int64(len(c.Addresses))) } diff --git a/setup/config/config_keyserver.go b/setup/config/config_keyserver.go index 6180ccbc8..5f2f22c8a 100644 --- a/setup/config/config_keyserver.go +++ b/setup/config/config_keyserver.go @@ -18,7 +18,12 @@ func (c *KeyServer) Defaults(generate bool) { } func (c *KeyServer) Verify(configErrs *ConfigErrors, isMonolith bool) { + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "key_server.database.connection_string", string(c.Database.ConnectionString)) + } + if isMonolith { // polylith required configs below + return + } checkURL(configErrs, "key_server.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "key_server.internal_api.bind", string(c.InternalAPI.Connect)) - checkNotEmpty(configErrs, "key_server.database.connection_string", string(c.Database.ConnectionString)) + checkURL(configErrs, "key_server.internal_api.connect", string(c.InternalAPI.Connect)) } diff --git a/setup/config/config_mediaapi.go b/setup/config/config_mediaapi.go index 9a7d84969..9717aa59e 100644 --- a/setup/config/config_mediaapi.go +++ b/setup/config/config_mediaapi.go @@ -23,7 +23,7 @@ type MediaAPI struct { // The maximum file size in bytes that is allowed to be stored on this server. // Note: if max_file_size_bytes is set to 0, the size is unlimited. // Note: if max_file_size_bytes is not set, it will default to 10485760 (10MB) - MaxFileSizeBytes *FileSizeBytes `yaml:"max_file_size_bytes,omitempty"` + MaxFileSizeBytes FileSizeBytes `yaml:"max_file_size_bytes,omitempty"` // Whether to dynamically generate thumbnails on-the-fly if the requested resolution is not already generated DynamicThumbnails bool `yaml:"dynamic_thumbnails"` @@ -42,30 +42,31 @@ func (c *MediaAPI) Defaults(generate bool) { c.InternalAPI.Listen = "http://localhost:7774" c.InternalAPI.Connect = "http://localhost:7774" c.ExternalAPI.Listen = "http://[::]:8074" + c.MaxFileSizeBytes = DefaultMaxFileSizeBytes + c.MaxThumbnailGenerators = 10 c.Database.Defaults(5) if generate { c.Database.ConnectionString = "file:mediaapi.db" c.BasePath = "./media_store" } - - c.MaxFileSizeBytes = &DefaultMaxFileSizeBytes - c.MaxThumbnailGenerators = 10 } func (c *MediaAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "media_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "media_api.internal_api.connect", string(c.InternalAPI.Connect)) - if !isMonolith { - checkURL(configErrs, "media_api.external_api.listen", string(c.ExternalAPI.Listen)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "media_api.database.connection_string", string(c.Database.ConnectionString)) } - checkNotEmpty(configErrs, "media_api.database.connection_string", string(c.Database.ConnectionString)) - checkNotEmpty(configErrs, "media_api.base_path", string(c.BasePath)) - checkPositive(configErrs, "media_api.max_file_size_bytes", int64(*c.MaxFileSizeBytes)) + checkPositive(configErrs, "media_api.max_file_size_bytes", int64(c.MaxFileSizeBytes)) checkPositive(configErrs, "media_api.max_thumbnail_generators", int64(c.MaxThumbnailGenerators)) for i, size := range c.ThumbnailSizes { checkPositive(configErrs, fmt.Sprintf("media_api.thumbnail_sizes[%d].width", i), int64(size.Width)) checkPositive(configErrs, fmt.Sprintf("media_api.thumbnail_sizes[%d].height", i), int64(size.Height)) } + if isMonolith { // polylith required configs below + return + } + checkURL(configErrs, "media_api.internal_api.listen", string(c.InternalAPI.Listen)) + checkURL(configErrs, "media_api.internal_api.connect", string(c.InternalAPI.Connect)) + checkURL(configErrs, "media_api.external_api.listen", string(c.ExternalAPI.Listen)) } diff --git a/setup/config/config_mscs.go b/setup/config/config_mscs.go index 66a4c80c9..b992f7152 100644 --- a/setup/config/config_mscs.go +++ b/setup/config/config_mscs.go @@ -31,5 +31,7 @@ func (c *MSCs) Enabled(msc string) bool { } func (c *MSCs) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkNotEmpty(configErrs, "mscs.database.connection_string", string(c.Database.ConnectionString)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "mscs.database.connection_string", string(c.Database.ConnectionString)) + } } diff --git a/setup/config/config_roomserver.go b/setup/config/config_roomserver.go index 73abb4f47..bd6aa1167 100644 --- a/setup/config/config_roomserver.go +++ b/setup/config/config_roomserver.go @@ -18,7 +18,12 @@ func (c *RoomServer) Defaults(generate bool) { } func (c *RoomServer) Verify(configErrs *ConfigErrors, isMonolith bool) { + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "room_server.database.connection_string", string(c.Database.ConnectionString)) + } + if isMonolith { // polylith required configs below + return + } checkURL(configErrs, "room_server.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "room_server.internal_ap.bind", string(c.InternalAPI.Connect)) - checkNotEmpty(configErrs, "room_server.database.connection_string", string(c.Database.ConnectionString)) + checkURL(configErrs, "room_server.internal_ap.connect", string(c.InternalAPI.Connect)) } diff --git a/setup/config/config_syncapi.go b/setup/config/config_syncapi.go index dc813cb7d..7d5e3808a 100644 --- a/setup/config/config_syncapi.go +++ b/setup/config/config_syncapi.go @@ -22,10 +22,13 @@ func (c *SyncAPI) Defaults(generate bool) { } func (c *SyncAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "sync_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "sync_api.internal_api.bind", string(c.InternalAPI.Connect)) - if !isMonolith { - checkURL(configErrs, "sync_api.external_api.listen", string(c.ExternalAPI.Listen)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "sync_api.database", string(c.Database.ConnectionString)) } - checkNotEmpty(configErrs, "sync_api.database", string(c.Database.ConnectionString)) + if isMonolith { // polylith required configs below + return + } + checkURL(configErrs, "sync_api.internal_api.listen", string(c.InternalAPI.Listen)) + checkURL(configErrs, "sync_api.internal_api.connect", string(c.InternalAPI.Connect)) + checkURL(configErrs, "sync_api.external_api.listen", string(c.ExternalAPI.Listen)) } diff --git a/setup/config/config_userapi.go b/setup/config/config_userapi.go index 570dc6030..d1e2b7fe1 100644 --- a/setup/config/config_userapi.go +++ b/setup/config/config_userapi.go @@ -26,17 +26,22 @@ const DefaultOpenIDTokenLifetimeMS = 3600000 // 60 minutes func (c *UserAPI) Defaults(generate bool) { c.InternalAPI.Listen = "http://localhost:7781" c.InternalAPI.Connect = "http://localhost:7781" + c.BCryptCost = bcrypt.DefaultCost + c.OpenIDTokenLifetimeMS = DefaultOpenIDTokenLifetimeMS c.AccountDatabase.Defaults(10) if generate { c.AccountDatabase.ConnectionString = "file:userapi_accounts.db" } - c.BCryptCost = bcrypt.DefaultCost - c.OpenIDTokenLifetimeMS = DefaultOpenIDTokenLifetimeMS } func (c *UserAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { + checkPositive(configErrs, "user_api.openid_token_lifetime_ms", c.OpenIDTokenLifetimeMS) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "user_api.account_database.connection_string", string(c.AccountDatabase.ConnectionString)) + } + if isMonolith { // polylith required configs below + return + } checkURL(configErrs, "user_api.internal_api.listen", string(c.InternalAPI.Listen)) checkURL(configErrs, "user_api.internal_api.connect", string(c.InternalAPI.Connect)) - checkNotEmpty(configErrs, "user_api.account_database.connection_string", string(c.AccountDatabase.ConnectionString)) - checkPositive(configErrs, "user_api.openid_token_lifetime_ms", c.OpenIDTokenLifetimeMS) } diff --git a/setup/flags.go b/setup/flags.go index 281cf3392..a9dac61a1 100644 --- a/setup/flags.go +++ b/setup/flags.go @@ -25,8 +25,9 @@ import ( ) var ( - configPath = flag.String("config", "dendrite.yaml", "The path to the config file. For more information, see the config file in this repository.") - version = flag.Bool("version", false, "Shows the current version and exits immediately.") + configPath = flag.String("config", "dendrite.yaml", "The path to the config file. For more information, see the config file in this repository.") + version = flag.Bool("version", false, "Shows the current version and exits immediately.") + enableRegistrationWithoutVerification = flag.Bool("really-enable-open-registration", false, "This allows open registration without secondary verification (reCAPTCHA). This is NOT RECOMMENDED and will SIGNIFICANTLY increase the risk that your server will be used to send spam or conduct attacks, which may result in your server being banned from rooms.") ) // ParseFlags parses the commandline flags and uses them to create a config. @@ -48,5 +49,9 @@ func ParseFlags(monolith bool) *config.Dendrite { logrus.Fatalf("Invalid config file: %s", err) } + if *enableRegistrationWithoutVerification { + cfg.ClientAPI.OpenRegistrationWithoutVerificationEnabled = true + } + return cfg } diff --git a/setup/jetstream/helpers.go b/setup/jetstream/helpers.go index 78cecb6ae..1c07583e9 100644 --- a/setup/jetstream/helpers.go +++ b/setup/jetstream/helpers.go @@ -35,6 +35,16 @@ func JetStreamConsumer( } go func() { for { + // If the parent context has given up then there's no point in + // carrying on doing anything, so stop the listener. + select { + case <-ctx.Done(): + if err := sub.Unsubscribe(); err != nil { + logrus.WithContext(ctx).Warnf("Failed to unsubscribe %q", durable) + } + return + default: + } // The context behaviour here is surprising — we supply a context // so that we can interrupt the fetch if we want, but NATS will still // enforce its own deadline (roughly 5 seconds by default). Therefore @@ -65,18 +75,18 @@ func JetStreamConsumer( continue } msg := msgs[0] - if err = msg.InProgress(); err != nil { + if err = msg.InProgress(nats.Context(ctx)); err != nil { logrus.WithContext(ctx).WithField("subject", subj).Warn(fmt.Errorf("msg.InProgress: %w", err)) sentry.CaptureException(err) continue } if f(ctx, msg) { - if err = msg.AckSync(); err != nil { + if err = msg.AckSync(nats.Context(ctx)); err != nil { logrus.WithContext(ctx).WithField("subject", subj).Warn(fmt.Errorf("msg.AckSync: %w", err)) sentry.CaptureException(err) } } else { - if err = msg.Nak(); err != nil { + if err = msg.Nak(nats.Context(ctx)); err != nil { logrus.WithContext(ctx).WithField("subject", subj).Warn(fmt.Errorf("msg.Nak: %w", err)) sentry.CaptureException(err) } diff --git a/setup/jetstream/nats.go b/setup/jetstream/nats.go index 1c8a89e8d..248b0e656 100644 --- a/setup/jetstream/nats.go +++ b/setup/jetstream/nats.go @@ -17,54 +17,55 @@ import ( natsclient "github.com/nats-io/nats.go" ) -var natsServer *natsserver.Server -var natsServerMutex sync.Mutex - -func PrepareForTests() (*process.ProcessContext, nats.JetStreamContext, *nats.Conn) { - cfg := &config.Dendrite{} - cfg.Defaults(true) - cfg.Global.JetStream.InMemory = true - pc := process.NewProcessContext() - js, jc := Prepare(pc, &cfg.Global.JetStream) - return pc, js, jc +type NATSInstance struct { + *natsserver.Server + sync.Mutex } -func Prepare(process *process.ProcessContext, cfg *config.JetStream) (natsclient.JetStreamContext, *natsclient.Conn) { +func DeleteAllStreams(js nats.JetStreamContext, cfg *config.JetStream) { + for _, stream := range streams { // streams are defined in streams.go + name := cfg.Prefixed(stream.Name) + _ = js.DeleteStream(name) + } +} + +func (s *NATSInstance) Prepare(process *process.ProcessContext, cfg *config.JetStream) (natsclient.JetStreamContext, *natsclient.Conn) { // check if we need an in-process NATS Server if len(cfg.Addresses) != 0 { return setupNATS(process, cfg, nil) } - natsServerMutex.Lock() - if natsServer == nil { + s.Lock() + if s.Server == nil { var err error - natsServer, err = natsserver.NewServer(&natsserver.Options{ + s.Server, err = natsserver.NewServer(&natsserver.Options{ ServerName: "monolith", DontListen: true, JetStream: true, StoreDir: string(cfg.StoragePath), NoSystemAccount: true, MaxPayload: 16 * 1024 * 1024, + NoSigs: true, }) if err != nil { panic(err) } - natsServer.ConfigureLogger() + s.ConfigureLogger() go func() { process.ComponentStarted() - natsServer.Start() + s.Start() }() go func() { <-process.WaitForShutdown() - natsServer.Shutdown() - natsServer.WaitForShutdown() + s.Shutdown() + s.WaitForShutdown() process.ComponentFinished() }() } - natsServerMutex.Unlock() - if !natsServer.ReadyForConnections(time.Second * 10) { + s.Unlock() + if !s.ReadyForConnections(time.Second * 10) { logrus.Fatalln("NATS did not start in time") } - nc, err := natsclient.Connect("", natsclient.InProcessServer(natsServer)) + nc, err := natsclient.Connect("", natsclient.InProcessServer(s)) if err != nil { logrus.Fatalln("Failed to create NATS client") } diff --git a/setup/monolith.go b/setup/monolith.go index 32f1a6494..41a897024 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -15,7 +15,6 @@ package setup import ( - "github.com/gorilla/mux" appserviceAPI "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/clientapi" "github.com/matrix-org/dendrite/clientapi/api" @@ -25,11 +24,10 @@ import ( keyAPI "github.com/matrix-org/dendrite/keyserver/api" "github.com/matrix-org/dendrite/mediaapi" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/dendrite/setup/process" "github.com/matrix-org/dendrite/syncapi" userapi "github.com/matrix-org/dendrite/userapi/api" - userdb "github.com/matrix-org/dendrite/userapi/storage" "github.com/matrix-org/gomatrixserverlib" ) @@ -37,12 +35,11 @@ import ( // all components of Dendrite, for use in monolith mode. type Monolith struct { Config *config.Dendrite - AccountDB userdb.Database KeyRing *gomatrixserverlib.KeyRing Client *gomatrixserverlib.Client FedClient *gomatrixserverlib.FederationClient - AppserviceAPI appserviceAPI.AppServiceQueryAPI + AppserviceAPI appserviceAPI.AppServiceInternalAPI FederationAPI federationAPI.FederationInternalAPI RoomserverAPI roomserverAPI.RoomserverInternalAPI UserAPI userapi.UserInternalAPI @@ -50,30 +47,28 @@ type Monolith struct { // Optional ExtPublicRoomsProvider api.ExtraPublicRoomsProvider - ExtUserDirectoryProvider userapi.UserDirectoryProvider + ExtUserDirectoryProvider userapi.QuerySearchProfilesAPI } // AddAllPublicRoutes attaches all public paths to the given router -func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux *mux.Router) { +func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) { userDirectoryProvider := m.ExtUserDirectoryProvider if userDirectoryProvider == nil { userDirectoryProvider = m.UserAPI } clientapi.AddPublicRoutes( - process, csMux, synapseMux, &m.Config.ClientAPI, - m.FedClient, m.RoomserverAPI, - m.AppserviceAPI, transactions.New(), + base, m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(), m.FederationAPI, m.UserAPI, userDirectoryProvider, m.KeyAPI, - m.ExtPublicRoomsProvider, &m.Config.MSCs, + m.ExtPublicRoomsProvider, ) federationapi.AddPublicRoutes( - process, ssMux, keyMux, wkMux, &m.Config.FederationAPI, m.UserAPI, m.FedClient, - m.KeyRing, m.RoomserverAPI, m.FederationAPI, - m.KeyAPI, &m.Config.MSCs, nil, + base, m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, + m.KeyAPI, nil, + ) + mediaapi.AddPublicRoutes( + base, m.UserAPI, m.Client, ) - mediaapi.AddPublicRoutes(mediaMux, &m.Config.MediaAPI, &m.Config.ClientAPI.RateLimiting, m.UserAPI, m.Client) syncapi.AddPublicRoutes( - process, csMux, m.UserAPI, m.RoomserverAPI, - m.KeyAPI, m.FedClient, &m.Config.SyncAPI, + base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, ) } diff --git a/setup/mscs/msc2836/msc2836.go b/setup/mscs/msc2836/msc2836.go index 29c781a88..452b14580 100644 --- a/setup/mscs/msc2836/msc2836.go +++ b/setup/mscs/msc2836/msc2836.go @@ -102,7 +102,7 @@ func Enable( base *base.BaseDendrite, rsAPI roomserver.RoomserverInternalAPI, fsAPI fs.FederationInternalAPI, userAPI userapi.UserInternalAPI, keyRing gomatrixserverlib.JSONVerifier, ) error { - db, err := NewDatabase(&base.Cfg.MSCs.Database) + db, err := NewDatabase(base, &base.Cfg.MSCs.Database) if err != nil { return fmt.Errorf("cannot enable MSC2836: %w", err) } diff --git a/setup/mscs/msc2836/storage.go b/setup/mscs/msc2836/storage.go index 72523916b..827e82f70 100644 --- a/setup/mscs/msc2836/storage.go +++ b/setup/mscs/msc2836/storage.go @@ -8,6 +8,7 @@ import ( "encoding/json" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" @@ -58,19 +59,17 @@ type DB struct { } // NewDatabase loads the database for msc2836 -func NewDatabase(dbOpts *config.DatabaseOptions) (Database, error) { +func NewDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) { if dbOpts.ConnectionString.IsPostgres() { - return newPostgresDatabase(dbOpts) + return newPostgresDatabase(base, dbOpts) } - return newSQLiteDatabase(dbOpts) + return newSQLiteDatabase(base, dbOpts) } -func newPostgresDatabase(dbOpts *config.DatabaseOptions) (Database, error) { - d := DB{ - writer: sqlutil.NewDummyWriter(), - } +func newPostgresDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) { + d := DB{} var err error - if d.db, err = sqlutil.Open(dbOpts); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbOpts, sqlutil.NewDummyWriter()); err != nil { return nil, err } _, err = d.db.Exec(` @@ -145,12 +144,10 @@ func newPostgresDatabase(dbOpts *config.DatabaseOptions) (Database, error) { return &d, err } -func newSQLiteDatabase(dbOpts *config.DatabaseOptions) (Database, error) { - d := DB{ - writer: sqlutil.NewExclusiveWriter(), - } +func newSQLiteDatabase(base *base.BaseDendrite, dbOpts *config.DatabaseOptions) (Database, error) { + d := DB{} var err error - if d.db, err = sqlutil.Open(dbOpts); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbOpts, sqlutil.NewExclusiveWriter()); err != nil { return nil, err } _, err = d.db.Exec(` diff --git a/syncapi/consumers/keychange.go b/syncapi/consumers/keychange.go index e806f76e6..c8d88ddac 100644 --- a/syncapi/consumers/keychange.go +++ b/syncapi/consumers/keychange.go @@ -42,8 +42,7 @@ type OutputKeyChangeEventConsumer struct { notifier *notifier.Notifier stream types.StreamProvider serverName gomatrixserverlib.ServerName // our server name - rsAPI roomserverAPI.RoomserverInternalAPI - keyAPI api.KeyInternalAPI + rsAPI roomserverAPI.SyncRoomserverAPI } // NewOutputKeyChangeEventConsumer creates a new OutputKeyChangeEventConsumer. @@ -53,8 +52,7 @@ func NewOutputKeyChangeEventConsumer( cfg *config.SyncAPI, topic string, js nats.JetStreamContext, - keyAPI api.KeyInternalAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.SyncRoomserverAPI, store storage.Database, notifier *notifier.Notifier, stream types.StreamProvider, @@ -66,7 +64,6 @@ func NewOutputKeyChangeEventConsumer( topic: topic, db: store, serverName: cfg.Matrix.ServerName, - keyAPI: keyAPI, rsAPI: rsAPI, notifier: notifier, stream: stream, diff --git a/syncapi/consumers/presence.go b/syncapi/consumers/presence.go index b198b2292..bfd72d604 100644 --- a/syncapi/consumers/presence.go +++ b/syncapi/consumers/presence.go @@ -41,7 +41,7 @@ type PresenceConsumer struct { db storage.Database stream types.StreamProvider notifier *notifier.Notifier - deviceAPI api.UserDeviceAPI + deviceAPI api.SyncUserAPI cfg *config.SyncAPI } @@ -55,7 +55,7 @@ func NewPresenceConsumer( db storage.Database, notifier *notifier.Notifier, stream types.StreamProvider, - deviceAPI api.UserDeviceAPI, + deviceAPI api.SyncUserAPI, ) *PresenceConsumer { return &PresenceConsumer{ ctx: process.Context(), @@ -88,6 +88,11 @@ func (s *PresenceConsumer) Start() error { } return } + if presence == nil { + presence = &types.PresenceInternal{ + UserID: userID, + } + } deviceRes := api.QueryDevicesResponse{} if err = s.deviceAPI.QueryDevices(s.ctx, &api.QueryDevicesRequest{UserID: userID}, &deviceRes); err != nil { @@ -106,7 +111,9 @@ func (s *PresenceConsumer) Start() error { m.Header.Set(jetstream.UserID, presence.UserID) m.Header.Set("presence", presence.ClientFields.Presence) - m.Header.Set("status_msg", *presence.ClientFields.StatusMsg) + if presence.ClientFields.StatusMsg != nil { + m.Header.Set("status_msg", *presence.ClientFields.StatusMsg) + } m.Header.Set("last_active_ts", strconv.Itoa(int(presence.LastActiveTS))) if err = msg.RespondMsg(m); err != nil { @@ -131,9 +138,12 @@ func (s *PresenceConsumer) onMessage(ctx context.Context, msg *nats.Msg) bool { presence := msg.Header.Get("presence") timestamp := msg.Header.Get("last_active_ts") fromSync, _ := strconv.ParseBool(msg.Header.Get("from_sync")) - logrus.Debugf("syncAPI received presence event: %+v", msg.Header) + if fromSync { // do not process local presence changes; we already did this synchronously. + return true + } + ts, err := strconv.Atoi(timestamp) if err != nil { return true @@ -144,15 +154,19 @@ func (s *PresenceConsumer) onMessage(ctx context.Context, msg *nats.Msg) bool { newMsg := msg.Header.Get("status_msg") statusMsg = &newMsg } - // OK is already checked, so no need to do it again + // already checked, so no need to check error p, _ := types.PresenceFromString(presence) - pos, err := s.db.UpdatePresence(ctx, userID, p, statusMsg, gomatrixserverlib.Timestamp(ts), fromSync) - if err != nil { - return true - } - - s.stream.Advance(pos) - s.notifier.OnNewPresence(types.StreamingToken{PresencePosition: pos}, userID) + s.EmitPresence(ctx, userID, p, statusMsg, ts, fromSync) return true } + +func (s *PresenceConsumer) EmitPresence(ctx context.Context, userID string, presence types.Presence, statusMsg *string, ts int, fromSync bool) { + pos, err := s.db.UpdatePresence(ctx, userID, presence, statusMsg, gomatrixserverlib.Timestamp(ts), fromSync) + if err != nil { + logrus.WithError(err).WithField("user", userID).WithField("presence", presence).Warn("failed to updated presence for user") + return + } + s.stream.Advance(pos) + s.notifier.OnNewPresence(types.StreamingToken{PresencePosition: pos}, userID) +} diff --git a/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go index 5bdc0fad7..f0ca2106f 100644 --- a/syncapi/consumers/roomserver.go +++ b/syncapi/consumers/roomserver.go @@ -38,7 +38,7 @@ import ( type OutputRoomEventConsumer struct { ctx context.Context cfg *config.SyncAPI - rsAPI api.RoomserverInternalAPI + rsAPI api.SyncRoomserverAPI jetstream nats.JetStreamContext durable string topic string @@ -58,7 +58,7 @@ func NewOutputRoomEventConsumer( notifier *notifier.Notifier, pduStream types.StreamProvider, inviteStream types.StreamProvider, - rsAPI api.RoomserverInternalAPI, + rsAPI api.SyncRoomserverAPI, producer *producers.UserAPIStreamEventProducer, ) *OutputRoomEventConsumer { return &OutputRoomEventConsumer{ @@ -154,41 +154,61 @@ func (s *OutputRoomEventConsumer) onNewRoomEvent( ctx context.Context, msg api.OutputNewRoomEvent, ) error { ev := msg.Event + addsStateEvents, missingEventIDs := msg.NeededStateEventIDs() - addsStateEvents := []*gomatrixserverlib.HeaderedEvent{} - foundEventIDs := map[string]bool{} - if len(msg.AddsStateEventIDs) > 0 { - for _, eventID := range msg.AddsStateEventIDs { - foundEventIDs[eventID] = false - } - foundEvents, err := s.db.Events(ctx, msg.AddsStateEventIDs) + // Work out the list of events we need to find out about. Either + // they will be the event supplied in the request, we will find it + // in the sync API database or we'll need to ask the roomserver. + knownEventIDs := make(map[string]bool, len(msg.AddsStateEventIDs)) + for _, eventID := range missingEventIDs { + knownEventIDs[eventID] = false + } + + // Look the events up in the database. If we know them, add them into + // the set of adds state events. + if len(missingEventIDs) > 0 { + alreadyKnown, err := s.db.Events(ctx, missingEventIDs) if err != nil { return fmt.Errorf("s.db.Events: %w", err) } - for _, event := range foundEvents { - foundEventIDs[event.EventID()] = true + for _, knownEvent := range alreadyKnown { + knownEventIDs[knownEvent.EventID()] = true + addsStateEvents = append(addsStateEvents, knownEvent) + } + } + + // Now work out if there are any remaining events we don't know. For + // these we will need to ask the roomserver for help. + missingEventIDs = missingEventIDs[:0] + for eventID, known := range knownEventIDs { + if !known { + missingEventIDs = append(missingEventIDs, eventID) + } + } + + // Ask the roomserver and add in the rest of the results into the set. + // Finally, work out if there are any more events missing. + if len(missingEventIDs) > 0 { + eventsReq := &api.QueryEventsByIDRequest{ + EventIDs: missingEventIDs, } - eventsReq := &api.QueryEventsByIDRequest{} eventsRes := &api.QueryEventsByIDResponse{} - for eventID, found := range foundEventIDs { - if !found { - eventsReq.EventIDs = append(eventsReq.EventIDs, eventID) - } - } - if err = s.rsAPI.QueryEventsByID(ctx, eventsReq, eventsRes); err != nil { + if err := s.rsAPI.QueryEventsByID(ctx, eventsReq, eventsRes); err != nil { return fmt.Errorf("s.rsAPI.QueryEventsByID: %w", err) } for _, event := range eventsRes.Events { - eventID := event.EventID() - foundEvents = append(foundEvents, event) - foundEventIDs[eventID] = true + addsStateEvents = append(addsStateEvents, event) + knownEventIDs[event.EventID()] = true } - for eventID, found := range foundEventIDs { + + // This should never happen because this would imply that the + // roomserver has sent us adds_state_event_ids for events that it + // also doesn't know about, but let's just be sure. + for eventID, found := range knownEventIDs { if !found { return fmt.Errorf("event %s is missing", eventID) } } - addsStateEvents = foundEvents } ev, err := s.updateStateEvent(ev) @@ -327,9 +347,11 @@ func (s *OutputRoomEventConsumer) onNewInviteEvent( ctx context.Context, msg api.OutputNewInviteEvent, ) { if msg.Event.StateKey() == nil { - log.WithFields(log.Fields{ - "event": string(msg.Event.JSON()), - }).Panicf("roomserver output log: invite has no state key") + return + } + if _, serverName, err := gomatrixserverlib.SplitID('@', *msg.Event.StateKey()); err != nil { + return + } else if serverName != s.cfg.Matrix.ServerName { return } pduPos, err := s.db.AddInviteEvent(ctx, msg.Event) diff --git a/syncapi/internal/keychange.go b/syncapi/internal/keychange.go index dc4acd8da..d96718d20 100644 --- a/syncapi/internal/keychange.go +++ b/syncapi/internal/keychange.go @@ -29,7 +29,7 @@ import ( const DeviceListLogName = "dl" // DeviceOTKCounts adds one-time key counts to the /sync response -func DeviceOTKCounts(ctx context.Context, keyAPI keyapi.KeyInternalAPI, userID, deviceID string, res *types.Response) error { +func DeviceOTKCounts(ctx context.Context, keyAPI keyapi.SyncKeyAPI, userID, deviceID string, res *types.Response) error { var queryRes keyapi.QueryOneTimeKeysResponse keyAPI.QueryOneTimeKeys(ctx, &keyapi.QueryOneTimeKeysRequest{ UserID: userID, @@ -46,7 +46,7 @@ func DeviceOTKCounts(ctx context.Context, keyAPI keyapi.KeyInternalAPI, userID, // was filled in, else false if there are no new device list changes because there is nothing to catch up on. The response MUST // be already filled in with join/leave information. func DeviceListCatchup( - ctx context.Context, keyAPI keyapi.KeyInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI, + ctx context.Context, keyAPI keyapi.SyncKeyAPI, rsAPI roomserverAPI.SyncRoomserverAPI, userID string, res *types.Response, from, to types.StreamPosition, ) (newPos types.StreamPosition, hasNew bool, err error) { @@ -130,7 +130,7 @@ func DeviceListCatchup( // TrackChangedUsers calculates the values of device_lists.changed|left in the /sync response. func TrackChangedUsers( - ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, userID string, newlyJoinedRooms, newlyLeftRooms []string, + ctx context.Context, rsAPI roomserverAPI.SyncRoomserverAPI, userID string, newlyJoinedRooms, newlyLeftRooms []string, ) (changed, left []string, err error) { // process leaves first, then joins afterwards so if we join/leave/join/leave we err on the side of including users. @@ -216,7 +216,7 @@ func TrackChangedUsers( } func filterSharedUsers( - ctx context.Context, rsAPI roomserverAPI.RoomserverInternalAPI, userID string, usersWithChangedKeys []string, + ctx context.Context, rsAPI roomserverAPI.SyncRoomserverAPI, userID string, usersWithChangedKeys []string, ) (map[string]int, []string) { var result []string var sharedUsersRes roomserverAPI.QuerySharedUsersResponse diff --git a/syncapi/notifier/notifier.go b/syncapi/notifier/notifier.go index 82834239b..87f0d86d7 100644 --- a/syncapi/notifier/notifier.go +++ b/syncapi/notifier/notifier.go @@ -333,6 +333,20 @@ func (n *Notifier) Load(ctx context.Context, db storage.Database) error { return nil } +// LoadRooms loads the membership states required to notify users correctly. +func (n *Notifier) LoadRooms(ctx context.Context, db storage.Database, roomIDs []string) error { + n.lock.Lock() + defer n.lock.Unlock() + + roomToUsers, err := db.AllJoinedUsersInRoom(ctx, roomIDs) + if err != nil { + return err + } + n.setUsersJoinedToRooms(roomToUsers) + + return nil +} + // CurrentPosition returns the current sync position func (n *Notifier) CurrentPosition() types.StreamingToken { n.lock.RLock() diff --git a/syncapi/routing/context.go b/syncapi/routing/context.go index 17215b669..96438e184 100644 --- a/syncapi/routing/context.go +++ b/syncapi/routing/context.go @@ -42,10 +42,10 @@ type ContextRespsonse struct { func Context( req *http.Request, device *userapi.Device, - rsAPI roomserver.RoomserverInternalAPI, + rsAPI roomserver.SyncRoomserverAPI, syncDB storage.Database, roomID, eventID string, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) util.JSONResponse { filter, err := parseRoomEventFilter(req) if err != nil { @@ -73,6 +73,12 @@ func Context( logrus.WithError(err).Error("unable to query membership") return jsonerror.InternalServerError() } + if !membershipRes.RoomExists { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("room does not exist"), + } + } stateFilter := gomatrixserverlib.StateFilter{ Limit: 100, @@ -155,7 +161,7 @@ func applyLazyLoadMembers( filter *gomatrixserverlib.RoomEventFilter, eventsAfter, eventsBefore []gomatrixserverlib.ClientEvent, state []*gomatrixserverlib.HeaderedEvent, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) []*gomatrixserverlib.HeaderedEvent { if filter == nil || !filter.LazyLoadMembers { return state diff --git a/syncapi/routing/filter.go b/syncapi/routing/filter.go index baa4d841c..1a10bd649 100644 --- a/syncapi/routing/filter.go +++ b/syncapi/routing/filter.go @@ -44,8 +44,8 @@ func GetFilter( return jsonerror.InternalServerError() } - filter, err := syncDB.GetFilter(req.Context(), localpart, filterID) - if err != nil { + filter := gomatrixserverlib.DefaultFilter() + if err := syncDB.GetFilter(req.Context(), &filter, localpart, filterID); err != nil { //TODO better error handling. This error message is *probably* right, // but if there are obscure db errors, this will also be returned, // even though it is not correct. diff --git a/syncapi/routing/messages.go b/syncapi/routing/messages.go index f34901bf2..e55c661d6 100644 --- a/syncapi/routing/messages.go +++ b/syncapi/routing/messages.go @@ -36,8 +36,7 @@ import ( type messagesReq struct { ctx context.Context db storage.Database - rsAPI api.RoomserverInternalAPI - federation *gomatrixserverlib.FederationClient + rsAPI api.SyncRoomserverAPI cfg *config.SyncAPI roomID string from *types.TopologyToken @@ -61,19 +60,24 @@ type messagesResp struct { // See: https://matrix.org/docs/spec/client_server/latest.html#get-matrix-client-r0-rooms-roomid-messages func OnIncomingMessagesRequest( req *http.Request, db storage.Database, roomID string, device *userapi.Device, - federation *gomatrixserverlib.FederationClient, - rsAPI api.RoomserverInternalAPI, + rsAPI api.SyncRoomserverAPI, cfg *config.SyncAPI, srp *sync.RequestPool, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) util.JSONResponse { var err error // check if the user has already forgotten about this room - isForgotten, err := checkIsRoomForgotten(req.Context(), roomID, device.UserID, rsAPI) + isForgotten, roomExists, err := checkIsRoomForgotten(req.Context(), roomID, device.UserID, rsAPI) if err != nil { return jsonerror.InternalServerError() } + if !roomExists { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden("room does not exist"), + } + } if isForgotten { return util.JSONResponse{ @@ -180,7 +184,6 @@ func OnIncomingMessagesRequest( ctx: req.Context(), db: db, rsAPI: rsAPI, - federation: federation, cfg: cfg, roomID: roomID, from: &from, @@ -247,17 +250,17 @@ func OnIncomingMessagesRequest( } } -func checkIsRoomForgotten(ctx context.Context, roomID, userID string, rsAPI api.RoomserverInternalAPI) (bool, error) { +func checkIsRoomForgotten(ctx context.Context, roomID, userID string, rsAPI api.SyncRoomserverAPI) (forgotten bool, exists bool, err error) { req := api.QueryMembershipForUserRequest{ RoomID: roomID, UserID: userID, } resp := api.QueryMembershipForUserResponse{} if err := rsAPI.QueryMembershipForUser(ctx, &req, &resp); err != nil { - return false, err + return false, false, err } - return resp.IsRoomForgotten, nil + return resp.IsRoomForgotten, resp.RoomExists, nil } // retrieveEvents retrieves events from the local database for a request on diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go index 4102cf073..6bc495d8d 100644 --- a/syncapi/routing/routing.go +++ b/syncapi/routing/routing.go @@ -36,10 +36,10 @@ import ( // nolint: gocyclo func Setup( csMux *mux.Router, srp *sync.RequestPool, syncDB storage.Database, - userAPI userapi.UserInternalAPI, federation *gomatrixserverlib.FederationClient, - rsAPI api.RoomserverInternalAPI, + userAPI userapi.SyncUserAPI, + rsAPI api.SyncRoomserverAPI, cfg *config.SyncAPI, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) { v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter() @@ -53,7 +53,7 @@ func Setup( if err != nil { return util.ErrorResponse(err) } - return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, federation, rsAPI, cfg, srp, lazyLoadCache) + return OnIncomingMessagesRequest(req, syncDB, vars["roomID"], device, rsAPI, cfg, srp, lazyLoadCache) })).Methods(http.MethodGet, http.MethodOptions) v3mux.Handle("/user/{userId}/filter", diff --git a/syncapi/storage/interface.go b/syncapi/storage/interface.go index 14cb08a52..5a036d889 100644 --- a/syncapi/storage/interface.go +++ b/syncapi/storage/interface.go @@ -39,6 +39,7 @@ type Database interface { GetStateDeltas(ctx context.Context, device *userapi.Device, r types.Range, userID string, stateFilter *gomatrixserverlib.StateFilter) ([]types.StateDelta, []string, error) RoomIDsWithMembership(ctx context.Context, userID string, membership string) ([]string, error) MembershipCount(ctx context.Context, roomID, membership string, pos types.StreamPosition) (int, error) + GetRoomHeroes(ctx context.Context, roomID, userID string, memberships []string) ([]string, error) RecentEvents(ctx context.Context, roomID string, r types.Range, eventFilter *gomatrixserverlib.RoomEventFilter, chronologicalOrder bool, onlySyncEvents bool) ([]types.StreamEvent, bool, error) @@ -51,6 +52,9 @@ type Database interface { // AllJoinedUsersInRooms returns a map of room ID to a list of all joined user IDs. AllJoinedUsersInRooms(ctx context.Context) (map[string][]string, error) + // AllJoinedUsersInRoom returns a map of room ID to a list of all joined user IDs for a given room. + AllJoinedUsersInRoom(ctx context.Context, roomIDs []string) (map[string][]string, error) + // AllPeekingDevicesInRooms returns a map of room ID to a list of all peeking devices. AllPeekingDevicesInRooms(ctx context.Context) (map[string][]types.PeekingDevice, error) // Events lookups a list of event by their event ID. @@ -80,7 +84,7 @@ type Database interface { // Returns a map following the format data[roomID] = []dataTypes // If no data is retrieved, returns an empty map // If there was an issue with the retrieval, returns an error - GetAccountDataInRange(ctx context.Context, userID string, r types.Range, accountDataFilterPart *gomatrixserverlib.EventFilter) (map[string][]string, error) + GetAccountDataInRange(ctx context.Context, userID string, r types.Range, accountDataFilterPart *gomatrixserverlib.EventFilter) (map[string][]string, types.StreamPosition, error) // UpsertAccountData keeps track of new or updated account data, by saving the type // of the new/updated data, and the user ID and room ID the data is related to (empty) // room ID means the data isn't specific to any room) @@ -124,10 +128,10 @@ type Database interface { // CleanSendToDeviceUpdates removes all send-to-device messages BEFORE the specified // from position, preventing the send-to-device table from growing indefinitely. CleanSendToDeviceUpdates(ctx context.Context, userID, deviceID string, before types.StreamPosition) (err error) - // GetFilter looks up the filter associated with a given local user and filter ID. - // Returns a filter structure. Otherwise returns an error if no such filter exists + // GetFilter looks up the filter associated with a given local user and filter ID + // and populates the target filter. Otherwise returns an error if no such filter exists // or if there was an error talking to the database. - GetFilter(ctx context.Context, localpart string, filterID string) (*gomatrixserverlib.Filter, error) + GetFilter(ctx context.Context, target *gomatrixserverlib.Filter, localpart string, filterID string) error // PutFilter puts the passed filter into the database. // Returns the filterID as a string. Otherwise returns an error if something // goes wrong. @@ -158,6 +162,6 @@ type Database interface { type Presence interface { UpdatePresence(ctx context.Context, userID string, presence types.Presence, statusMsg *string, lastActiveTS gomatrixserverlib.Timestamp, fromSync bool) (types.StreamPosition, error) GetPresence(ctx context.Context, userID string) (*types.PresenceInternal, error) - PresenceAfter(ctx context.Context, after types.StreamPosition) (map[string]*types.PresenceInternal, error) + PresenceAfter(ctx context.Context, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (map[string]*types.PresenceInternal, error) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) } diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index 25bdb1da3..e9c72058b 100644 --- a/syncapi/storage/postgres/account_data_table.go +++ b/syncapi/storage/postgres/account_data_table.go @@ -57,7 +57,7 @@ const insertAccountDataSQL = "" + " RETURNING id" const selectAccountDataInRangeSQL = "" + - "SELECT room_id, type FROM syncapi_account_data_type" + + "SELECT id, room_id, type FROM syncapi_account_data_type" + " WHERE user_id = $1 AND id > $2 AND id <= $3" + " AND ( $4::text[] IS NULL OR type LIKE ANY($4) )" + " AND ( $5::text[] IS NULL OR NOT(type LIKE ANY($5)) )" + @@ -103,7 +103,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( userID string, r types.Range, accountDataEventFilter *gomatrixserverlib.EventFilter, -) (data map[string][]string, err error) { +) (data map[string][]string, pos types.StreamPosition, err error) { data = make(map[string][]string) rows, err := s.selectAccountDataInRangeStmt.QueryContext(ctx, userID, r.Low(), r.High(), @@ -116,11 +116,12 @@ func (s *accountDataStatements) SelectAccountDataInRange( } defer internal.CloseAndLogIfError(ctx, rows, "selectAccountDataInRange: rows.close() failed") - for rows.Next() { - var dataType string - var roomID string + var dataType string + var roomID string + var id types.StreamPosition - if err = rows.Scan(&roomID, &dataType); err != nil { + for rows.Next() { + if err = rows.Scan(&id, &roomID, &dataType); err != nil { return } @@ -129,8 +130,14 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } + if id > pos { + pos = id + } } - return data, rows.Err() + if pos == 0 { + pos = r.High() + } + return data, pos, rows.Err() } func (s *accountDataStatements) SelectMaxAccountDataID( diff --git a/syncapi/storage/postgres/current_room_state_table.go b/syncapi/storage/postgres/current_room_state_table.go index fe68788d1..8ee387b39 100644 --- a/syncapi/storage/postgres/current_room_state_table.go +++ b/syncapi/storage/postgres/current_room_state_table.go @@ -93,6 +93,9 @@ const selectCurrentStateSQL = "" + const selectJoinedUsersSQL = "" + "SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join'" +const selectJoinedUsersInRoomSQL = "" + + "SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join' AND room_id = ANY($1)" + const selectStateEventSQL = "" + "SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1 AND type = $2 AND state_key = $3" @@ -112,6 +115,7 @@ type currentRoomStateStatements struct { selectRoomIDsWithAnyMembershipStmt *sql.Stmt selectCurrentStateStmt *sql.Stmt selectJoinedUsersStmt *sql.Stmt + selectJoinedUsersInRoomStmt *sql.Stmt selectEventsWithEventIDsStmt *sql.Stmt selectStateEventStmt *sql.Stmt } @@ -143,6 +147,9 @@ func NewPostgresCurrentRoomStateTable(db *sql.DB) (tables.CurrentRoomState, erro if s.selectJoinedUsersStmt, err = db.Prepare(selectJoinedUsersSQL); err != nil { return nil, err } + if s.selectJoinedUsersInRoomStmt, err = db.Prepare(selectJoinedUsersInRoomSQL); err != nil { + return nil, err + } if s.selectEventsWithEventIDsStmt, err = db.Prepare(selectEventsWithEventIDsSQL); err != nil { return nil, err } @@ -163,9 +170,32 @@ func (s *currentRoomStateStatements) SelectJoinedUsers( defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsers: rows.close() failed") result := make(map[string][]string) + var roomID string + var userID string + for rows.Next() { + if err := rows.Scan(&roomID, &userID); err != nil { + return nil, err + } + users := result[roomID] + users = append(users, userID) + result[roomID] = users + } + return result, rows.Err() +} + +// SelectJoinedUsersInRoom returns a map of room ID to a list of joined user IDs for a given room. +func (s *currentRoomStateStatements) SelectJoinedUsersInRoom( + ctx context.Context, roomIDs []string, +) (map[string][]string, error) { + rows, err := s.selectJoinedUsersInRoomStmt.QueryContext(ctx, pq.StringArray(roomIDs)) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsers: rows.close() failed") + + result := make(map[string][]string) + var userID, roomID string for rows.Next() { - var roomID string - var userID string if err := rows.Scan(&roomID, &userID); err != nil { return nil, err } diff --git a/syncapi/storage/postgres/filter_table.go b/syncapi/storage/postgres/filter_table.go index dfd3d6963..c82ef092f 100644 --- a/syncapi/storage/postgres/filter_table.go +++ b/syncapi/storage/postgres/filter_table.go @@ -73,21 +73,20 @@ func NewPostgresFilterTable(db *sql.DB) (tables.Filter, error) { } func (s *filterStatements) SelectFilter( - ctx context.Context, localpart string, filterID string, -) (*gomatrixserverlib.Filter, error) { + ctx context.Context, target *gomatrixserverlib.Filter, localpart string, filterID string, +) error { // Retrieve filter from database (stored as canonical JSON) var filterData []byte err := s.selectFilterStmt.QueryRowContext(ctx, localpart, filterID).Scan(&filterData) if err != nil { - return nil, err + return err } // Unmarshal JSON into Filter struct - filter := gomatrixserverlib.DefaultFilter() - if err = json.Unmarshal(filterData, &filter); err != nil { - return nil, err + if err = json.Unmarshal(filterData, &target); err != nil { + return err } - return &filter, nil + return nil } func (s *filterStatements) InsertFilter( diff --git a/syncapi/storage/postgres/memberships_table.go b/syncapi/storage/postgres/memberships_table.go index 39fa656cb..00223c57a 100644 --- a/syncapi/storage/postgres/memberships_table.go +++ b/syncapi/storage/postgres/memberships_table.go @@ -19,6 +19,8 @@ import ( "database/sql" "fmt" + "github.com/lib/pq" + "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/syncapi/storage/tables" "github.com/matrix-org/dendrite/syncapi/types" @@ -61,9 +63,13 @@ const selectMembershipCountSQL = "" + " SELECT DISTINCT ON (room_id, user_id) room_id, user_id, membership FROM syncapi_memberships WHERE room_id = $1 AND stream_pos <= $2 ORDER BY room_id, user_id, stream_pos DESC" + ") t WHERE t.membership = $3" +const selectHeroesSQL = "" + + "SELECT DISTINCT user_id FROM syncapi_memberships WHERE room_id = $1 AND user_id != $2 AND membership = ANY($3) LIMIT 5" + type membershipsStatements struct { upsertMembershipStmt *sql.Stmt selectMembershipCountStmt *sql.Stmt + selectHeroesStmt *sql.Stmt } func NewPostgresMembershipsTable(db *sql.DB) (tables.Memberships, error) { @@ -72,13 +78,11 @@ func NewPostgresMembershipsTable(db *sql.DB) (tables.Memberships, error) { if err != nil { return nil, err } - if s.upsertMembershipStmt, err = db.Prepare(upsertMembershipSQL); err != nil { - return nil, err - } - if s.selectMembershipCountStmt, err = db.Prepare(selectMembershipCountSQL); err != nil { - return nil, err - } - return s, nil + return s, sqlutil.StatementList{ + {&s.upsertMembershipStmt, upsertMembershipSQL}, + {&s.selectMembershipCountStmt, selectMembershipCountSQL}, + {&s.selectHeroesStmt, selectHeroesSQL}, + }.Prepare(db) } func (s *membershipsStatements) UpsertMembership( @@ -108,3 +112,23 @@ func (s *membershipsStatements) SelectMembershipCount( err = stmt.QueryRowContext(ctx, roomID, pos, membership).Scan(&count) return } + +func (s *membershipsStatements) SelectHeroes( + ctx context.Context, txn *sql.Tx, roomID, userID string, memberships []string, +) (heroes []string, err error) { + stmt := sqlutil.TxStmt(txn, s.selectHeroesStmt) + var rows *sql.Rows + rows, err = stmt.QueryContext(ctx, roomID, userID, pq.StringArray(memberships)) + if err != nil { + return + } + defer internal.CloseAndLogIfError(ctx, rows, "SelectHeroes: rows.close() failed") + var hero string + for rows.Next() { + if err = rows.Scan(&hero); err != nil { + return + } + heroes = append(heroes, hero) + } + return heroes, rows.Err() +} diff --git a/syncapi/storage/postgres/output_room_events_table.go b/syncapi/storage/postgres/output_room_events_table.go index 17e2feab6..d84d0cfa2 100644 --- a/syncapi/storage/postgres/output_room_events_table.go +++ b/syncapi/storage/postgres/output_room_events_table.go @@ -69,6 +69,11 @@ CREATE TABLE IF NOT EXISTS syncapi_output_room_events ( -- were emitted. exclude_from_sync BOOL DEFAULT FALSE ); + +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_type_idx ON syncapi_output_room_events (type); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync); ` const insertEventSQL = "" + diff --git a/syncapi/storage/postgres/presence_table.go b/syncapi/storage/postgres/presence_table.go index 49336c4eb..7194afea6 100644 --- a/syncapi/storage/postgres/presence_table.go +++ b/syncapi/storage/postgres/presence_table.go @@ -17,6 +17,7 @@ package postgres import ( "context" "database/sql" + "time" "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" @@ -72,7 +73,8 @@ const selectMaxPresenceSQL = "" + const selectPresenceAfter = "" + " SELECT id, user_id, presence, status_msg, last_active_ts" + " FROM syncapi_presence" + - " WHERE id > $1" + " WHERE id > $1 AND last_active_ts >= $2" + + " ORDER BY id ASC LIMIT $3" type presenceStatements struct { upsertPresenceStmt *sql.Stmt @@ -127,6 +129,9 @@ func (p *presenceStatements) GetPresenceForUser( } stmt := sqlutil.TxStmt(txn, p.selectPresenceForUsersStmt) err := stmt.QueryRowContext(ctx, userID).Scan(&result.Presence, &result.ClientFields.StatusMsg, &result.LastActiveTS) + if err == sql.ErrNoRows { + return nil, nil + } result.ClientFields.Presence = result.Presence.String() return result, err } @@ -141,11 +146,12 @@ func (p *presenceStatements) GetMaxPresenceID(ctx context.Context, txn *sql.Tx) func (p *presenceStatements) GetPresenceAfter( ctx context.Context, txn *sql.Tx, after types.StreamPosition, + filter gomatrixserverlib.EventFilter, ) (presences map[string]*types.PresenceInternal, err error) { presences = make(map[string]*types.PresenceInternal) stmt := sqlutil.TxStmt(txn, p.selectPresenceAfterStmt) - - rows, err := stmt.QueryContext(ctx, after) + afterTS := gomatrixserverlib.AsTimestamp(time.Now().Add(time.Minute * -5)) + rows, err := stmt.QueryContext(ctx, after, afterTS, filter.Limit) if err != nil { return nil, err } diff --git a/syncapi/storage/postgres/syncserver.go b/syncapi/storage/postgres/syncserver.go index 914d336d3..a044716ce 100644 --- a/syncapi/storage/postgres/syncserver.go +++ b/syncapi/storage/postgres/syncserver.go @@ -21,6 +21,7 @@ import ( // Import the postgres database driver. _ "github.com/lib/pq" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/syncapi/storage/shared" ) @@ -34,13 +35,12 @@ type SyncServerDatasource struct { } // NewDatabase creates a new sync server database -func NewDatabase(dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) { var d SyncServerDatasource var err error - if d.db, err = sqlutil.Open(dbProperties); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()); err != nil { return nil, err } - d.writer = sqlutil.NewDummyWriter() accountData, err := NewPostgresAccountDataTable(d.db) if err != nil { return nil, err diff --git a/syncapi/storage/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index 2143fd672..ec5edd355 100644 --- a/syncapi/storage/shared/syncserver.go +++ b/syncapi/storage/shared/syncserver.go @@ -124,6 +124,10 @@ func (d *Database) MembershipCount(ctx context.Context, roomID, membership strin return d.Memberships.SelectMembershipCount(ctx, nil, roomID, membership, pos) } +func (d *Database) GetRoomHeroes(ctx context.Context, roomID, userID string, memberships []string) ([]string, error) { + return d.Memberships.SelectHeroes(ctx, nil, roomID, userID, memberships) +} + func (d *Database) RecentEvents(ctx context.Context, roomID string, r types.Range, eventFilter *gomatrixserverlib.RoomEventFilter, chronologicalOrder bool, onlySyncEvents bool) ([]types.StreamEvent, bool, error) { return d.OutputEvents.SelectRecentEvents(ctx, nil, roomID, r, eventFilter, chronologicalOrder, onlySyncEvents) } @@ -164,6 +168,10 @@ func (d *Database) AllJoinedUsersInRooms(ctx context.Context) (map[string][]stri return d.CurrentRoomState.SelectJoinedUsers(ctx) } +func (d *Database) AllJoinedUsersInRoom(ctx context.Context, roomIDs []string) (map[string][]string, error) { + return d.CurrentRoomState.SelectJoinedUsersInRoom(ctx, roomIDs) +} + func (d *Database) AllPeekingDevicesInRooms(ctx context.Context) (map[string][]types.PeekingDevice, error) { return d.Peeks.SelectPeekingDevices(ctx) } @@ -261,7 +269,7 @@ func (d *Database) DeletePeeks( func (d *Database) GetAccountDataInRange( ctx context.Context, userID string, r types.Range, accountDataFilterPart *gomatrixserverlib.EventFilter, -) (map[string][]string, error) { +) (map[string][]string, types.StreamPosition, error) { return d.AccountData.SelectAccountDataInRange(ctx, userID, r, accountDataFilterPart) } @@ -509,9 +517,9 @@ func (d *Database) StreamToTopologicalPosition( } func (d *Database) GetFilter( - ctx context.Context, localpart string, filterID string, -) (*gomatrixserverlib.Filter, error) { - return d.Filter.SelectFilter(ctx, localpart, filterID) + ctx context.Context, target *gomatrixserverlib.Filter, localpart string, filterID string, +) error { + return d.Filter.SelectFilter(ctx, target, localpart, filterID) } func (d *Database) PutFilter( @@ -1052,8 +1060,8 @@ func (s *Database) GetPresence(ctx context.Context, userID string) (*types.Prese return s.Presence.GetPresenceForUser(ctx, nil, userID) } -func (s *Database) PresenceAfter(ctx context.Context, after types.StreamPosition) (map[string]*types.PresenceInternal, error) { - return s.Presence.GetPresenceAfter(ctx, nil, after) +func (s *Database) PresenceAfter(ctx context.Context, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (map[string]*types.PresenceInternal, error) { + return s.Presence.GetPresenceAfter(ctx, nil, after, filter) } func (s *Database) MaxStreamPositionForPresence(ctx context.Context) (types.StreamPosition, error) { diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index 71a098177..21a16dcd3 100644 --- a/syncapi/storage/sqlite3/account_data_table.go +++ b/syncapi/storage/sqlite3/account_data_table.go @@ -43,7 +43,7 @@ const insertAccountDataSQL = "" + // further parameters are added by prepareWithFilters const selectAccountDataInRangeSQL = "" + - "SELECT room_id, type FROM syncapi_account_data_type" + + "SELECT id, room_id, type FROM syncapi_account_data_type" + " WHERE user_id = $1 AND id > $2 AND id <= $3" const selectMaxAccountDataIDSQL = "" + @@ -95,7 +95,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( userID string, r types.Range, filter *gomatrixserverlib.EventFilter, -) (data map[string][]string, err error) { +) (data map[string][]string, pos types.StreamPosition, err error) { data = make(map[string][]string) stmt, params, err := prepareWithFilters( s.db, nil, selectAccountDataInRangeSQL, @@ -112,11 +112,12 @@ func (s *accountDataStatements) SelectAccountDataInRange( } defer internal.CloseAndLogIfError(ctx, rows, "selectAccountDataInRange: rows.close() failed") - for rows.Next() { - var dataType string - var roomID string + var dataType string + var roomID string + var id types.StreamPosition - if err = rows.Scan(&roomID, &dataType); err != nil { + for rows.Next() { + if err = rows.Scan(&id, &roomID, &dataType); err != nil { return } @@ -125,9 +126,14 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } + if id > pos { + pos = id + } } - - return data, nil + if pos == 0 { + pos = r.High() + } + return data, pos, nil } func (s *accountDataStatements) SelectMaxAccountDataID( diff --git a/syncapi/storage/sqlite3/current_room_state_table.go b/syncapi/storage/sqlite3/current_room_state_table.go index ccda005c1..f0a1c7bb7 100644 --- a/syncapi/storage/sqlite3/current_room_state_table.go +++ b/syncapi/storage/sqlite3/current_room_state_table.go @@ -77,6 +77,9 @@ const selectCurrentStateSQL = "" + const selectJoinedUsersSQL = "" + "SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join'" +const selectJoinedUsersInRoomSQL = "" + + "SELECT room_id, state_key FROM syncapi_current_room_state WHERE type = 'm.room.member' AND membership = 'join' AND room_id IN ($1)" + const selectStateEventSQL = "" + "SELECT headered_event_json FROM syncapi_current_room_state WHERE room_id = $1 AND type = $2 AND state_key = $3" @@ -97,7 +100,8 @@ type currentRoomStateStatements struct { selectRoomIDsWithMembershipStmt *sql.Stmt selectRoomIDsWithAnyMembershipStmt *sql.Stmt selectJoinedUsersStmt *sql.Stmt - selectStateEventStmt *sql.Stmt + //selectJoinedUsersInRoomStmt *sql.Stmt - prepared at runtime due to variadic + selectStateEventStmt *sql.Stmt } func NewSqliteCurrentRoomStateTable(db *sql.DB, streamID *StreamIDStatements) (tables.CurrentRoomState, error) { @@ -127,13 +131,16 @@ func NewSqliteCurrentRoomStateTable(db *sql.DB, streamID *StreamIDStatements) (t if s.selectJoinedUsersStmt, err = db.Prepare(selectJoinedUsersSQL); err != nil { return nil, err } + //if s.selectJoinedUsersInRoomStmt, err = db.Prepare(selectJoinedUsersInRoomSQL); err != nil { + // return nil, err + //} if s.selectStateEventStmt, err = db.Prepare(selectStateEventSQL); err != nil { return nil, err } return s, nil } -// JoinedMemberLists returns a map of room ID to a list of joined user IDs. +// SelectJoinedUsers returns a map of room ID to a list of joined user IDs. func (s *currentRoomStateStatements) SelectJoinedUsers( ctx context.Context, ) (map[string][]string, error) { @@ -144,9 +151,9 @@ func (s *currentRoomStateStatements) SelectJoinedUsers( defer internal.CloseAndLogIfError(ctx, rows, "selectJoinedUsers: rows.close() failed") result := make(map[string][]string) + var roomID string + var userID string for rows.Next() { - var roomID string - var userID string if err := rows.Scan(&roomID, &userID); err != nil { return nil, err } @@ -157,6 +164,40 @@ func (s *currentRoomStateStatements) SelectJoinedUsers( return result, nil } +// SelectJoinedUsersInRoom returns a map of room ID to a list of joined user IDs for a given room. +func (s *currentRoomStateStatements) SelectJoinedUsersInRoom( + ctx context.Context, roomIDs []string, +) (map[string][]string, error) { + query := strings.Replace(selectJoinedUsersInRoomSQL, "($1)", sqlutil.QueryVariadic(len(roomIDs)), 1) + params := make([]interface{}, 0, len(roomIDs)) + for _, roomID := range roomIDs { + params = append(params, roomID) + } + stmt, err := s.db.Prepare(query) + if err != nil { + return nil, fmt.Errorf("SelectJoinedUsersInRoom s.db.Prepare: %w", err) + } + defer internal.CloseAndLogIfError(ctx, stmt, "SelectJoinedUsersInRoom: stmt.close() failed") + + rows, err := stmt.QueryContext(ctx, params...) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "SelectJoinedUsersInRoom: rows.close() failed") + + result := make(map[string][]string) + var userID, roomID string + for rows.Next() { + if err := rows.Scan(&roomID, &userID); err != nil { + return nil, err + } + users := result[roomID] + users = append(users, userID) + result[roomID] = users + } + return result, rows.Err() +} + // SelectRoomIDsWithMembership returns the list of room IDs which have the given user in the given membership state. func (s *currentRoomStateStatements) SelectRoomIDsWithMembership( ctx context.Context, diff --git a/syncapi/storage/sqlite3/filter_table.go b/syncapi/storage/sqlite3/filter_table.go index 0cfebef2a..6081a48b1 100644 --- a/syncapi/storage/sqlite3/filter_table.go +++ b/syncapi/storage/sqlite3/filter_table.go @@ -77,21 +77,20 @@ func NewSqliteFilterTable(db *sql.DB) (tables.Filter, error) { } func (s *filterStatements) SelectFilter( - ctx context.Context, localpart string, filterID string, -) (*gomatrixserverlib.Filter, error) { + ctx context.Context, target *gomatrixserverlib.Filter, localpart string, filterID string, +) error { // Retrieve filter from database (stored as canonical JSON) var filterData []byte err := s.selectFilterStmt.QueryRowContext(ctx, localpart, filterID).Scan(&filterData) if err != nil { - return nil, err + return err } // Unmarshal JSON into Filter struct - filter := gomatrixserverlib.DefaultFilter() - if err = json.Unmarshal(filterData, &filter); err != nil { - return nil, err + if err = json.Unmarshal(filterData, &target); err != nil { + return err } - return &filter, nil + return nil } func (s *filterStatements) InsertFilter( diff --git a/syncapi/storage/sqlite3/memberships_table.go b/syncapi/storage/sqlite3/memberships_table.go index 9f3530ccd..e4daa99c1 100644 --- a/syncapi/storage/sqlite3/memberships_table.go +++ b/syncapi/storage/sqlite3/memberships_table.go @@ -18,7 +18,9 @@ import ( "context" "database/sql" "fmt" + "strings" + "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/syncapi/storage/tables" "github.com/matrix-org/dendrite/syncapi/types" @@ -61,10 +63,14 @@ const selectMembershipCountSQL = "" + " SELECT * FROM syncapi_memberships WHERE room_id = $1 AND stream_pos <= $2 GROUP BY user_id HAVING(max(stream_pos))" + ") t WHERE t.membership = $3" +const selectHeroesSQL = "" + + "SELECT DISTINCT user_id FROM syncapi_memberships WHERE room_id = $1 AND user_id != $2 AND membership IN ($3) LIMIT 5" + type membershipsStatements struct { db *sql.DB upsertMembershipStmt *sql.Stmt selectMembershipCountStmt *sql.Stmt + //selectHeroesStmt *sql.Stmt - prepared at runtime due to variadic } func NewSqliteMembershipsTable(db *sql.DB) (tables.Memberships, error) { @@ -75,13 +81,11 @@ func NewSqliteMembershipsTable(db *sql.DB) (tables.Memberships, error) { if err != nil { return nil, err } - if s.upsertMembershipStmt, err = db.Prepare(upsertMembershipSQL); err != nil { - return nil, err - } - if s.selectMembershipCountStmt, err = db.Prepare(selectMembershipCountSQL); err != nil { - return nil, err - } - return s, nil + return s, sqlutil.StatementList{ + {&s.upsertMembershipStmt, upsertMembershipSQL}, + {&s.selectMembershipCountStmt, selectMembershipCountSQL}, + // {&s.selectHeroesStmt, selectHeroesSQL}, - prepared at runtime due to variadic + }.Prepare(db) } func (s *membershipsStatements) UpsertMembership( @@ -111,3 +115,36 @@ func (s *membershipsStatements) SelectMembershipCount( err = stmt.QueryRowContext(ctx, roomID, pos, membership).Scan(&count) return } + +func (s *membershipsStatements) SelectHeroes( + ctx context.Context, txn *sql.Tx, roomID, userID string, memberships []string, +) (heroes []string, err error) { + stmtSQL := strings.Replace(selectHeroesSQL, "($3)", sqlutil.QueryVariadicOffset(len(memberships), 2), 1) + stmt, err := s.db.PrepareContext(ctx, stmtSQL) + if err != nil { + return + } + defer internal.CloseAndLogIfError(ctx, stmt, "SelectHeroes: stmt.close() failed") + params := []interface{}{ + roomID, userID, + } + for _, membership := range memberships { + params = append(params, membership) + } + + stmt = sqlutil.TxStmt(txn, stmt) + var rows *sql.Rows + rows, err = stmt.QueryContext(ctx, params...) + if err != nil { + return + } + defer internal.CloseAndLogIfError(ctx, rows, "SelectHeroes: rows.close() failed") + var hero string + for rows.Next() { + if err = rows.Scan(&hero); err != nil { + return + } + heroes = append(heroes, hero) + } + return heroes, rows.Err() +} diff --git a/syncapi/storage/sqlite3/output_room_events_table.go b/syncapi/storage/sqlite3/output_room_events_table.go index 188f7582b..f9961a9d1 100644 --- a/syncapi/storage/sqlite3/output_room_events_table.go +++ b/syncapi/storage/sqlite3/output_room_events_table.go @@ -49,6 +49,11 @@ CREATE TABLE IF NOT EXISTS syncapi_output_room_events ( transaction_id TEXT, exclude_from_sync BOOL NOT NULL DEFAULT FALSE ); + +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_type_idx ON syncapi_output_room_events (type); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_sender_idx ON syncapi_output_room_events (sender); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_room_id_idx ON syncapi_output_room_events (room_id); +CREATE INDEX IF NOT EXISTS syncapi_output_room_events_exclude_from_sync_idx ON syncapi_output_room_events (exclude_from_sync); ` const insertEventSQL = "" + diff --git a/syncapi/storage/sqlite3/presence_table.go b/syncapi/storage/sqlite3/presence_table.go index 00b16458d..b61a825df 100644 --- a/syncapi/storage/sqlite3/presence_table.go +++ b/syncapi/storage/sqlite3/presence_table.go @@ -17,6 +17,7 @@ package sqlite3 import ( "context" "database/sql" + "time" "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" @@ -71,7 +72,8 @@ const selectMaxPresenceSQL = "" + const selectPresenceAfter = "" + " SELECT id, user_id, presence, status_msg, last_active_ts" + " FROM syncapi_presence" + - " WHERE id > $1" + " WHERE id > $1 AND last_active_ts >= $2" + + " ORDER BY id ASC LIMIT $3" type presenceStatements struct { db *sql.DB @@ -142,6 +144,9 @@ func (p *presenceStatements) GetPresenceForUser( } stmt := sqlutil.TxStmt(txn, p.selectPresenceForUsersStmt) err := stmt.QueryRowContext(ctx, userID).Scan(&result.Presence, &result.ClientFields.StatusMsg, &result.LastActiveTS) + if err == sql.ErrNoRows { + return nil, nil + } result.ClientFields.Presence = result.Presence.String() return result, err } @@ -155,12 +160,12 @@ func (p *presenceStatements) GetMaxPresenceID(ctx context.Context, txn *sql.Tx) // GetPresenceAfter returns the changes presences after a given stream id func (p *presenceStatements) GetPresenceAfter( ctx context.Context, txn *sql.Tx, - after types.StreamPosition, + after types.StreamPosition, filter gomatrixserverlib.EventFilter, ) (presences map[string]*types.PresenceInternal, err error) { presences = make(map[string]*types.PresenceInternal) stmt := sqlutil.TxStmt(txn, p.selectPresenceAfterStmt) - - rows, err := stmt.QueryContext(ctx, after) + afterTS := gomatrixserverlib.AsTimestamp(time.Now().Add(time.Minute * -5)) + rows, err := stmt.QueryContext(ctx, after, afterTS, filter.Limit) if err != nil { return nil, err } diff --git a/syncapi/storage/sqlite3/syncserver.go b/syncapi/storage/sqlite3/syncserver.go index 196c1f036..65b2bb38a 100644 --- a/syncapi/storage/sqlite3/syncserver.go +++ b/syncapi/storage/sqlite3/syncserver.go @@ -19,6 +19,7 @@ import ( "database/sql" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/syncapi/storage/shared" ) @@ -34,13 +35,12 @@ type SyncServerDatasource struct { // NewDatabase creates a new sync server database // nolint: gocyclo -func NewDatabase(dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) { +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (*SyncServerDatasource, error) { var d SyncServerDatasource var err error - if d.db, err = sqlutil.Open(dbProperties); err != nil { + if d.db, d.writer, err = base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()); err != nil { return nil, err } - d.writer = sqlutil.NewExclusiveWriter() if err = d.prepare(); err != nil { return nil, err } diff --git a/syncapi/storage/storage.go b/syncapi/storage/storage.go index 7f9c28e9d..5b20c6cc2 100644 --- a/syncapi/storage/storage.go +++ b/syncapi/storage/storage.go @@ -20,18 +20,19 @@ package storage import ( "fmt" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/syncapi/storage/postgres" "github.com/matrix-org/dendrite/syncapi/storage/sqlite3" ) // NewSyncServerDatasource opens a database connection. -func NewSyncServerDatasource(dbProperties *config.DatabaseOptions) (Database, error) { +func NewSyncServerDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties) + return postgres.NewDatabase(base, dbProperties) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/syncapi/storage/storage_test.go b/syncapi/storage/storage_test.go index 15bb769a2..563c92e34 100644 --- a/syncapi/storage/storage_test.go +++ b/syncapi/storage/storage_test.go @@ -17,7 +17,7 @@ var ctx = context.Background() func MustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) { connStr, close := test.PrepareDBConnectionString(t, dbType) - db, err := storage.NewSyncServerDatasource(&config.DatabaseOptions{ + db, err := storage.NewSyncServerDatasource(nil, &config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), }) if err != nil { @@ -47,7 +47,7 @@ func MustWriteEvents(t *testing.T, db storage.Database, events []*gomatrixserver func TestWriteEvents(t *testing.T) { test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { - alice := test.NewUser() + alice := test.NewUser(t) r := test.NewRoom(t, alice) db, close := MustCreateDatabase(t, dbType) defer close() @@ -60,7 +60,7 @@ func TestRecentEventsPDU(t *testing.T) { test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { db, close := MustCreateDatabase(t, dbType) defer close() - alice := test.NewUser() + alice := test.NewUser(t) // dummy room to make sure SQL queries are filtering on room ID MustWriteEvents(t, db, test.NewRoom(t, alice).Events()) @@ -163,7 +163,7 @@ func TestGetEventsInRangeWithTopologyToken(t *testing.T) { test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { db, close := MustCreateDatabase(t, dbType) defer close() - alice := test.NewUser() + alice := test.NewUser(t) r := test.NewRoom(t, alice) for i := 0; i < 10; i++ { r.CreateAndInsert(t, alice, "m.room.message", map[string]interface{}{"body": fmt.Sprintf("hi %d", i)}) diff --git a/syncapi/storage/storage_wasm.go b/syncapi/storage/storage_wasm.go index f7fef962b..c15444743 100644 --- a/syncapi/storage/storage_wasm.go +++ b/syncapi/storage/storage_wasm.go @@ -17,15 +17,16 @@ package storage import ( "fmt" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/syncapi/storage/sqlite3" ) // NewPublicRoomsServerDatabase opens a database connection. -func NewSyncServerDatasource(dbProperties *config.DatabaseOptions) (Database, error) { +func NewSyncServerDatasource(base *base.BaseDendrite, dbProperties *config.DatabaseOptions) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties) + return sqlite3.NewDatabase(base, dbProperties) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/syncapi/storage/tables/interface.go b/syncapi/storage/tables/interface.go index 993e2022b..ccdebfdbd 100644 --- a/syncapi/storage/tables/interface.go +++ b/syncapi/storage/tables/interface.go @@ -27,7 +27,7 @@ import ( type AccountData interface { InsertAccountData(ctx context.Context, txn *sql.Tx, userID, roomID, dataType string) (pos types.StreamPosition, err error) // SelectAccountDataInRange returns a map of room ID to a list of `dataType`. - SelectAccountDataInRange(ctx context.Context, userID string, r types.Range, accountDataEventFilter *gomatrixserverlib.EventFilter) (data map[string][]string, err error) + SelectAccountDataInRange(ctx context.Context, userID string, r types.Range, accountDataEventFilter *gomatrixserverlib.EventFilter) (data map[string][]string, pos types.StreamPosition, err error) SelectMaxAccountDataID(ctx context.Context, txn *sql.Tx) (id int64, err error) } @@ -102,6 +102,8 @@ type CurrentRoomState interface { SelectRoomIDsWithAnyMembership(ctx context.Context, txn *sql.Tx, userID string) (map[string]string, error) // SelectJoinedUsers returns a map of room ID to a list of joined user IDs. SelectJoinedUsers(ctx context.Context) (map[string][]string, error) + // SelectJoinedUsersInRoom returns a map of room ID to a list of joined user IDs for a given room. + SelectJoinedUsersInRoom(ctx context.Context, roomIDs []string) (map[string][]string, error) } // BackwardsExtremities keeps track of backwards extremities for a room. @@ -157,7 +159,7 @@ type SendToDevice interface { } type Filter interface { - SelectFilter(ctx context.Context, localpart string, filterID string) (*gomatrixserverlib.Filter, error) + SelectFilter(ctx context.Context, target *gomatrixserverlib.Filter, localpart string, filterID string) error InsertFilter(ctx context.Context, filter *gomatrixserverlib.Filter, localpart string) (filterID string, err error) } @@ -170,6 +172,7 @@ type Receipts interface { type Memberships interface { UpsertMembership(ctx context.Context, txn *sql.Tx, event *gomatrixserverlib.HeaderedEvent, streamPos, topologicalPos types.StreamPosition) error SelectMembershipCount(ctx context.Context, txn *sql.Tx, roomID, membership string, pos types.StreamPosition) (count int, err error) + SelectHeroes(ctx context.Context, txn *sql.Tx, roomID, userID string, memberships []string) (heroes []string, err error) } type NotificationData interface { @@ -187,5 +190,5 @@ type Presence interface { UpsertPresence(ctx context.Context, txn *sql.Tx, userID string, statusMsg *string, presence types.Presence, lastActiveTS gomatrixserverlib.Timestamp, fromSync bool) (pos types.StreamPosition, err error) GetPresenceForUser(ctx context.Context, txn *sql.Tx, userID string) (presence *types.PresenceInternal, err error) GetMaxPresenceID(ctx context.Context, txn *sql.Tx) (pos types.StreamPosition, err error) - GetPresenceAfter(ctx context.Context, txn *sql.Tx, after types.StreamPosition) (presences map[string]*types.PresenceInternal, err error) + GetPresenceAfter(ctx context.Context, txn *sql.Tx, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (presences map[string]*types.PresenceInternal, err error) } diff --git a/syncapi/storage/tables/output_room_events_test.go b/syncapi/storage/tables/output_room_events_test.go index a143e5ecd..69bbd04c9 100644 --- a/syncapi/storage/tables/output_room_events_test.go +++ b/syncapi/storage/tables/output_room_events_test.go @@ -21,7 +21,7 @@ func newOutputRoomEventsTable(t *testing.T, dbType test.DBType) (tables.Events, connStr, close := test.PrepareDBConnectionString(t, dbType) db, err := sqlutil.Open(&config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), - }) + }, sqlutil.NewExclusiveWriter()) if err != nil { t.Fatalf("failed to open db: %s", err) } @@ -45,7 +45,7 @@ func newOutputRoomEventsTable(t *testing.T, dbType test.DBType) (tables.Events, func TestOutputRoomEventsTable(t *testing.T) { ctx := context.Background() - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { tab, db, close := newOutputRoomEventsTable(t, dbType) diff --git a/syncapi/storage/tables/topology_test.go b/syncapi/storage/tables/topology_test.go index b6ece0b0d..f4f75bdf3 100644 --- a/syncapi/storage/tables/topology_test.go +++ b/syncapi/storage/tables/topology_test.go @@ -20,7 +20,7 @@ func newTopologyTable(t *testing.T, dbType test.DBType) (tables.Topology, *sql.D connStr, close := test.PrepareDBConnectionString(t, dbType) db, err := sqlutil.Open(&config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), - }) + }, sqlutil.NewExclusiveWriter()) if err != nil { t.Fatalf("failed to open db: %s", err) } @@ -40,7 +40,7 @@ func newTopologyTable(t *testing.T, dbType test.DBType) (tables.Topology, *sql.D func TestTopologyTable(t *testing.T) { ctx := context.Background() - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { tab, db, close := newTopologyTable(t, dbType) diff --git a/syncapi/streams/stream_accountdata.go b/syncapi/streams/stream_accountdata.go index 105d85260..9c19b846b 100644 --- a/syncapi/streams/stream_accountdata.go +++ b/syncapi/streams/stream_accountdata.go @@ -10,7 +10,7 @@ import ( type AccountDataStreamProvider struct { StreamProvider - userAPI userapi.UserInternalAPI + userAPI userapi.SyncUserAPI } func (p *AccountDataStreamProvider) Setup() { @@ -30,37 +30,7 @@ func (p *AccountDataStreamProvider) CompleteSync( ctx context.Context, req *types.SyncRequest, ) types.StreamPosition { - dataReq := &userapi.QueryAccountDataRequest{ - UserID: req.Device.UserID, - } - dataRes := &userapi.QueryAccountDataResponse{} - if err := p.userAPI.QueryAccountData(ctx, dataReq, dataRes); err != nil { - req.Log.WithError(err).Error("p.userAPI.QueryAccountData failed") - return p.LatestPosition(ctx) - } - for datatype, databody := range dataRes.GlobalAccountData { - req.Response.AccountData.Events = append( - req.Response.AccountData.Events, - gomatrixserverlib.ClientEvent{ - Type: datatype, - Content: gomatrixserverlib.RawJSON(databody), - }, - ) - } - for r, j := range req.Response.Rooms.Join { - for datatype, databody := range dataRes.RoomAccountData[r] { - j.AccountData.Events = append( - j.AccountData.Events, - gomatrixserverlib.ClientEvent{ - Type: datatype, - Content: gomatrixserverlib.RawJSON(databody), - }, - ) - req.Response.Rooms.Join[r] = j - } - } - - return p.LatestPosition(ctx) + return p.IncrementalSync(ctx, req, 0, p.LatestPosition(ctx)) } func (p *AccountDataStreamProvider) IncrementalSync( @@ -72,10 +42,9 @@ func (p *AccountDataStreamProvider) IncrementalSync( From: from, To: to, } - accountDataFilter := gomatrixserverlib.DefaultEventFilter() // TODO: use filter provided in req instead - dataTypes, err := p.DB.GetAccountDataInRange( - ctx, req.Device.UserID, r, &accountDataFilter, + dataTypes, pos, err := p.DB.GetAccountDataInRange( + ctx, req.Device.UserID, r, &req.Filter.AccountData, ) if err != nil { req.Log.WithError(err).Error("p.DB.GetAccountDataInRange failed") @@ -84,6 +53,12 @@ func (p *AccountDataStreamProvider) IncrementalSync( // Iterate over the rooms for roomID, dataTypes := range dataTypes { + // For a complete sync, make sure we're only including this room if + // that room was present in the joined rooms. + if from == 0 && roomID != "" && !req.IsRoomPresent(roomID) { + continue + } + // Request the missing data from the database for _, dataType := range dataTypes { dataReq := userapi.QueryAccountDataRequest{ @@ -126,5 +101,5 @@ func (p *AccountDataStreamProvider) IncrementalSync( } } - return to + return pos } diff --git a/syncapi/streams/stream_devicelist.go b/syncapi/streams/stream_devicelist.go index 6ff8a7fd5..f42099510 100644 --- a/syncapi/streams/stream_devicelist.go +++ b/syncapi/streams/stream_devicelist.go @@ -11,8 +11,8 @@ import ( type DeviceListStreamProvider struct { StreamProvider - rsAPI api.RoomserverInternalAPI - keyAPI keyapi.KeyInternalAPI + rsAPI api.SyncRoomserverAPI + keyAPI keyapi.SyncKeyAPI } func (p *DeviceListStreamProvider) CompleteSync( diff --git a/syncapi/streams/stream_pdu.go b/syncapi/streams/stream_pdu.go index df5fb8e08..00b3dfe3b 100644 --- a/syncapi/streams/stream_pdu.go +++ b/syncapi/streams/stream_pdu.go @@ -4,13 +4,16 @@ import ( "context" "database/sql" "fmt" + "sort" "sync" "time" "github.com/matrix-org/dendrite/internal/caching" + roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/syncapi/types" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" + "github.com/tidwall/gjson" "go.uber.org/atomic" ) @@ -29,7 +32,8 @@ type PDUStreamProvider struct { tasks chan func() workers atomic.Int32 // userID+deviceID -> lazy loading cache - lazyLoadCache *caching.LazyLoadCache + lazyLoadCache caching.LazyLoadCache + rsAPI roomserverAPI.SyncRoomserverAPI } func (p *PDUStreamProvider) worker() { @@ -290,16 +294,11 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse( } } - // Work out how many members are in the room. - joinedCount, _ := p.DB.MembershipCount(ctx, delta.RoomID, gomatrixserverlib.Join, latestPosition) - invitedCount, _ := p.DB.MembershipCount(ctx, delta.RoomID, gomatrixserverlib.Invite, latestPosition) - switch delta.Membership { case gomatrixserverlib.Join: jr := types.NewJoinResponse() if hasMembershipChange { - jr.Summary.JoinedMemberCount = &joinedCount - jr.Summary.InvitedMemberCount = &invitedCount + p.addRoomSummary(ctx, jr, delta.RoomID, device.UserID, latestPosition) } jr.Timeline.PrevBatch = &prevBatch jr.Timeline.Events = gomatrixserverlib.HeaderedToClientEvents(recentEvents, gomatrixserverlib.FormatSync) @@ -332,6 +331,45 @@ func (p *PDUStreamProvider) addRoomDeltaToResponse( return latestPosition, nil } +func (p *PDUStreamProvider) addRoomSummary(ctx context.Context, jr *types.JoinResponse, roomID, userID string, latestPosition types.StreamPosition) { + // Work out how many members are in the room. + joinedCount, _ := p.DB.MembershipCount(ctx, roomID, gomatrixserverlib.Join, latestPosition) + invitedCount, _ := p.DB.MembershipCount(ctx, roomID, gomatrixserverlib.Invite, latestPosition) + + jr.Summary.JoinedMemberCount = &joinedCount + jr.Summary.InvitedMemberCount = &invitedCount + + fetchStates := []gomatrixserverlib.StateKeyTuple{ + {EventType: gomatrixserverlib.MRoomName}, + {EventType: gomatrixserverlib.MRoomCanonicalAlias}, + } + // Check if the room has a name or a canonical alias + latestState := &roomserverAPI.QueryLatestEventsAndStateResponse{} + err := p.rsAPI.QueryLatestEventsAndState(ctx, &roomserverAPI.QueryLatestEventsAndStateRequest{StateToFetch: fetchStates, RoomID: roomID}, latestState) + if err != nil { + return + } + // Check if the room has a name or canonical alias, if so, return. + for _, ev := range latestState.StateEvents { + switch ev.Type() { + case gomatrixserverlib.MRoomName: + if gjson.GetBytes(ev.Content(), "name").Str != "" { + return + } + case gomatrixserverlib.MRoomCanonicalAlias: + if gjson.GetBytes(ev.Content(), "alias").Str != "" { + return + } + } + } + heroes, err := p.DB.GetRoomHeroes(ctx, roomID, userID, []string{"join", "invite"}) + if err != nil { + return + } + sort.Strings(heroes) + jr.Summary.Heroes = heroes +} + func (p *PDUStreamProvider) getJoinResponseForCompleteSync( ctx context.Context, roomID string, @@ -416,9 +454,7 @@ func (p *PDUStreamProvider) getJoinResponseForCompleteSync( prevBatch.Decrement() } - // Work out how many members are in the room. - joinedCount, _ := p.DB.MembershipCount(ctx, roomID, gomatrixserverlib.Join, r.From) - invitedCount, _ := p.DB.MembershipCount(ctx, roomID, gomatrixserverlib.Invite, r.From) + p.addRoomSummary(ctx, jr, roomID, device.UserID, r.From) // We don't include a device here as we don't need to send down // transaction IDs for complete syncs, but we do it anyway because Sytest demands it for: @@ -439,8 +475,6 @@ func (p *PDUStreamProvider) getJoinResponseForCompleteSync( } } - jr.Summary.JoinedMemberCount = &joinedCount - jr.Summary.InvitedMemberCount = &invitedCount jr.Timeline.PrevBatch = prevBatch jr.Timeline.Events = gomatrixserverlib.HeaderedToClientEvents(recentEvents, gomatrixserverlib.FormatSync) jr.Timeline.Limited = limited diff --git a/syncapi/streams/stream_presence.go b/syncapi/streams/stream_presence.go index 9a6c5c130..35ce53cb6 100644 --- a/syncapi/streams/stream_presence.go +++ b/syncapi/streams/stream_presence.go @@ -16,7 +16,6 @@ package streams import ( "context" - "database/sql" "encoding/json" "sync" @@ -54,7 +53,8 @@ func (p *PresenceStreamProvider) IncrementalSync( req *types.SyncRequest, from, to types.StreamPosition, ) types.StreamPosition { - presences, err := p.DB.PresenceAfter(ctx, from) + // We pull out a larger number than the filter asks for, since we're filtering out events later + presences, err := p.DB.PresenceAfter(ctx, from, gomatrixserverlib.EventFilter{Limit: 1000}) if err != nil { req.Log.WithError(err).Error("p.DB.PresenceAfter failed") return from @@ -67,12 +67,12 @@ func (p *PresenceStreamProvider) IncrementalSync( // add newly joined rooms user presences newlyJoined := joinedRooms(req.Response, req.Device.UserID) if len(newlyJoined) > 0 { - // TODO: This refreshes all lists and is quite expensive - // The notifier should update the lists itself - if err = p.notifier.Load(ctx, p.DB); err != nil { + // TODO: Check if this is working better than before. + if err = p.notifier.LoadRooms(ctx, p.DB, newlyJoined); err != nil { req.Log.WithError(err).Error("unable to refresh notifier lists") return from } + NewlyJoinedLoop: for _, roomID := range newlyJoined { roomUsers := p.notifier.JoinedUsers(roomID) for i := range roomUsers { @@ -80,21 +80,25 @@ func (p *PresenceStreamProvider) IncrementalSync( if _, ok := presences[roomUsers[i]]; ok { continue } + // Bear in mind that this might return nil, but at least populating + // a nil means that there's a map entry so we won't repeat this call. presences[roomUsers[i]], err = p.DB.GetPresence(ctx, roomUsers[i]) if err != nil { - if err == sql.ErrNoRows { - continue - } req.Log.WithError(err).Error("unable to query presence for user") return from } + if len(presences) > req.Filter.Presence.Limit { + break NewlyJoinedLoop + } } } } - lastPos := to - for i := range presences { - presence := presences[i] + lastPos := from + for _, presence := range presences { + if presence == nil { + continue + } // Ignore users we don't share a room with if req.Device.UserID != presence.UserID && !p.notifier.IsSharedUser(req.Device.UserID, presence.UserID) { continue @@ -135,9 +139,16 @@ func (p *PresenceStreamProvider) IncrementalSync( if presence.StreamPos > lastPos { lastPos = presence.StreamPos } + if len(req.Response.Presence.Events) == req.Filter.Presence.Limit { + break + } p.cache.Store(cacheKey, presence) } + if len(req.Response.Presence.Events) == 0 { + return to + } + return lastPos } diff --git a/syncapi/streams/stream_receipt.go b/syncapi/streams/stream_receipt.go index 9d7d479a2..f4e84c7d0 100644 --- a/syncapi/streams/stream_receipt.go +++ b/syncapi/streams/stream_receipt.go @@ -62,6 +62,12 @@ func (p *ReceiptStreamProvider) IncrementalSync( } for roomID, receipts := range receiptsByRoom { + // For a complete sync, make sure we're only including this room if + // that room was present in the joined rooms. + if from == 0 && !req.IsRoomPresent(roomID) { + continue + } + jr := *types.NewJoinResponse() if existing, ok := req.Response.Rooms.Join[roomID]; ok { jr = existing diff --git a/syncapi/streams/streams.go b/syncapi/streams/streams.go index d3195b78f..1ca4ee8c3 100644 --- a/syncapi/streams/streams.go +++ b/syncapi/streams/streams.go @@ -25,14 +25,15 @@ type Streams struct { } func NewSyncStreamProviders( - d storage.Database, userAPI userapi.UserInternalAPI, - rsAPI rsapi.RoomserverInternalAPI, keyAPI keyapi.KeyInternalAPI, - eduCache *caching.EDUCache, lazyLoadCache *caching.LazyLoadCache, notifier *notifier.Notifier, + d storage.Database, userAPI userapi.SyncUserAPI, + rsAPI rsapi.SyncRoomserverAPI, keyAPI keyapi.SyncKeyAPI, + eduCache *caching.EDUCache, lazyLoadCache caching.LazyLoadCache, notifier *notifier.Notifier, ) *Streams { streams := &Streams{ PDUStreamProvider: &PDUStreamProvider{ StreamProvider: StreamProvider{DB: d}, lazyLoadCache: lazyLoadCache, + rsAPI: rsAPI, }, TypingStreamProvider: &TypingStreamProvider{ StreamProvider: StreamProvider{DB: d}, diff --git a/syncapi/sync/request.go b/syncapi/sync/request.go index f04f172d3..9d4740e93 100644 --- a/syncapi/sync/request.go +++ b/syncapi/sync/request.go @@ -18,6 +18,7 @@ import ( "database/sql" "encoding/json" "fmt" + "math" "net/http" "strconv" "time" @@ -47,6 +48,13 @@ func newSyncRequest(req *http.Request, device userapi.Device, syncDB storage.Dat } // TODO: read from stored filters too filter := gomatrixserverlib.DefaultFilter() + if since.IsEmpty() { + // Send as much account data down for complete syncs as possible + // by default, otherwise clients do weird things while waiting + // for the rest of the data to trickle down. + filter.AccountData.Limit = math.MaxInt32 + filter.Room.AccountData.Limit = math.MaxInt32 + } filterQuery := req.URL.Query().Get("filter") if filterQuery != "" { if filterQuery[0] == '{' { @@ -61,11 +69,9 @@ func newSyncRequest(req *http.Request, device userapi.Device, syncDB storage.Dat util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed") return nil, fmt.Errorf("gomatrixserverlib.SplitID: %w", err) } - if f, err := syncDB.GetFilter(req.Context(), localpart, filterQuery); err != nil && err != sql.ErrNoRows { + if err := syncDB.GetFilter(req.Context(), &filter, localpart, filterQuery); err != nil && err != sql.ErrNoRows { util.GetLogger(req.Context()).WithError(err).Error("syncDB.GetFilter failed") return nil, fmt.Errorf("syncDB.GetFilter: %w", err) - } else if f != nil { - filter = *f } } } diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 703340997..7b9526b53 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -45,31 +45,38 @@ import ( type RequestPool struct { db storage.Database cfg *config.SyncAPI - userAPI userapi.UserInternalAPI - keyAPI keyapi.KeyInternalAPI - rsAPI roomserverAPI.RoomserverInternalAPI + userAPI userapi.SyncUserAPI + keyAPI keyapi.SyncKeyAPI + rsAPI roomserverAPI.SyncRoomserverAPI lastseen *sync.Map presence *sync.Map streams *streams.Streams Notifier *notifier.Notifier producer PresencePublisher + consumer PresenceConsumer } type PresencePublisher interface { SendPresence(userID string, presence types.Presence, statusMsg *string) error } +type PresenceConsumer interface { + EmitPresence(ctx context.Context, userID string, presence types.Presence, statusMsg *string, ts int, fromSync bool) +} + // NewRequestPool makes a new RequestPool func NewRequestPool( db storage.Database, cfg *config.SyncAPI, - userAPI userapi.UserInternalAPI, keyAPI keyapi.KeyInternalAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + userAPI userapi.SyncUserAPI, keyAPI keyapi.SyncKeyAPI, + rsAPI roomserverAPI.SyncRoomserverAPI, streams *streams.Streams, notifier *notifier.Notifier, - producer PresencePublisher, + producer PresencePublisher, consumer PresenceConsumer, enableMetrics bool, ) *RequestPool { - prometheus.MustRegister( - activeSyncRequests, waitingSyncRequests, - ) + if enableMetrics { + prometheus.MustRegister( + activeSyncRequests, waitingSyncRequests, + ) + } rp := &RequestPool{ db: db, cfg: cfg, @@ -81,6 +88,7 @@ func NewRequestPool( streams: streams, Notifier: notifier, producer: producer, + consumer: consumer, } go rp.cleanLastSeen() go rp.cleanPresence(db, time.Minute*5) @@ -127,14 +135,23 @@ func (rp *RequestPool) updatePresence(db storage.Presence, presence string, user if !ok { // this should almost never happen return } + newPresence := types.PresenceInternal{ - ClientFields: types.PresenceClientResponse{ - Presence: presenceID.String(), - }, Presence: presenceID, UserID: userID, LastActiveTS: gomatrixserverlib.AsTimestamp(time.Now()), } + + // ensure we also send the current status_msg to federated servers and not nil + dbPresence, err := db.GetPresence(context.Background(), userID) + if err != nil && err != sql.ErrNoRows { + return + } + if dbPresence != nil { + newPresence.ClientFields = dbPresence.ClientFields + } + newPresence.ClientFields.Presence = presenceID.String() + defer rp.presence.Store(userID, newPresence) // avoid spamming presence updates when syncing existingPresence, ok := rp.presence.LoadOrStore(userID, newPresence) @@ -145,16 +162,17 @@ func (rp *RequestPool) updatePresence(db storage.Presence, presence string, user } } - // ensure we also send the current status_msg to federated servers and not nil - dbPresence, err := db.GetPresence(context.Background(), userID) - if err != nil && err != sql.ErrNoRows { - return - } - - if err := rp.producer.SendPresence(userID, presenceID, dbPresence.ClientFields.StatusMsg); err != nil { + if err := rp.producer.SendPresence(userID, presenceID, newPresence.ClientFields.StatusMsg); err != nil { logrus.WithError(err).Error("Unable to publish presence message from sync") return } + + // now synchronously update our view of the world. It's critical we do this before calculating + // the /sync response else we may not return presence: online immediately. + rp.consumer.EmitPresence( + context.Background(), userID, presenceID, newPresence.ClientFields.StatusMsg, + int(gomatrixserverlib.AsTimestamp(time.Now())), true, + ) } func (rp *RequestPool) updateLastSeen(req *http.Request, device *userapi.Device) { @@ -179,6 +197,7 @@ func (rp *RequestPool) updateLastSeen(req *http.Request, device *userapi.Device) UserID: device.UserID, DeviceID: device.ID, RemoteAddr: remoteAddr, + UserAgent: req.UserAgent(), } lsres := &userapi.PerformLastSeenUpdateResponse{} go rp.userAPI.PerformLastSeenUpdate(req.Context(), lsreq, lsres) // nolint:errcheck @@ -232,114 +251,151 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi. waitingSyncRequests.Inc() defer waitingSyncRequests.Dec() - currentPos := rp.Notifier.CurrentPosition() + // loop until we get some data + for { + startTime := time.Now() + currentPos := rp.Notifier.CurrentPosition() - if !rp.shouldReturnImmediately(syncReq, currentPos) { - timer := time.NewTimer(syncReq.Timeout) // case of timeout=0 is handled above - defer timer.Stop() + // if the since token matches the current positions, wait via the notifier + if !rp.shouldReturnImmediately(syncReq, currentPos) { + timer := time.NewTimer(syncReq.Timeout) // case of timeout=0 is handled above + defer timer.Stop() - userStreamListener := rp.Notifier.GetListener(*syncReq) - defer userStreamListener.Close() + userStreamListener := rp.Notifier.GetListener(*syncReq) + defer userStreamListener.Close() - giveup := func() util.JSONResponse { - syncReq.Response.NextBatch = syncReq.Since - return util.JSONResponse{ - Code: http.StatusOK, - JSON: syncReq.Response, + giveup := func() util.JSONResponse { + syncReq.Log.Debugln("Responding to sync since client gave up or timeout was reached") + syncReq.Response.NextBatch = syncReq.Since + // We should always try to include OTKs in sync responses, otherwise clients might upload keys + // even if that's not required. See also: + // https://github.com/matrix-org/synapse/blob/29f06704b8871a44926f7c99e73cf4a978fb8e81/synapse/rest/client/sync.py#L276-L281 + // Only try to get OTKs if the context isn't already done. + if syncReq.Context.Err() == nil { + err = internal.DeviceOTKCounts(syncReq.Context, rp.keyAPI, syncReq.Device.UserID, syncReq.Device.ID, syncReq.Response) + if err != nil && err != context.Canceled { + syncReq.Log.WithError(err).Warn("failed to get OTK counts") + } + } + return util.JSONResponse{ + Code: http.StatusOK, + JSON: syncReq.Response, + } + } + + select { + case <-syncReq.Context.Done(): // Caller gave up + return giveup() + + case <-timer.C: // Timeout reached + return giveup() + + case <-userStreamListener.GetNotifyChannel(syncReq.Since): + syncReq.Log.Debugln("Responding to sync after wake-up") + currentPos.ApplyUpdates(userStreamListener.GetSyncPosition()) + } + } else { + syncReq.Log.WithField("currentPos", currentPos).Debugln("Responding to sync immediately") + } + + if syncReq.Since.IsEmpty() { + // Complete sync + syncReq.Response.NextBatch = types.StreamingToken{ + PDUPosition: rp.streams.PDUStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + TypingPosition: rp.streams.TypingStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + ReceiptPosition: rp.streams.ReceiptStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + InvitePosition: rp.streams.InviteStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + SendToDevicePosition: rp.streams.SendToDeviceStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + AccountDataPosition: rp.streams.AccountDataStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + NotificationDataPosition: rp.streams.NotificationDataStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + DeviceListPosition: rp.streams.DeviceListStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + PresencePosition: rp.streams.PresenceStreamProvider.CompleteSync( + syncReq.Context, syncReq, + ), + } + } else { + // Incremental sync + syncReq.Response.NextBatch = types.StreamingToken{ + PDUPosition: rp.streams.PDUStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.PDUPosition, currentPos.PDUPosition, + ), + TypingPosition: rp.streams.TypingStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.TypingPosition, currentPos.TypingPosition, + ), + ReceiptPosition: rp.streams.ReceiptStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.ReceiptPosition, currentPos.ReceiptPosition, + ), + InvitePosition: rp.streams.InviteStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.InvitePosition, currentPos.InvitePosition, + ), + SendToDevicePosition: rp.streams.SendToDeviceStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.SendToDevicePosition, currentPos.SendToDevicePosition, + ), + AccountDataPosition: rp.streams.AccountDataStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.AccountDataPosition, currentPos.AccountDataPosition, + ), + NotificationDataPosition: rp.streams.NotificationDataStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.NotificationDataPosition, currentPos.NotificationDataPosition, + ), + DeviceListPosition: rp.streams.DeviceListStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.DeviceListPosition, currentPos.DeviceListPosition, + ), + PresencePosition: rp.streams.PresenceStreamProvider.IncrementalSync( + syncReq.Context, syncReq, + syncReq.Since.PresencePosition, currentPos.PresencePosition, + ), + } + // it's possible for there to be no updates for this user even though since < current pos, + // e.g busy servers with a quiet user. In this scenario, we don't want to return a no-op + // response immediately, so let's try this again but pretend they bumped their since token. + // If the incremental sync was processed very quickly then we expect the next loop to block + // with a notifier, but if things are slow it's entirely possible that currentPos is no + // longer the current position so we will hit this code path again. We need to do this and + // not return a no-op response because: + // - It's an inefficient use of bandwidth. + // - Some sytests which test 'waking up' sync rely on some sync requests to block, which + // they weren't always doing, resulting in flakey tests. + if !syncReq.Response.HasUpdates() { + syncReq.Since = currentPos + // do not loop again if the ?timeout= is 0 as that means "return immediately" + if syncReq.Timeout > 0 { + syncReq.Timeout = syncReq.Timeout - time.Since(startTime) + if syncReq.Timeout < 0 { + syncReq.Timeout = 0 + } + continue + } } } - select { - case <-syncReq.Context.Done(): // Caller gave up - return giveup() - - case <-timer.C: // Timeout reached - return giveup() - - case <-userStreamListener.GetNotifyChannel(syncReq.Since): - syncReq.Log.Debugln("Responding to sync after wake-up") - currentPos.ApplyUpdates(userStreamListener.GetSyncPosition()) + return util.JSONResponse{ + Code: http.StatusOK, + JSON: syncReq.Response, } - } else { - syncReq.Log.WithField("currentPos", currentPos).Debugln("Responding to sync immediately") - } - - if syncReq.Since.IsEmpty() { - // Complete sync - syncReq.Response.NextBatch = types.StreamingToken{ - PDUPosition: rp.streams.PDUStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - TypingPosition: rp.streams.TypingStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - ReceiptPosition: rp.streams.ReceiptStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - InvitePosition: rp.streams.InviteStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - SendToDevicePosition: rp.streams.SendToDeviceStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - AccountDataPosition: rp.streams.AccountDataStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - NotificationDataPosition: rp.streams.NotificationDataStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - DeviceListPosition: rp.streams.DeviceListStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - PresencePosition: rp.streams.PresenceStreamProvider.CompleteSync( - syncReq.Context, syncReq, - ), - } - } else { - // Incremental sync - syncReq.Response.NextBatch = types.StreamingToken{ - PDUPosition: rp.streams.PDUStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.PDUPosition, currentPos.PDUPosition, - ), - TypingPosition: rp.streams.TypingStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.TypingPosition, currentPos.TypingPosition, - ), - ReceiptPosition: rp.streams.ReceiptStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.ReceiptPosition, currentPos.ReceiptPosition, - ), - InvitePosition: rp.streams.InviteStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.InvitePosition, currentPos.InvitePosition, - ), - SendToDevicePosition: rp.streams.SendToDeviceStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.SendToDevicePosition, currentPos.SendToDevicePosition, - ), - AccountDataPosition: rp.streams.AccountDataStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.AccountDataPosition, currentPos.AccountDataPosition, - ), - NotificationDataPosition: rp.streams.NotificationDataStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.NotificationDataPosition, currentPos.NotificationDataPosition, - ), - DeviceListPosition: rp.streams.DeviceListStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.DeviceListPosition, currentPos.DeviceListPosition, - ), - PresencePosition: rp.streams.PresenceStreamProvider.IncrementalSync( - syncReq.Context, syncReq, - syncReq.Since.PresencePosition, currentPos.PresencePosition, - ), - } - } - - return util.JSONResponse{ - Code: http.StatusOK, - JSON: syncReq.Response, } } diff --git a/syncapi/sync/requestpool_test.go b/syncapi/sync/requestpool_test.go index a80089945..0c7209521 100644 --- a/syncapi/sync/requestpool_test.go +++ b/syncapi/sync/requestpool_test.go @@ -30,7 +30,7 @@ func (d dummyDB) GetPresence(ctx context.Context, userID string) (*types.Presenc return &types.PresenceInternal{}, nil } -func (d dummyDB) PresenceAfter(ctx context.Context, after types.StreamPosition) (map[string]*types.PresenceInternal, error) { +func (d dummyDB) PresenceAfter(ctx context.Context, after types.StreamPosition, filter gomatrixserverlib.EventFilter) (map[string]*types.PresenceInternal, error) { return map[string]*types.PresenceInternal{}, nil } @@ -38,6 +38,12 @@ func (d dummyDB) MaxStreamPositionForPresence(ctx context.Context) (types.Stream return 0, nil } +type dummyConsumer struct{} + +func (d dummyConsumer) EmitPresence(ctx context.Context, userID string, presence types.Presence, statusMsg *string, ts int, fromSync bool) { + +} + func TestRequestPool_updatePresence(t *testing.T) { type args struct { presence string @@ -45,6 +51,7 @@ func TestRequestPool_updatePresence(t *testing.T) { sleep time.Duration } publisher := &dummyPublisher{} + consumer := &dummyConsumer{} syncMap := sync.Map{} tests := []struct { @@ -101,6 +108,7 @@ func TestRequestPool_updatePresence(t *testing.T) { rp := &RequestPool{ presence: &syncMap, producer: publisher, + consumer: consumer, cfg: &config.SyncAPI{ Matrix: &config.Global{ JetStream: config.JetStream{ diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index b2d333f74..92db18d56 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -17,17 +17,14 @@ package syncapi import ( "context" - "github.com/gorilla/mux" "github.com/matrix-org/dendrite/internal/caching" "github.com/sirupsen/logrus" keyapi "github.com/matrix-org/dendrite/keyserver/api" "github.com/matrix-org/dendrite/roomserver/api" - "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/jetstream" - "github.com/matrix-org/dendrite/setup/process" userapi "github.com/matrix-org/dendrite/userapi/api" - "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/dendrite/syncapi/consumers" "github.com/matrix-org/dendrite/syncapi/notifier" @@ -41,28 +38,23 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the SyncAPI // component. func AddPublicRoutes( - process *process.ProcessContext, - router *mux.Router, - userAPI userapi.UserInternalAPI, - rsAPI api.RoomserverInternalAPI, - keyAPI keyapi.KeyInternalAPI, - federation *gomatrixserverlib.FederationClient, - cfg *config.SyncAPI, + base *base.BaseDendrite, + userAPI userapi.SyncUserAPI, + rsAPI api.SyncRoomserverAPI, + keyAPI keyapi.SyncKeyAPI, ) { - js, natsClient := jetstream.Prepare(process, &cfg.Matrix.JetStream) + cfg := &base.Cfg.SyncAPI - syncDB, err := storage.NewSyncServerDatasource(&cfg.Database) + js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + + syncDB, err := storage.NewSyncServerDatasource(base, &cfg.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to sync db") } eduCache := caching.NewTypingCache() - lazyLoadCache, err := caching.NewLazyLoadCache() - if err != nil { - logrus.WithError(err).Panicf("failed to create lazy loading cache") - } notifier := notifier.NewNotifier() - streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, keyAPI, eduCache, lazyLoadCache, notifier) + streams := streams.NewSyncStreamProviders(syncDB, userAPI, rsAPI, keyAPI, eduCache, base.Caches, notifier) notifier.SetCurrentPosition(streams.Latest(context.Background())) if err = notifier.Load(context.Background(), syncDB); err != nil { logrus.WithError(err).Panicf("failed to load notifier ") @@ -72,8 +64,17 @@ func AddPublicRoutes( Topic: cfg.Matrix.JetStream.Prefixed(jetstream.OutputPresenceEvent), JetStream: js, } + presenceConsumer := consumers.NewPresenceConsumer( + base.ProcessContext, cfg, js, natsClient, syncDB, + notifier, streams.PresenceStreamProvider, + userAPI, + ) - requestPool := sync.NewRequestPool(syncDB, cfg, userAPI, keyAPI, rsAPI, streams, notifier, federationPresenceProducer) + requestPool := sync.NewRequestPool(syncDB, cfg, userAPI, keyAPI, rsAPI, streams, notifier, federationPresenceProducer, presenceConsumer, base.EnableMetrics) + + if err = presenceConsumer.Start(); err != nil { + logrus.WithError(err).Panicf("failed to start presence consumer") + } userAPIStreamEventProducer := &producers.UserAPIStreamEventProducer{ JetStream: js, @@ -86,8 +87,8 @@ func AddPublicRoutes( } keyChangeConsumer := consumers.NewOutputKeyChangeEventConsumer( - process, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), - js, keyAPI, rsAPI, syncDB, notifier, + base.ProcessContext, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), + js, rsAPI, syncDB, notifier, streams.DeviceListStreamProvider, ) if err = keyChangeConsumer.Start(); err != nil { @@ -95,7 +96,7 @@ func AddPublicRoutes( } roomConsumer := consumers.NewOutputRoomEventConsumer( - process, cfg, js, syncDB, notifier, streams.PDUStreamProvider, + base.ProcessContext, cfg, js, syncDB, notifier, streams.PDUStreamProvider, streams.InviteStreamProvider, rsAPI, userAPIStreamEventProducer, ) if err = roomConsumer.Start(); err != nil { @@ -103,7 +104,7 @@ func AddPublicRoutes( } clientConsumer := consumers.NewOutputClientDataConsumer( - process, cfg, js, syncDB, notifier, streams.AccountDataStreamProvider, + base.ProcessContext, cfg, js, syncDB, notifier, streams.AccountDataStreamProvider, userAPIReadUpdateProducer, ) if err = clientConsumer.Start(); err != nil { @@ -111,42 +112,36 @@ func AddPublicRoutes( } notificationConsumer := consumers.NewOutputNotificationDataConsumer( - process, cfg, js, syncDB, notifier, streams.NotificationDataStreamProvider, + base.ProcessContext, cfg, js, syncDB, notifier, streams.NotificationDataStreamProvider, ) if err = notificationConsumer.Start(); err != nil { logrus.WithError(err).Panicf("failed to start notification data consumer") } typingConsumer := consumers.NewOutputTypingEventConsumer( - process, cfg, js, eduCache, notifier, streams.TypingStreamProvider, + base.ProcessContext, cfg, js, eduCache, notifier, streams.TypingStreamProvider, ) if err = typingConsumer.Start(); err != nil { logrus.WithError(err).Panicf("failed to start typing consumer") } sendToDeviceConsumer := consumers.NewOutputSendToDeviceEventConsumer( - process, cfg, js, syncDB, notifier, streams.SendToDeviceStreamProvider, + base.ProcessContext, cfg, js, syncDB, notifier, streams.SendToDeviceStreamProvider, ) if err = sendToDeviceConsumer.Start(); err != nil { logrus.WithError(err).Panicf("failed to start send-to-device consumer") } receiptConsumer := consumers.NewOutputReceiptEventConsumer( - process, cfg, js, syncDB, notifier, streams.ReceiptStreamProvider, + base.ProcessContext, cfg, js, syncDB, notifier, streams.ReceiptStreamProvider, userAPIReadUpdateProducer, ) if err = receiptConsumer.Start(); err != nil { logrus.WithError(err).Panicf("failed to start receipts consumer") } - presenceConsumer := consumers.NewPresenceConsumer( - process, cfg, js, natsClient, syncDB, - notifier, streams.PresenceStreamProvider, - userAPI, + routing.Setup( + base.PublicClientAPIMux, requestPool, syncDB, userAPI, + rsAPI, cfg, base.Caches, ) - if err = presenceConsumer.Start(); err != nil { - logrus.WithError(err).Panicf("failed to start presence consumer") - } - - routing.Setup(router, requestPool, syncDB, userAPI, federation, rsAPI, cfg, lazyLoadCache) } diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go new file mode 100644 index 000000000..3ce7c64b7 --- /dev/null +++ b/syncapi/syncapi_test.go @@ -0,0 +1,330 @@ +package syncapi + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + keyapi "github.com/matrix-org/dendrite/keyserver/api" + "github.com/matrix-org/dendrite/roomserver/api" + rsapi "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/setup/base" + "github.com/matrix-org/dendrite/setup/jetstream" + "github.com/matrix-org/dendrite/syncapi/types" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/dendrite/test/testrig" + userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" + "github.com/nats-io/nats.go" + "github.com/tidwall/gjson" +) + +type syncRoomserverAPI struct { + rsapi.SyncRoomserverAPI + rooms []*test.Room +} + +func (s *syncRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req *rsapi.QueryLatestEventsAndStateRequest, res *rsapi.QueryLatestEventsAndStateResponse) error { + var room *test.Room + for _, r := range s.rooms { + if r.ID == req.RoomID { + room = r + break + } + } + if room == nil { + res.RoomExists = false + return nil + } + res.RoomVersion = room.Version + return nil // TODO: return state +} + +func (s *syncRoomserverAPI) QuerySharedUsers(ctx context.Context, req *rsapi.QuerySharedUsersRequest, res *rsapi.QuerySharedUsersResponse) error { + res.UserIDsToCount = make(map[string]int) + return nil +} +func (s *syncRoomserverAPI) QueryBulkStateContent(ctx context.Context, req *rsapi.QueryBulkStateContentRequest, res *rsapi.QueryBulkStateContentResponse) error { + return nil +} + +type syncUserAPI struct { + userapi.SyncUserAPI + accounts []userapi.Device +} + +func (s *syncUserAPI) QueryAccessToken(ctx context.Context, req *userapi.QueryAccessTokenRequest, res *userapi.QueryAccessTokenResponse) error { + for _, acc := range s.accounts { + if acc.AccessToken == req.AccessToken { + res.Device = &acc + return nil + } + } + res.Err = "unknown user" + return nil +} + +func (s *syncUserAPI) PerformLastSeenUpdate(ctx context.Context, req *userapi.PerformLastSeenUpdateRequest, res *userapi.PerformLastSeenUpdateResponse) error { + return nil +} + +type syncKeyAPI struct { + keyapi.SyncKeyAPI +} + +func (s *syncKeyAPI) QueryKeyChanges(ctx context.Context, req *keyapi.QueryKeyChangesRequest, res *keyapi.QueryKeyChangesResponse) { +} +func (s *syncKeyAPI) QueryOneTimeKeys(ctx context.Context, req *keyapi.QueryOneTimeKeysRequest, res *keyapi.QueryOneTimeKeysResponse) { + +} + +func TestSyncAPIAccessTokens(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + testSyncAccessTokens(t, dbType) + }) +} + +func testSyncAccessTokens(t *testing.T, dbType test.DBType) { + user := test.NewUser(t) + room := test.NewRoom(t, user) + alice := userapi.Device{ + ID: "ALICEID", + UserID: user.ID, + AccessToken: "ALICE_BEARER_TOKEN", + DisplayName: "Alice", + AccountType: userapi.AccountTypeUser, + } + + base, close := testrig.CreateBaseDendrite(t, dbType) + defer close() + + jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) + msgs := toNATSMsgs(t, base, room.Events()) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) + testrig.MustPublishMsgs(t, jsctx, msgs...) + + testCases := []struct { + name string + req *http.Request + wantCode int + wantJoinedRooms []string + }{ + { + name: "missing access token", + req: test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "timeout": "0", + })), + wantCode: 401, + }, + { + name: "unknown access token", + req: test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "access_token": "foo", + "timeout": "0", + })), + wantCode: 401, + }, + { + name: "valid access token", + req: test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "access_token": alice.AccessToken, + "timeout": "0", + })), + wantCode: 200, + wantJoinedRooms: []string{room.ID}, + }, + } + // TODO: find a better way + time.Sleep(500 * time.Millisecond) + + for _, tc := range testCases { + w := httptest.NewRecorder() + base.PublicClientAPIMux.ServeHTTP(w, tc.req) + if w.Code != tc.wantCode { + t.Fatalf("%s: got HTTP %d want %d", tc.name, w.Code, tc.wantCode) + } + if tc.wantJoinedRooms != nil { + var res types.Response + if err := json.NewDecoder(w.Body).Decode(&res); err != nil { + t.Fatalf("%s: failed to decode response body: %s", tc.name, err) + } + if len(res.Rooms.Join) != len(tc.wantJoinedRooms) { + t.Errorf("%s: got %v joined rooms, want %v.\nResponse: %+v", tc.name, len(res.Rooms.Join), len(tc.wantJoinedRooms), res) + } + t.Logf("res: %+v", res.Rooms.Join[room.ID]) + + gotEventIDs := make([]string, len(res.Rooms.Join[room.ID].Timeline.Events)) + for i, ev := range res.Rooms.Join[room.ID].Timeline.Events { + gotEventIDs[i] = ev.EventID + } + test.AssertEventIDsEqual(t, gotEventIDs, room.Events()) + } + } +} + +// Tests what happens when we create a room and then /sync before all events from /createRoom have +// been sent to the syncapi +func TestSyncAPICreateRoomSyncEarly(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + testSyncAPICreateRoomSyncEarly(t, dbType) + }) +} + +func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { + user := test.NewUser(t) + room := test.NewRoom(t, user) + alice := userapi.Device{ + ID: "ALICEID", + UserID: user.ID, + AccessToken: "ALICE_BEARER_TOKEN", + DisplayName: "Alice", + AccountType: userapi.AccountTypeUser, + } + + base, close := testrig.CreateBaseDendrite(t, dbType) + defer close() + + jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) + // order is: + // m.room.create + // m.room.member + // m.room.power_levels + // m.room.join_rules + // m.room.history_visibility + msgs := toNATSMsgs(t, base, room.Events()) + sinceTokens := make([]string, len(msgs)) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) + for i, msg := range msgs { + testrig.MustPublishMsgs(t, jsctx, msg) + time.Sleep(100 * time.Millisecond) + w := httptest.NewRecorder() + base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "access_token": alice.AccessToken, + "timeout": "0", + }))) + if w.Code != 200 { + t.Errorf("got HTTP %d want 200", w.Code) + continue + } + var res types.Response + if err := json.NewDecoder(w.Body).Decode(&res); err != nil { + t.Errorf("failed to decode response body: %s", err) + } + sinceTokens[i] = res.NextBatch.String() + if i == 0 { // create event does not produce a room section + if len(res.Rooms.Join) != 0 { + t.Fatalf("i=%v got %d joined rooms, want 0", i, len(res.Rooms.Join)) + } + } else { // we should have that room somewhere + if len(res.Rooms.Join) != 1 { + t.Fatalf("i=%v got %d joined rooms, want 1", i, len(res.Rooms.Join)) + } + } + } + + // sync with no token "" and with the penultimate token and this should neatly return room events in the timeline block + sinceTokens = append([]string{""}, sinceTokens[:len(sinceTokens)-1]...) + + t.Logf("waited for events to be consumed; syncing with %v", sinceTokens) + for i, since := range sinceTokens { + w := httptest.NewRecorder() + base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "access_token": alice.AccessToken, + "timeout": "0", + "since": since, + }))) + if w.Code != 200 { + t.Errorf("since=%s got HTTP %d want 200", since, w.Code) + } + var res types.Response + if err := json.NewDecoder(w.Body).Decode(&res); err != nil { + t.Errorf("failed to decode response body: %s", err) + } + if len(res.Rooms.Join) != 1 { + t.Fatalf("since=%s got %d joined rooms, want 1", since, len(res.Rooms.Join)) + } + t.Logf("since=%s res state:%+v res timeline:%+v", since, res.Rooms.Join[room.ID].State.Events, res.Rooms.Join[room.ID].Timeline.Events) + gotEventIDs := make([]string, len(res.Rooms.Join[room.ID].Timeline.Events)) + for j, ev := range res.Rooms.Join[room.ID].Timeline.Events { + gotEventIDs[j] = ev.EventID + } + test.AssertEventIDsEqual(t, gotEventIDs, room.Events()[i:]) + } +} + +// Test that if we hit /sync we get back presence: online, regardless of whether messages get delivered +// via NATS. Regression test for a flakey test "User sees their own presence in a sync" +func TestSyncAPIUpdatePresenceImmediately(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + testSyncAPIUpdatePresenceImmediately(t, dbType) + }) +} + +func testSyncAPIUpdatePresenceImmediately(t *testing.T, dbType test.DBType) { + user := test.NewUser(t) + alice := userapi.Device{ + ID: "ALICEID", + UserID: user.ID, + AccessToken: "ALICE_BEARER_TOKEN", + DisplayName: "Alice", + AccountType: userapi.AccountTypeUser, + } + + base, close := testrig.CreateBaseDendrite(t, dbType) + base.Cfg.Global.Presence.EnableOutbound = true + base.Cfg.Global.Presence.EnableInbound = true + defer close() + + jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{}, &syncKeyAPI{}) + w := httptest.NewRecorder() + base.PublicClientAPIMux.ServeHTTP(w, test.NewRequest(t, "GET", "/_matrix/client/v3/sync", test.WithQueryParams(map[string]string{ + "access_token": alice.AccessToken, + "timeout": "0", + "set_presence": "online", + }))) + if w.Code != 200 { + t.Fatalf("got HTTP %d want %d", w.Code, 200) + } + var res types.Response + if err := json.NewDecoder(w.Body).Decode(&res); err != nil { + t.Errorf("failed to decode response body: %s", err) + } + if len(res.Presence.Events) != 1 { + t.Fatalf("expected 1 presence events, got: %+v", res.Presence.Events) + } + if res.Presence.Events[0].Sender != alice.UserID { + t.Errorf("sender: got %v want %v", res.Presence.Events[0].Sender, alice.UserID) + } + if res.Presence.Events[0].Type != "m.presence" { + t.Errorf("type: got %v want %v", res.Presence.Events[0].Type, "m.presence") + } + if gjson.ParseBytes(res.Presence.Events[0].Content).Get("presence").Str != "online" { + t.Errorf("content: not online, got %v", res.Presence.Events[0].Content) + } + +} + +func toNATSMsgs(t *testing.T, base *base.BaseDendrite, input []*gomatrixserverlib.HeaderedEvent) []*nats.Msg { + result := make([]*nats.Msg, len(input)) + for i, ev := range input { + var addsStateIDs []string + if ev.StateKey() != nil { + addsStateIDs = append(addsStateIDs, ev.EventID()) + } + result[i] = testrig.NewOutputEventMsg(t, base, ev.RoomID(), api.OutputEvent{ + Type: rsapi.OutputTypeNewRoomEvent, + NewRoomEvent: &rsapi.OutputNewRoomEvent{ + Event: ev, + AddsStateEventIDs: addsStateIDs, + }, + }) + } + return result +} diff --git a/syncapi/types/provider.go b/syncapi/types/provider.go index e6777f643..a9ea234d0 100644 --- a/syncapi/types/provider.go +++ b/syncapi/types/provider.go @@ -25,6 +25,23 @@ type SyncRequest struct { IgnoredUsers IgnoredUsers } +func (r *SyncRequest) IsRoomPresent(roomID string) bool { + membership, ok := r.Rooms[roomID] + if !ok { + return false + } + switch membership { + case gomatrixserverlib.Join: + return true + case gomatrixserverlib.Invite: + return true + case gomatrixserverlib.Peek: + return true + default: + return false + } +} + type StreamProvider interface { Setup() diff --git a/syncapi/types/types.go b/syncapi/types/types.go index ba6b4f8cd..159fa08b6 100644 --- a/syncapi/types/types.go +++ b/syncapi/types/types.go @@ -350,6 +350,19 @@ type Response struct { DeviceListsOTKCount map[string]int `json:"device_one_time_keys_count,omitempty"` } +func (r *Response) HasUpdates() bool { + // purposefully exclude DeviceListsOTKCount as we always include them + return (len(r.AccountData.Events) > 0 || + len(r.Presence.Events) > 0 || + len(r.Rooms.Invite) > 0 || + len(r.Rooms.Join) > 0 || + len(r.Rooms.Leave) > 0 || + len(r.Rooms.Peek) > 0 || + len(r.ToDevice.Events) > 0 || + len(r.DeviceLists.Changed) > 0 || + len(r.DeviceLists.Left) > 0) +} + // NewResponse creates an empty response with initialised maps. func NewResponse() *Response { res := Response{} diff --git a/sytest-blacklist b/sytest-blacklist index f1bd60db1..be0826eee 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -1,7 +1,3 @@ -# Blacklisted until matrix-org/dendrite#862 is reverted due to Riot bug - -Latest account data appears in v2 /sync - # Relies on a rejected PL event which will never be accepted into the DAG # Caused by @@ -52,4 +48,3 @@ Notifications can be viewed with GET /notifications # More flakey If remote user leaves room we no longer receive device updates -Local device key changes get to remote servers diff --git a/sytest-whitelist b/sytest-whitelist index 979f12bf6..6af8d89ff 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -154,7 +154,7 @@ Can add account data Can add account data to room Can get account data without syncing Can get room account data without syncing -#Latest account data appears in v2 /sync +Latest account data appears in v2 /sync New account data appears in incremental v2 /sync Checking local federation server Inbound federation can query profile data @@ -681,8 +681,6 @@ GET /presence/:user_id/status fetches initial status PUT /presence/:user_id/status updates my presence Presence change reports an event to myself Existing members see new members' presence -#Existing members see new member's presence -Newly joined room includes presence in incremental sync Get presence for newly joined members in incremental sync User sees their own presence in a sync User sees updates to presence from other users in the incremental sync. @@ -709,4 +707,12 @@ Gapped incremental syncs include all state changes Old leaves are present in gapped incremental syncs Leaves are present in non-gapped incremental syncs Members from the gap are included in gappy incr LL sync -Presence can be set from sync \ No newline at end of file +Presence can be set from sync +/state returns M_NOT_FOUND for a rejected message event +/state_ids returns M_NOT_FOUND for a rejected message event +/state returns M_NOT_FOUND for a rejected state event +/state_ids returns M_NOT_FOUND for a rejected state event +PUT /rooms/:room_id/redact/:event_id/:txn_id is idempotent +Unnamed room comes with a name summary +Named room comes with just joined member count summary +Room summary only has 5 heroes \ No newline at end of file diff --git a/test/db.go b/test/db.go index 6412feaa6..c7cb919f6 100644 --- a/test/db.go +++ b/test/db.go @@ -33,10 +33,20 @@ var DBTypeSQLite DBType = 1 var DBTypePostgres DBType = 2 var Quiet = false +var Required = os.Getenv("DENDRITE_TEST_SKIP_NODB") == "" -func createLocalDB(dbName string) { - if !Quiet { - fmt.Println("Note: tests require a postgres install accessible to the current user") +func fatalError(t *testing.T, format string, args ...interface{}) { + if Required { + t.Fatalf(format, args...) + } else { + t.Skipf(format, args...) + } +} + +func createLocalDB(t *testing.T, dbName string) { + if _, err := exec.LookPath("createdb"); err != nil { + fatalError(t, "Note: tests require a postgres install accessible to the current user") + return } createDB := exec.Command("createdb", dbName) if !Quiet { @@ -52,7 +62,10 @@ func createLocalDB(dbName string) { func createRemoteDB(t *testing.T, dbName, user, connStr string) { db, err := sql.Open("postgres", connStr+" dbname=postgres") if err != nil { - t.Fatalf("failed to open postgres conn with connstr=%s : %s", connStr, err) + fatalError(t, "failed to open postgres conn with connstr=%s : %s", connStr, err) + } + if err = db.Ping(); err != nil { + fatalError(t, "failed to open postgres conn with connstr=%s : %s", connStr, err) } _, err = db.Exec(fmt.Sprintf(`CREATE DATABASE %s;`, dbName)) if err != nil { @@ -133,7 +146,7 @@ func PrepareDBConnectionString(t *testing.T, dbType DBType) (connStr string, clo hash := sha256.Sum256([]byte(wd)) dbName := fmt.Sprintf("dendrite_test_%s", hex.EncodeToString(hash[:16])) if postgresDB == "" { // local server, use createdb - createLocalDB(dbName) + createLocalDB(t, dbName) } else { // remote server, shell into the postgres user and CREATE DATABASE createRemoteDB(t, dbName, user, connStr) } diff --git a/test/event.go b/test/event.go index b2e2805ba..73fc656bd 100644 --- a/test/event.go +++ b/test/event.go @@ -52,6 +52,24 @@ func WithUnsigned(unsigned interface{}) eventModifier { } } +func WithKeyID(keyID gomatrixserverlib.KeyID) eventModifier { + return func(e *eventMods) { + e.keyID = keyID + } +} + +func WithPrivateKey(pkey ed25519.PrivateKey) eventModifier { + return func(e *eventMods) { + e.privKey = pkey + } +} + +func WithOrigin(origin gomatrixserverlib.ServerName) eventModifier { + return func(e *eventMods) { + e.origin = origin + } +} + // Reverse a list of events func Reversed(in []*gomatrixserverlib.HeaderedEvent) []*gomatrixserverlib.HeaderedEvent { out := make([]*gomatrixserverlib.HeaderedEvent, len(in)) @@ -64,7 +82,8 @@ func Reversed(in []*gomatrixserverlib.HeaderedEvent) []*gomatrixserverlib.Header func AssertEventIDsEqual(t *testing.T, gotEventIDs []string, wants []*gomatrixserverlib.HeaderedEvent) { t.Helper() if len(gotEventIDs) != len(wants) { - t.Fatalf("length mismatch: got %d events, want %d", len(gotEventIDs), len(wants)) + t.Errorf("length mismatch: got %d events, want %d", len(gotEventIDs), len(wants)) + return } for i := range wants { w := wants[i].EventID() diff --git a/test/http.go b/test/http.go new file mode 100644 index 000000000..37b3648f8 --- /dev/null +++ b/test/http.go @@ -0,0 +1,92 @@ +package test + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "path/filepath" + "sync" + "testing" +) + +type HTTPRequestOpt func(req *http.Request) + +func WithJSONBody(t *testing.T, body interface{}) HTTPRequestOpt { + t.Helper() + b, err := json.Marshal(body) + if err != nil { + t.Fatalf("WithJSONBody: %s", err) + } + return func(req *http.Request) { + req.Body = io.NopCloser(bytes.NewBuffer(b)) + } +} + +func WithQueryParams(qps map[string]string) HTTPRequestOpt { + var vals url.Values = map[string][]string{} + for k, v := range qps { + vals.Set(k, v) + } + return func(req *http.Request) { + req.URL.RawQuery = vals.Encode() + } +} + +func NewRequest(t *testing.T, method, path string, opts ...HTTPRequestOpt) *http.Request { + t.Helper() + req, err := http.NewRequest(method, "http://localhost"+path, nil) + if err != nil { + t.Fatalf("failed to make new HTTP request %v %v : %v", method, path, err) + } + for _, o := range opts { + o(req) + } + return req +} + +// ListenAndServe will listen on a random high-numbered port and attach the given router. +// Returns the base URL to send requests to. Call `cancel` to shutdown the server, which will block until it has closed. +func ListenAndServe(t *testing.T, router http.Handler, withTLS bool) (apiURL string, cancel func()) { + listener, err := net.Listen("tcp", ":0") + if err != nil { + t.Fatalf("failed to listen: %s", err) + } + port := listener.Addr().(*net.TCPAddr).Port + srv := http.Server{} + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + srv.Handler = router + var err error + if withTLS { + certFile := filepath.Join(t.TempDir(), "dendrite.cert") + keyFile := filepath.Join(t.TempDir(), "dendrite.key") + err = NewTLSKey(keyFile, certFile) + if err != nil { + t.Errorf("failed to make TLS key: %s", err) + return + } + err = srv.ServeTLS(listener, certFile, keyFile) + } else { + err = srv.Serve(listener) + } + if err != nil && err != http.ErrServerClosed { + t.Logf("Listen failed: %s", err) + } + }() + s := "" + if withTLS { + s = "s" + } + return fmt.Sprintf("http%s://localhost:%d", s, port), func() { + _ = srv.Shutdown(context.Background()) + wg.Wait() + } +} diff --git a/internal/test/keyring.go b/test/keyring.go similarity index 100% rename from internal/test/keyring.go rename to test/keyring.go diff --git a/internal/test/config.go b/test/keys.go similarity index 61% rename from internal/test/config.go rename to test/keys.go index d8e0c4531..75e3800e0 100644 --- a/internal/test/config.go +++ b/test/keys.go @@ -25,103 +25,19 @@ import ( "io/ioutil" "math/big" "os" - "path/filepath" "strings" "time" - - "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/gomatrixserverlib" - "gopkg.in/yaml.v2" ) const ( - // ConfigFile is the name of the config file for a server. - ConfigFile = "dendrite.yaml" // ServerKeyFile is the name of the file holding the matrix server private key. ServerKeyFile = "server_key.pem" // TLSCertFile is the name of the file holding the TLS certificate used for federation. TLSCertFile = "tls_cert.pem" // TLSKeyFile is the name of the file holding the TLS key used for federation. TLSKeyFile = "tls_key.pem" - // MediaDir is the name of the directory used to store media. - MediaDir = "media" ) -// MakeConfig makes a config suitable for running integration tests. -// Generates new matrix and TLS keys for the server. -func MakeConfig(configDir, kafkaURI, database, host string, startPort int) (*config.Dendrite, int, error) { - var cfg config.Dendrite - cfg.Defaults(true) - - port := startPort - assignAddress := func() config.HTTPAddress { - result := config.HTTPAddress(fmt.Sprintf("http://%s:%d", host, port)) - port++ - return result - } - - serverKeyPath := filepath.Join(configDir, ServerKeyFile) - tlsCertPath := filepath.Join(configDir, TLSKeyFile) - tlsKeyPath := filepath.Join(configDir, TLSCertFile) - mediaBasePath := filepath.Join(configDir, MediaDir) - - if err := NewMatrixKey(serverKeyPath); err != nil { - return nil, 0, err - } - - if err := NewTLSKey(tlsKeyPath, tlsCertPath); err != nil { - return nil, 0, err - } - - cfg.Version = config.Version - - cfg.Global.ServerName = gomatrixserverlib.ServerName(assignAddress()) - cfg.Global.PrivateKeyPath = config.Path(serverKeyPath) - - cfg.MediaAPI.BasePath = config.Path(mediaBasePath) - - cfg.Global.JetStream.Addresses = []string{kafkaURI} - - // TODO: Use different databases for the different schemas. - // Using the same database for every schema currently works because - // the table names are globally unique. But we might not want to - // rely on that in the future. - cfg.AppServiceAPI.Database.ConnectionString = config.DataSource(database) - cfg.FederationAPI.Database.ConnectionString = config.DataSource(database) - cfg.KeyServer.Database.ConnectionString = config.DataSource(database) - cfg.MediaAPI.Database.ConnectionString = config.DataSource(database) - cfg.RoomServer.Database.ConnectionString = config.DataSource(database) - cfg.SyncAPI.Database.ConnectionString = config.DataSource(database) - cfg.UserAPI.AccountDatabase.ConnectionString = config.DataSource(database) - - cfg.AppServiceAPI.InternalAPI.Listen = assignAddress() - cfg.FederationAPI.InternalAPI.Listen = assignAddress() - cfg.KeyServer.InternalAPI.Listen = assignAddress() - cfg.MediaAPI.InternalAPI.Listen = assignAddress() - cfg.RoomServer.InternalAPI.Listen = assignAddress() - cfg.SyncAPI.InternalAPI.Listen = assignAddress() - cfg.UserAPI.InternalAPI.Listen = assignAddress() - - cfg.AppServiceAPI.InternalAPI.Connect = cfg.AppServiceAPI.InternalAPI.Listen - cfg.FederationAPI.InternalAPI.Connect = cfg.FederationAPI.InternalAPI.Listen - cfg.KeyServer.InternalAPI.Connect = cfg.KeyServer.InternalAPI.Listen - cfg.MediaAPI.InternalAPI.Connect = cfg.MediaAPI.InternalAPI.Listen - cfg.RoomServer.InternalAPI.Connect = cfg.RoomServer.InternalAPI.Listen - cfg.SyncAPI.InternalAPI.Connect = cfg.SyncAPI.InternalAPI.Listen - cfg.UserAPI.InternalAPI.Connect = cfg.UserAPI.InternalAPI.Listen - - return &cfg, port, nil -} - -// WriteConfig writes the config file to the directory. -func WriteConfig(cfg *config.Dendrite, configDir string) error { - data, err := yaml.Marshal(cfg) - if err != nil { - return err - } - return ioutil.WriteFile(filepath.Join(configDir, ConfigFile), data, 0666) -} - // NewMatrixKey generates a new ed25519 matrix server key and writes it to a file. func NewMatrixKey(matrixKeyPath string) (err error) { var data [35]byte diff --git a/test/room.go b/test/room.go index 619cb5c9a..6ae403b3f 100644 --- a/test/room.go +++ b/test/room.go @@ -15,7 +15,6 @@ package test import ( - "crypto/ed25519" "encoding/json" "fmt" "sync/atomic" @@ -35,12 +34,6 @@ var ( PresetTrustedPrivateChat Preset = 3 roomIDCounter = int64(0) - - testKeyID = gomatrixserverlib.KeyID("ed25519:test") - testPrivateKey = ed25519.NewKeyFromSeed([]byte{ - 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, - 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, - }) ) type Room struct { @@ -49,22 +42,25 @@ type Room struct { preset Preset creator *User - authEvents gomatrixserverlib.AuthEvents - events []*gomatrixserverlib.HeaderedEvent + authEvents gomatrixserverlib.AuthEvents + currentState map[string]*gomatrixserverlib.HeaderedEvent + events []*gomatrixserverlib.HeaderedEvent } // Create a new test room. Automatically creates the initial create events. func NewRoom(t *testing.T, creator *User, modifiers ...roomModifier) *Room { t.Helper() counter := atomic.AddInt64(&roomIDCounter, 1) - - // set defaults then let roomModifiers override + if creator.srvName == "" { + t.Fatalf("NewRoom: creator doesn't belong to a server: %+v", *creator) + } r := &Room{ - ID: fmt.Sprintf("!%d:localhost", counter), - creator: creator, - authEvents: gomatrixserverlib.NewAuthEvents(nil), - preset: PresetPublicChat, - Version: gomatrixserverlib.RoomVersionV9, + ID: fmt.Sprintf("!%d:%s", counter, creator.srvName), + creator: creator, + authEvents: gomatrixserverlib.NewAuthEvents(nil), + preset: PresetPublicChat, + Version: gomatrixserverlib.RoomVersionV9, + currentState: make(map[string]*gomatrixserverlib.HeaderedEvent), } for _, m := range modifiers { m(t, r) @@ -73,6 +69,24 @@ func NewRoom(t *testing.T, creator *User, modifiers ...roomModifier) *Room { return r } +func (r *Room) MustGetAuthEventRefsForEvent(t *testing.T, needed gomatrixserverlib.StateNeeded) []gomatrixserverlib.EventReference { + t.Helper() + a, err := needed.AuthEventReferences(&r.authEvents) + if err != nil { + t.Fatalf("MustGetAuthEvents: %v", err) + } + return a +} + +func (r *Room) ForwardExtremities() []string { + if len(r.events) == 0 { + return nil + } + return []string{ + r.events[len(r.events)-1].EventID(), + } +} + func (r *Room) insertCreateEvents(t *testing.T) { t.Helper() var joinRule gomatrixserverlib.JoinRuleContent @@ -88,6 +102,7 @@ func (r *Room) insertCreateEvents(t *testing.T) { joinRule.JoinRule = "public" hisVis.HistoryVisibility = "shared" } + r.CreateAndInsert(t, r.creator, gomatrixserverlib.MRoomCreate, map[string]interface{}{ "creator": r.creator.ID, "room_version": r.Version, @@ -112,16 +127,16 @@ func (r *Room) CreateEvent(t *testing.T, creator *User, eventType string, conten } if mod.privKey == nil { - mod.privKey = testPrivateKey + mod.privKey = creator.privKey } if mod.keyID == "" { - mod.keyID = testKeyID + mod.keyID = creator.keyID } if mod.originServerTS.IsZero() { mod.originServerTS = time.Now() } if mod.origin == "" { - mod.origin = gomatrixserverlib.ServerName("localhost") + mod.origin = creator.srvName } var unsigned gomatrixserverlib.RawJSON @@ -174,13 +189,14 @@ func (r *Room) CreateEvent(t *testing.T, creator *User, eventType string, conten // Add a new event to this room DAG. Not thread-safe. func (r *Room) InsertEvent(t *testing.T, he *gomatrixserverlib.HeaderedEvent) { t.Helper() - // Add the event to the list of auth events + // Add the event to the list of auth/state events r.events = append(r.events, he) if he.StateKey() != nil { err := r.authEvents.AddEvent(he.Unwrap()) if err != nil { t.Fatalf("InsertEvent: failed to add event to auth events: %s", err) } + r.currentState[he.Type()+" "+*he.StateKey()] = he } } @@ -188,6 +204,16 @@ func (r *Room) Events() []*gomatrixserverlib.HeaderedEvent { return r.events } +func (r *Room) CurrentState() []*gomatrixserverlib.HeaderedEvent { + events := make([]*gomatrixserverlib.HeaderedEvent, len(r.currentState)) + i := 0 + for _, e := range r.currentState { + events[i] = e + i++ + } + return events +} + func (r *Room) CreateAndInsert(t *testing.T, creator *User, eventType string, content interface{}, mods ...eventModifier) *gomatrixserverlib.HeaderedEvent { t.Helper() he := r.CreateEvent(t, creator, eventType, content, mods...) diff --git a/internal/test/slice.go b/test/slice.go similarity index 100% rename from internal/test/slice.go rename to test/slice.go diff --git a/test/testrig/base.go b/test/testrig/base.go new file mode 100644 index 000000000..facb49f3e --- /dev/null +++ b/test/testrig/base.go @@ -0,0 +1,91 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package testrig + +import ( + "errors" + "fmt" + "io/fs" + "os" + "strings" + "testing" + + "github.com/matrix-org/dendrite/setup/base" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/nats-io/nats.go" +) + +func CreateBaseDendrite(t *testing.T, dbType test.DBType) (*base.BaseDendrite, func()) { + var cfg config.Dendrite + cfg.Defaults(false) + cfg.Global.JetStream.InMemory = true + + switch dbType { + case test.DBTypePostgres: + cfg.Global.Defaults(true) // autogen a signing key + cfg.MediaAPI.Defaults(true) // autogen a media path + // use a distinct prefix else concurrent postgres/sqlite runs will clash since NATS will use + // the file system event with InMemory=true :( + cfg.Global.JetStream.TopicPrefix = fmt.Sprintf("Test_%d_", dbType) + connStr, close := test.PrepareDBConnectionString(t, dbType) + cfg.Global.DatabaseOptions = config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + MaxOpenConnections: 10, + MaxIdleConnections: 2, + ConnMaxLifetimeSeconds: 60, + } + return base.NewBaseDendrite(&cfg, "Test", base.DisableMetrics), close + case test.DBTypeSQLite: + cfg.Defaults(true) // sets a sqlite db per component + // use a distinct prefix else concurrent postgres/sqlite runs will clash since NATS will use + // the file system event with InMemory=true :( + cfg.Global.JetStream.TopicPrefix = fmt.Sprintf("Test_%d_", dbType) + return base.NewBaseDendrite(&cfg, "Test", base.DisableMetrics), func() { + // cleanup db files. This risks getting out of sync as we add more database strings :( + dbFiles := []config.DataSource{ + cfg.AppServiceAPI.Database.ConnectionString, + cfg.FederationAPI.Database.ConnectionString, + cfg.KeyServer.Database.ConnectionString, + cfg.MSCs.Database.ConnectionString, + cfg.MediaAPI.Database.ConnectionString, + cfg.RoomServer.Database.ConnectionString, + cfg.SyncAPI.Database.ConnectionString, + cfg.UserAPI.AccountDatabase.ConnectionString, + } + for _, fileURI := range dbFiles { + path := strings.TrimPrefix(string(fileURI), "file:") + err := os.Remove(path) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + t.Fatalf("failed to cleanup sqlite db '%s': %s", fileURI, err) + } + } + } + default: + t.Fatalf("unknown db type: %v", dbType) + } + return nil, nil +} + +func Base(cfg *config.Dendrite) (*base.BaseDendrite, nats.JetStreamContext, *nats.Conn) { + if cfg == nil { + cfg = &config.Dendrite{} + cfg.Defaults(true) + } + cfg.Global.JetStream.InMemory = true + base := base.NewBaseDendrite(cfg, "Tests") + js, jc := base.NATS.Prepare(base.ProcessContext, &cfg.Global.JetStream) + return base, js, jc +} diff --git a/test/testrig/jetstream.go b/test/testrig/jetstream.go new file mode 100644 index 000000000..74cf95062 --- /dev/null +++ b/test/testrig/jetstream.go @@ -0,0 +1,35 @@ +package testrig + +import ( + "encoding/json" + "testing" + + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/setup/base" + "github.com/matrix-org/dendrite/setup/jetstream" + "github.com/nats-io/nats.go" +) + +func MustPublishMsgs(t *testing.T, jsctx nats.JetStreamContext, msgs ...*nats.Msg) { + t.Helper() + for _, msg := range msgs { + if _, err := jsctx.PublishMsg(msg); err != nil { + t.Fatalf("MustPublishMsgs: failed to publish message: %s", err) + } + } +} + +func NewOutputEventMsg(t *testing.T, base *base.BaseDendrite, roomID string, update api.OutputEvent) *nats.Msg { + t.Helper() + msg := &nats.Msg{ + Subject: base.Cfg.Global.JetStream.Prefixed(jetstream.OutputRoomEvent), + Header: nats.Header{}, + } + msg.Header.Set(jetstream.RoomID, roomID) + var err error + msg.Data, err = json.Marshal(update) + if err != nil { + t.Fatalf("failed to marshal update: %s", err) + } + return msg +} diff --git a/test/user.go b/test/user.go index 41a66e1c4..0020098a5 100644 --- a/test/user.go +++ b/test/user.go @@ -15,22 +15,64 @@ package test import ( + "crypto/ed25519" "fmt" "sync/atomic" + "testing" + + "github.com/matrix-org/gomatrixserverlib" ) var ( userIDCounter = int64(0) + + serverName = gomatrixserverlib.ServerName("test") + keyID = gomatrixserverlib.KeyID("ed25519:test") + privateKey = ed25519.NewKeyFromSeed([]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, + }) + + // private keys that tests can use + PrivateKeyA = ed25519.NewKeyFromSeed([]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 77, + }) + PrivateKeyB = ed25519.NewKeyFromSeed([]byte{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 66, + }) ) type User struct { ID string + // key ID and private key of the server who has this user, if known. + keyID gomatrixserverlib.KeyID + privKey ed25519.PrivateKey + srvName gomatrixserverlib.ServerName } -func NewUser() *User { - counter := atomic.AddInt64(&userIDCounter, 1) - u := &User{ - ID: fmt.Sprintf("@%d:localhost", counter), +type UserOpt func(*User) + +func WithSigningServer(srvName gomatrixserverlib.ServerName, keyID gomatrixserverlib.KeyID, privKey ed25519.PrivateKey) UserOpt { + return func(u *User) { + u.keyID = keyID + u.privKey = privKey + u.srvName = srvName } - return u +} + +func NewUser(t *testing.T, opts ...UserOpt) *User { + counter := atomic.AddInt64(&userIDCounter, 1) + var u User + for _, opt := range opts { + opt(&u) + } + if u.keyID == "" || u.srvName == "" || u.privKey == nil { + t.Logf("NewUser: missing signing server credentials; using default.") + WithSigningServer(serverName, keyID, privateKey)(&u) + } + u.ID = fmt.Sprintf("@%d:%s", counter, u.srvName) + t.Logf("NewUser: created user %s", u.ID) + return &u } diff --git a/userapi/api/api.go b/userapi/api/api.go index 6aa6a6842..df9408acb 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -26,73 +26,102 @@ import ( // UserInternalAPI is the internal API for information about users and devices. type UserInternalAPI interface { - LoginTokenInternalAPI - UserProfileAPI - UserRegisterAPI - UserAccountAPI - UserThreePIDAPI - UserDeviceAPI + AppserviceUserAPI + SyncUserAPI + ClientUserAPI + MediaUserAPI + FederationUserAPI + RoomserverUserAPI + KeyserverUserAPI - InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error - - PerformOpenIDTokenCreation(ctx context.Context, req *PerformOpenIDTokenCreationRequest, res *PerformOpenIDTokenCreationResponse) error - PerformKeyBackup(ctx context.Context, req *PerformKeyBackupRequest, res *PerformKeyBackupResponse) error - PerformPusherSet(ctx context.Context, req *PerformPusherSetRequest, res *struct{}) error - PerformPusherDeletion(ctx context.Context, req *PerformPusherDeletionRequest, res *struct{}) error - PerformPushRulesPut(ctx context.Context, req *PerformPushRulesPutRequest, res *struct{}) error - - QueryKeyBackup(ctx context.Context, req *QueryKeyBackupRequest, res *QueryKeyBackupResponse) - QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error - QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error - QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error - QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error - QueryPushRules(ctx context.Context, req *QueryPushRulesRequest, res *QueryPushRulesResponse) error - QueryNotifications(ctx context.Context, req *QueryNotificationsRequest, res *QueryNotificationsResponse) error + QuerySearchProfilesAPI // used by p2p demos } -type UserDeviceAPI interface { - PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) error +// api functions required by the appservice api +type AppserviceUserAPI interface { + PerformAccountCreation(ctx context.Context, req *PerformAccountCreationRequest, res *PerformAccountCreationResponse) error + PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error +} + +type KeyserverUserAPI interface { + QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error + QueryDeviceInfos(ctx context.Context, req *QueryDeviceInfosRequest, res *QueryDeviceInfosResponse) error +} + +type RoomserverUserAPI interface { + QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error +} + +// api functions required by the media api +type MediaUserAPI interface { + QueryAcccessTokenAPI +} + +// api functions required by the federation api +type FederationUserAPI interface { + QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error + QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error +} + +// api functions required by the sync api +type SyncUserAPI interface { + QueryAcccessTokenAPI + QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error PerformLastSeenUpdate(ctx context.Context, req *PerformLastSeenUpdateRequest, res *PerformLastSeenUpdateResponse) error PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error QueryDeviceInfos(ctx context.Context, req *QueryDeviceInfosRequest, res *QueryDeviceInfosResponse) error } -type UserDirectoryProvider interface { - QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error -} - -// UserProfileAPI provides functions for getting user profiles -type UserProfileAPI interface { - QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error - QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error - SetAvatarURL(ctx context.Context, req *PerformSetAvatarURLRequest, res *PerformSetAvatarURLResponse) error - SetDisplayName(ctx context.Context, req *PerformUpdateDisplayNameRequest, res *struct{}) error -} - -// UserRegisterAPI defines functions for registering accounts -type UserRegisterAPI interface { +// api functions required by the client api +type ClientUserAPI interface { + QueryAcccessTokenAPI + LoginTokenInternalAPI + UserLoginAPI QueryNumericLocalpart(ctx context.Context, res *QueryNumericLocalpartResponse) error + QueryDevices(ctx context.Context, req *QueryDevicesRequest, res *QueryDevicesResponse) error + QueryProfile(ctx context.Context, req *QueryProfileRequest, res *QueryProfileResponse) error + QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error + QueryPushers(ctx context.Context, req *QueryPushersRequest, res *QueryPushersResponse) error + QueryPushRules(ctx context.Context, req *QueryPushRulesRequest, res *QueryPushRulesResponse) error QueryAccountAvailability(ctx context.Context, req *QueryAccountAvailabilityRequest, res *QueryAccountAvailabilityResponse) error PerformAccountCreation(ctx context.Context, req *PerformAccountCreationRequest, res *PerformAccountCreationResponse) error PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error -} - -// UserAccountAPI defines functions for changing an account -type UserAccountAPI interface { + PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error + PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) error PerformPasswordUpdate(ctx context.Context, req *PerformPasswordUpdateRequest, res *PerformPasswordUpdateResponse) error + PerformPusherDeletion(ctx context.Context, req *PerformPusherDeletionRequest, res *struct{}) error + PerformPusherSet(ctx context.Context, req *PerformPusherSetRequest, res *struct{}) error + PerformPushRulesPut(ctx context.Context, req *PerformPushRulesPutRequest, res *struct{}) error PerformAccountDeactivation(ctx context.Context, req *PerformAccountDeactivationRequest, res *PerformAccountDeactivationResponse) error - QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error -} + PerformOpenIDTokenCreation(ctx context.Context, req *PerformOpenIDTokenCreationRequest, res *PerformOpenIDTokenCreationResponse) error + SetAvatarURL(ctx context.Context, req *PerformSetAvatarURLRequest, res *PerformSetAvatarURLResponse) error + SetDisplayName(ctx context.Context, req *PerformUpdateDisplayNameRequest, res *struct{}) error + QueryNotifications(ctx context.Context, req *QueryNotificationsRequest, res *QueryNotificationsResponse) error + InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error + PerformKeyBackup(ctx context.Context, req *PerformKeyBackupRequest, res *PerformKeyBackupResponse) error + QueryKeyBackup(ctx context.Context, req *QueryKeyBackupRequest, res *QueryKeyBackupResponse) -// UserThreePIDAPI defines functions for 3PID -type UserThreePIDAPI interface { - QueryLocalpartForThreePID(ctx context.Context, req *QueryLocalpartForThreePIDRequest, res *QueryLocalpartForThreePIDResponse) error QueryThreePIDsForLocalpart(ctx context.Context, req *QueryThreePIDsForLocalpartRequest, res *QueryThreePIDsForLocalpartResponse) error + QueryLocalpartForThreePID(ctx context.Context, req *QueryLocalpartForThreePIDRequest, res *QueryLocalpartForThreePIDResponse) error PerformForgetThreePID(ctx context.Context, req *PerformForgetThreePIDRequest, res *struct{}) error PerformSaveThreePIDAssociation(ctx context.Context, req *PerformSaveThreePIDAssociationRequest, res *struct{}) error } +// custom api functions required by pinecone / p2p demos +type QuerySearchProfilesAPI interface { + QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error +} + +// common function for creating authenticated endpoints (used in client/media/sync api) +type QueryAcccessTokenAPI interface { + QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error +} + +type UserLoginAPI interface { + QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error +} + type PerformKeyBackupRequest struct { UserID string Version string // optional if modifying a key backup @@ -320,6 +349,7 @@ type PerformLastSeenUpdateRequest struct { UserID string DeviceID string RemoteAddr string + UserAgent string } // PerformLastSeenUpdateResponse is the response for PerformLastSeenUpdate. diff --git a/userapi/consumers/syncapi_streamevent.go b/userapi/consumers/syncapi_streamevent.go index 9ef7b5083..7807c7637 100644 --- a/userapi/consumers/syncapi_streamevent.go +++ b/userapi/consumers/syncapi_streamevent.go @@ -29,7 +29,7 @@ type OutputStreamEventConsumer struct { ctx context.Context cfg *config.UserAPI userAPI api.UserInternalAPI - rsAPI rsapi.RoomserverInternalAPI + rsAPI rsapi.UserRoomserverAPI jetstream nats.JetStreamContext durable string db storage.Database @@ -45,7 +45,7 @@ func NewOutputStreamEventConsumer( store storage.Database, pgClient pushgateway.Client, userAPI api.UserInternalAPI, - rsAPI rsapi.RoomserverInternalAPI, + rsAPI rsapi.UserRoomserverAPI, syncProducer *producers.SyncAPI, ) *OutputStreamEventConsumer { return &OutputStreamEventConsumer{ @@ -455,7 +455,7 @@ func (s *OutputStreamEventConsumer) evaluatePushRules(ctx context.Context, event type ruleSetEvalContext struct { ctx context.Context - rsAPI rsapi.RoomserverInternalAPI + rsAPI rsapi.UserRoomserverAPI mem *localMembership roomID string roomSize int diff --git a/userapi/internal/api.go b/userapi/internal/api.go index d1c12f05f..9d2f63c72 100644 --- a/userapi/internal/api.go +++ b/userapi/internal/api.go @@ -48,7 +48,7 @@ type UserInternalAPI struct { ServerName gomatrixserverlib.ServerName // AppServices is the list of all registered AS AppServices []config.ApplicationService - KeyAPI keyapi.KeyInternalAPI + KeyAPI keyapi.UserKeyAPI } func (a *UserInternalAPI) InputAccountData(ctx context.Context, req *api.InputAccountDataRequest, res *api.InputAccountDataResponse) error { @@ -90,6 +90,13 @@ func (a *UserInternalAPI) PerformAccountCreation(ctx context.Context, req *api.P return nil } + // Inform the SyncAPI about the newly created push_rules + if err = a.SyncProducer.SendAccountData(acc.UserID, "", "m.push_rules"); err != nil { + util.GetLogger(ctx).WithFields(logrus.Fields{ + "user_id": acc.UserID, + }).WithError(err).Warn("failed to send account data to the SyncAPI") + } + if req.AccountType == api.AccountTypeGuest { res.AccountCreated = true res.Account = acc @@ -203,7 +210,7 @@ func (a *UserInternalAPI) PerformLastSeenUpdate( if err != nil { return fmt.Errorf("gomatrixserverlib.SplitID: %w", err) } - if err := a.DB.UpdateDeviceLastSeen(ctx, localpart, req.DeviceID, req.RemoteAddr); err != nil { + if err := a.DB.UpdateDeviceLastSeen(ctx, localpart, req.DeviceID, req.RemoteAddr, req.UserAgent); err != nil { return fmt.Errorf("a.DeviceDB.UpdateDeviceLastSeen: %w", err) } return nil diff --git a/userapi/storage/interface.go b/userapi/storage/interface.go index b15470dd4..f7cd1810a 100644 --- a/userapi/storage/interface.go +++ b/userapi/storage/interface.go @@ -22,23 +22,30 @@ import ( "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/dendrite/userapi/storage/tables" + "github.com/matrix-org/dendrite/userapi/types" ) type Profile interface { GetProfileByLocalpart(ctx context.Context, localpart string) (*authtypes.Profile, error) SearchProfiles(ctx context.Context, searchString string, limit int) ([]authtypes.Profile, error) - SetPassword(ctx context.Context, localpart string, plaintextPassword string) error SetAvatarURL(ctx context.Context, localpart string, avatarURL string) error SetDisplayName(ctx context.Context, localpart string, displayName string) error } -type Database interface { - Profile - GetAccountByPassword(ctx context.Context, localpart, plaintextPassword string) (*api.Account, error) +type Account interface { // CreateAccount makes a new account with the given login name and password, and creates an empty profile // for this account. If no password is supplied, the account will be a passwordless account. If the // account already exists, it will return nil, ErrUserExists. CreateAccount(ctx context.Context, localpart string, plaintextPassword string, appserviceID string, accountType api.AccountType) (*api.Account, error) + GetAccountByPassword(ctx context.Context, localpart, plaintextPassword string) (*api.Account, error) + GetNewNumericLocalpart(ctx context.Context) (int64, error) + CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) + GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error) + DeactivateAccount(ctx context.Context, localpart string) (err error) + SetPassword(ctx context.Context, localpart string, plaintextPassword string) error +} + +type AccountData interface { SaveAccountData(ctx context.Context, localpart, roomID, dataType string, content json.RawMessage) error GetAccountData(ctx context.Context, localpart string) (global map[string]json.RawMessage, rooms map[string]map[string]json.RawMessage, err error) // GetAccountDataByType returns account data matching a given @@ -46,26 +53,9 @@ type Database interface { // If no account data could be found, returns nil // Returns an error if there was an issue with the retrieval GetAccountDataByType(ctx context.Context, localpart, roomID, dataType string) (data json.RawMessage, err error) - GetNewNumericLocalpart(ctx context.Context) (int64, error) - SaveThreePIDAssociation(ctx context.Context, threepid, localpart, medium string) (err error) - RemoveThreePIDAssociation(ctx context.Context, threepid string, medium string) (err error) - GetLocalpartForThreePID(ctx context.Context, threepid string, medium string) (localpart string, err error) - GetThreePIDsForLocalpart(ctx context.Context, localpart string) (threepids []authtypes.ThreePID, err error) - CheckAccountAvailability(ctx context.Context, localpart string) (bool, error) - GetAccountByLocalpart(ctx context.Context, localpart string) (*api.Account, error) - DeactivateAccount(ctx context.Context, localpart string) (err error) - CreateOpenIDToken(ctx context.Context, token, localpart string) (exp int64, err error) - GetOpenIDTokenAttributes(ctx context.Context, token string) (*api.OpenIDTokenAttributes, error) - - // Key backups - CreateKeyBackup(ctx context.Context, userID, algorithm string, authData json.RawMessage) (version string, err error) - UpdateKeyBackupAuthData(ctx context.Context, userID, version string, authData json.RawMessage) (err error) - DeleteKeyBackup(ctx context.Context, userID, version string) (exists bool, err error) - GetKeyBackup(ctx context.Context, userID, version string) (versionResult, algorithm string, authData json.RawMessage, etag string, deleted bool, err error) - UpsertBackupKeys(ctx context.Context, version, userID string, uploads []api.InternalKeyBackupSession) (count int64, etag string, err error) - GetBackupKeys(ctx context.Context, version, userID, filterRoomID, filterSessionID string) (result map[string]map[string]api.KeyBackupSession, err error) - CountBackupKeys(ctx context.Context, version, userID string) (count int64, err error) +} +type Device interface { GetDeviceByAccessToken(ctx context.Context, token string) (*api.Device, error) GetDeviceByID(ctx context.Context, localpart, deviceID string) (*api.Device, error) GetDevicesByLocalpart(ctx context.Context, localpart string) ([]api.Device, error) @@ -78,12 +68,23 @@ type Database interface { // Returns the device on success. CreateDevice(ctx context.Context, localpart string, deviceID *string, accessToken string, displayName *string, ipAddr, userAgent string) (dev *api.Device, returnErr error) UpdateDevice(ctx context.Context, localpart, deviceID string, displayName *string) error - UpdateDeviceLastSeen(ctx context.Context, localpart, deviceID, ipAddr string) error - RemoveDevice(ctx context.Context, deviceID, localpart string) error + UpdateDeviceLastSeen(ctx context.Context, localpart, deviceID, ipAddr, userAgent string) error RemoveDevices(ctx context.Context, localpart string, devices []string) error // RemoveAllDevices deleted all devices for this user. Returns the devices deleted. RemoveAllDevices(ctx context.Context, localpart, exceptDeviceID string) (devices []api.Device, err error) +} +type KeyBackup interface { + CreateKeyBackup(ctx context.Context, userID, algorithm string, authData json.RawMessage) (version string, err error) + UpdateKeyBackupAuthData(ctx context.Context, userID, version string, authData json.RawMessage) (err error) + DeleteKeyBackup(ctx context.Context, userID, version string) (exists bool, err error) + GetKeyBackup(ctx context.Context, userID, version string) (versionResult, algorithm string, authData json.RawMessage, etag string, deleted bool, err error) + UpsertBackupKeys(ctx context.Context, version, userID string, uploads []api.InternalKeyBackupSession) (count int64, etag string, err error) + GetBackupKeys(ctx context.Context, version, userID, filterRoomID, filterSessionID string) (result map[string]map[string]api.KeyBackupSession, err error) + CountBackupKeys(ctx context.Context, version, userID string) (count int64, err error) +} + +type LoginToken interface { // CreateLoginToken generates a token, stores and returns it. The lifetime is // determined by the loginTokenLifetime given to the Database constructor. CreateLoginToken(ctx context.Context, data *api.LoginTokenData) (*api.LoginTokenMetadata, error) @@ -94,21 +95,55 @@ type Database interface { // GetLoginTokenDataByToken returns the data associated with the given token. // May return sql.ErrNoRows. GetLoginTokenDataByToken(ctx context.Context, token string) (*api.LoginTokenData, error) +} - InsertNotification(ctx context.Context, localpart, eventID string, pos int64, tweaks map[string]interface{}, n *api.Notification) error - DeleteNotificationsUpTo(ctx context.Context, localpart, roomID string, pos int64) (affected bool, err error) - SetNotificationsRead(ctx context.Context, localpart, roomID string, pos int64, b bool) (affected bool, err error) - GetNotifications(ctx context.Context, localpart string, fromID int64, limit int, filter tables.NotificationFilter) ([]*api.Notification, int64, error) - GetNotificationCount(ctx context.Context, localpart string, filter tables.NotificationFilter) (int64, error) - GetRoomNotificationCounts(ctx context.Context, localpart, roomID string) (total int64, highlight int64, _ error) - DeleteOldNotifications(ctx context.Context) error +type OpenID interface { + CreateOpenIDToken(ctx context.Context, token, userID string) (exp int64, err error) + GetOpenIDTokenAttributes(ctx context.Context, token string) (*api.OpenIDTokenAttributes, error) +} +type Pusher interface { UpsertPusher(ctx context.Context, p api.Pusher, localpart string) error GetPushers(ctx context.Context, localpart string) ([]api.Pusher, error) RemovePusher(ctx context.Context, appid, pushkey, localpart string) error RemovePushers(ctx context.Context, appid, pushkey string) error } +type ThreePID interface { + SaveThreePIDAssociation(ctx context.Context, threepid, localpart, medium string) (err error) + RemoveThreePIDAssociation(ctx context.Context, threepid string, medium string) (err error) + GetLocalpartForThreePID(ctx context.Context, threepid string, medium string) (localpart string, err error) + GetThreePIDsForLocalpart(ctx context.Context, localpart string) (threepids []authtypes.ThreePID, err error) +} + +type Notification interface { + InsertNotification(ctx context.Context, localpart, eventID string, pos int64, tweaks map[string]interface{}, n *api.Notification) error + DeleteNotificationsUpTo(ctx context.Context, localpart, roomID string, pos int64) (affected bool, err error) + SetNotificationsRead(ctx context.Context, localpart, roomID string, pos int64, read bool) (affected bool, err error) + GetNotifications(ctx context.Context, localpart string, fromID int64, limit int, filter tables.NotificationFilter) ([]*api.Notification, int64, error) + GetNotificationCount(ctx context.Context, localpart string, filter tables.NotificationFilter) (int64, error) + GetRoomNotificationCounts(ctx context.Context, localpart, roomID string) (total int64, highlight int64, _ error) + DeleteOldNotifications(ctx context.Context) error +} + +type Database interface { + Account + AccountData + Device + KeyBackup + LoginToken + Notification + OpenID + Profile + Pusher + Statistics + ThreePID +} + +type Statistics interface { + UserStatistics(ctx context.Context) (*types.UserStatistics, *types.DatabaseEngine, error) +} + // Err3PIDInUse is the error returned when trying to save an association involving // a third-party identifier which is already associated to a local user. var Err3PIDInUse = errors.New("this third-party identifier is already in use") diff --git a/userapi/storage/postgres/accounts_table.go b/userapi/storage/postgres/accounts_table.go index 448337f9a..c3955434c 100644 --- a/userapi/storage/postgres/accounts_table.go +++ b/userapi/storage/postgres/accounts_table.go @@ -48,8 +48,6 @@ CREATE TABLE IF NOT EXISTS account_accounts ( -- TODO: -- upgraded_ts, devices, any email reset stuff? ); --- Create sequence for autogenerated numeric usernames -CREATE SEQUENCE IF NOT EXISTS numeric_username_seq START 1; ` const insertAccountSQL = "" + @@ -68,7 +66,7 @@ const selectPasswordHashSQL = "" + "SELECT password_hash FROM account_accounts WHERE localpart = $1 AND is_deactivated = FALSE" const selectNewNumericLocalpartSQL = "" + - "SELECT nextval('numeric_username_seq')" + "SELECT COALESCE(MAX(localpart::integer), 0) FROM account_accounts WHERE localpart ~ '^[0-9]*$'" type accountsStatements struct { insertAccountStmt *sql.Stmt @@ -196,5 +194,5 @@ func (s *accountsStatements) SelectNewNumericLocalpart( stmt = sqlutil.TxStmt(txn, stmt) } err = stmt.QueryRowContext(ctx).Scan(&id) - return + return id + 1, err } diff --git a/userapi/storage/postgres/devices_table.go b/userapi/storage/postgres/devices_table.go index 3b2332214..6decee30a 100644 --- a/userapi/storage/postgres/devices_table.go +++ b/userapi/storage/postgres/devices_table.go @@ -76,10 +76,10 @@ const selectDeviceByTokenSQL = "" + "SELECT session_id, device_id, localpart FROM device_devices WHERE access_token = $1" const selectDeviceByIDSQL = "" + - "SELECT display_name FROM device_devices WHERE localpart = $1 and device_id = $2" + "SELECT display_name, last_seen_ts, ip FROM device_devices WHERE localpart = $1 and device_id = $2" const selectDevicesByLocalpartSQL = "" + - "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2" + "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2 ORDER BY last_seen_ts DESC" const updateDeviceNameSQL = "" + "UPDATE device_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3" @@ -94,10 +94,10 @@ const deleteDevicesSQL = "" + "DELETE FROM device_devices WHERE localpart = $1 AND device_id = ANY($2)" const selectDevicesByIDSQL = "" + - "SELECT device_id, localpart, display_name FROM device_devices WHERE device_id = ANY($1)" + "SELECT device_id, localpart, display_name, last_seen_ts FROM device_devices WHERE device_id = ANY($1) ORDER BY last_seen_ts DESC" const updateDeviceLastSeen = "" + - "UPDATE device_devices SET last_seen_ts = $1, ip = $2 WHERE localpart = $3 AND device_id = $4" + "UPDATE device_devices SET last_seen_ts = $1, ip = $2, user_agent = $3 WHERE localpart = $4 AND device_id = $5" type devicesStatements struct { insertDeviceStmt *sql.Stmt @@ -225,15 +225,22 @@ func (s *devicesStatements) SelectDeviceByID( ctx context.Context, localpart, deviceID string, ) (*api.Device, error) { var dev api.Device - var displayName sql.NullString + var displayName, ip sql.NullString + var lastseenTS sql.NullInt64 stmt := s.selectDeviceByIDStmt - err := stmt.QueryRowContext(ctx, localpart, deviceID).Scan(&displayName) + err := stmt.QueryRowContext(ctx, localpart, deviceID).Scan(&displayName, &lastseenTS, &ip) if err == nil { dev.ID = deviceID dev.UserID = userutil.MakeUserID(localpart, s.serverName) if displayName.Valid { dev.DisplayName = displayName.String } + if lastseenTS.Valid { + dev.LastSeenTS = lastseenTS.Int64 + } + if ip.Valid { + dev.LastSeenIP = ip.String + } } return &dev, err } @@ -245,16 +252,20 @@ func (s *devicesStatements) SelectDevicesByID(ctx context.Context, deviceIDs []s } defer internal.CloseAndLogIfError(ctx, rows, "selectDevicesByID: rows.close() failed") var devices []api.Device + var dev api.Device + var localpart string + var lastseents sql.NullInt64 + var displayName sql.NullString for rows.Next() { - var dev api.Device - var localpart string - var displayName sql.NullString - if err := rows.Scan(&dev.ID, &localpart, &displayName); err != nil { + if err := rows.Scan(&dev.ID, &localpart, &displayName, &lastseents); err != nil { return nil, err } if displayName.Valid { dev.DisplayName = displayName.String } + if lastseents.Valid { + dev.LastSeenTS = lastseents.Int64 + } dev.UserID = userutil.MakeUserID(localpart, s.serverName) devices = append(devices, dev) } @@ -272,10 +283,10 @@ func (s *devicesStatements) SelectDevicesByLocalpart( } defer internal.CloseAndLogIfError(ctx, rows, "selectDevicesByLocalpart: rows.close() failed") + var dev api.Device + var lastseents sql.NullInt64 + var id, displayname, ip, useragent sql.NullString for rows.Next() { - var dev api.Device - var lastseents sql.NullInt64 - var id, displayname, ip, useragent sql.NullString err = rows.Scan(&id, &displayname, &lastseents, &ip, &useragent) if err != nil { return devices, err @@ -303,9 +314,9 @@ func (s *devicesStatements) SelectDevicesByLocalpart( return devices, rows.Err() } -func (s *devicesStatements) UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr string) error { +func (s *devicesStatements) UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr, userAgent string) error { lastSeenTs := time.Now().UnixNano() / 1000000 stmt := sqlutil.TxStmt(txn, s.updateDeviceLastSeenStmt) - _, err := stmt.ExecContext(ctx, lastSeenTs, ipAddr, localpart, deviceID) + _, err := stmt.ExecContext(ctx, lastSeenTs, ipAddr, userAgent, localpart, deviceID) return err } diff --git a/userapi/storage/postgres/stats_table.go b/userapi/storage/postgres/stats_table.go new file mode 100644 index 000000000..c0b317503 --- /dev/null +++ b/userapi/storage/postgres/stats_table.go @@ -0,0 +1,437 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package postgres + +import ( + "context" + "database/sql" + "time" + + "github.com/lib/pq" + "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/storage/tables" + "github.com/matrix-org/dendrite/userapi/types" + "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" +) + +const userDailyVisitsSchema = ` +CREATE TABLE IF NOT EXISTS userapi_daily_visits ( + localpart TEXT NOT NULL, + device_id TEXT NOT NULL, + timestamp BIGINT NOT NULL, + user_agent TEXT +); + +-- Device IDs and timestamp must be unique for a given user per day +CREATE UNIQUE INDEX IF NOT EXISTS userapi_daily_visits_localpart_device_timestamp_idx ON userapi_daily_visits(localpart, device_id, timestamp); +CREATE INDEX IF NOT EXISTS userapi_daily_visits_timestamp_idx ON userapi_daily_visits(timestamp); +CREATE INDEX IF NOT EXISTS userapi_daily_visits_localpart_timestamp_idx ON userapi_daily_visits(localpart, timestamp); +` + +const countUsersLastSeenAfterSQL = "" + + "SELECT COUNT(*) FROM (" + + " SELECT localpart FROM device_devices WHERE last_seen_ts > $1 " + + " GROUP BY localpart" + + " ) u" + +// Note on the following countR30UsersSQL and countR30UsersV2SQL: The different checks are intentional. +// This is to ensure the values reported by Dendrite are the same as by Synapse. +// Queries are taken from: https://github.com/matrix-org/synapse/blob/9ce51a47f6e37abd0a1275281806399d874eb026/synapse/storage/databases/main/stats.py + +/* +R30Users counts the number of 30 day retained users, defined as: +- Users who have created their accounts more than 30 days ago +- Where last seen at most 30 days ago +- Where account creation and last_seen are > 30 days apart +*/ +const countR30UsersSQL = ` +SELECT platform, COUNT(*) FROM ( + SELECT users.localpart, platform, users.created_ts, MAX(uip.last_seen_ts) + FROM account_accounts users + INNER JOIN + (SELECT + localpart, last_seen_ts, + CASE + WHEN user_agent LIKE '%%Android%%' THEN 'android' + WHEN user_agent LIKE '%%iOS%%' THEN 'ios' + WHEN user_agent LIKE '%%Electron%%' THEN 'electron' + WHEN user_agent LIKE '%%Mozilla%%' THEN 'web' + WHEN user_agent LIKE '%%Gecko%%' THEN 'web' + ELSE 'unknown' + END + AS platform + FROM device_devices + ) uip + ON users.localpart = uip.localpart + AND users.account_type <> 4 + AND users.created_ts < $1 + AND uip.last_seen_ts > $1 + AND (uip.last_seen_ts) - users.created_ts > $2 + GROUP BY users.localpart, platform, users.created_ts + ) u GROUP BY PLATFORM +` + +/* +R30UsersV2 counts the number of 30 day retained users, defined as users that: +- Appear more than once in the past 60 days +- Have more than 30 days between the most and least recent appearances that occurred in the past 60 days. +*/ +const countR30UsersV2SQL = ` +SELECT + client_type, + count(client_type) +FROM + ( + SELECT + localpart, + CASE + WHEN + LOWER(user_agent) LIKE '%%riot%%' OR + LOWER(user_agent) LIKE '%%element%%' + THEN CASE + WHEN LOWER(user_agent) LIKE '%%electron%%' THEN 'electron' + WHEN LOWER(user_agent) LIKE '%%android%%' THEN 'android' + WHEN LOWER(user_agent) LIKE '%%ios%%' THEN 'ios' + ELSE 'unknown' + END + WHEN LOWER(user_agent) LIKE '%%mozilla%%' OR LOWER(user_agent) LIKE '%%gecko%%' THEN 'web' + ELSE 'unknown' + END as client_type + FROM userapi_daily_visits + WHERE timestamp > $1 AND timestamp < $2 + GROUP BY localpart, client_type + HAVING max(timestamp) - min(timestamp) > $3 + ) AS temp +GROUP BY client_type +` + +const countUserByAccountTypeSQL = ` +SELECT COUNT(*) FROM account_accounts WHERE account_type = ANY($1) +` + +// $1 = All non guest AccountType IDs +// $2 = Guest AccountType +const countRegisteredUserByTypeStmt = ` +SELECT user_type, COUNT(*) AS count FROM ( + SELECT + CASE + WHEN account_type = ANY($1) AND appservice_id IS NULL THEN 'native' + WHEN account_type = $2 AND appservice_id IS NULL THEN 'guest' + WHEN account_type = ANY($1) AND appservice_id IS NOT NULL THEN 'bridged' + END AS user_type + FROM account_accounts + WHERE created_ts > $3 +) AS t GROUP BY user_type +` + +// account_type 1 = users; 3 = admins +const updateUserDailyVisitsSQL = ` +INSERT INTO userapi_daily_visits(localpart, device_id, timestamp, user_agent) + SELECT u.localpart, u.device_id, $1, MAX(u.user_agent) + FROM device_devices AS u + LEFT JOIN ( + SELECT localpart, device_id, timestamp FROM userapi_daily_visits + WHERE timestamp = $1 + ) udv + ON u.localpart = udv.localpart AND u.device_id = udv.device_id + INNER JOIN device_devices d ON d.localpart = u.localpart + INNER JOIN account_accounts a ON a.localpart = u.localpart + WHERE $2 <= d.last_seen_ts AND d.last_seen_ts < $3 + AND a.account_type in (1, 3) + GROUP BY u.localpart, u.device_id +ON CONFLICT (localpart, device_id, timestamp) DO NOTHING +; +` + +const queryDBEngineVersion = "SHOW server_version;" + +type statsStatements struct { + serverName gomatrixserverlib.ServerName + lastUpdate time.Time + countUsersLastSeenAfterStmt *sql.Stmt + countR30UsersStmt *sql.Stmt + countR30UsersV2Stmt *sql.Stmt + updateUserDailyVisitsStmt *sql.Stmt + countUserByAccountTypeStmt *sql.Stmt + countRegisteredUserByTypeStmt *sql.Stmt + dbEngineVersionStmt *sql.Stmt +} + +func NewPostgresStatsTable(db *sql.DB, serverName gomatrixserverlib.ServerName) (tables.StatsTable, error) { + s := &statsStatements{ + serverName: serverName, + lastUpdate: time.Now(), + } + + _, err := db.Exec(userDailyVisitsSchema) + if err != nil { + return nil, err + } + go s.startTimers() + return s, sqlutil.StatementList{ + {&s.countUsersLastSeenAfterStmt, countUsersLastSeenAfterSQL}, + {&s.countR30UsersStmt, countR30UsersSQL}, + {&s.countR30UsersV2Stmt, countR30UsersV2SQL}, + {&s.updateUserDailyVisitsStmt, updateUserDailyVisitsSQL}, + {&s.countUserByAccountTypeStmt, countUserByAccountTypeSQL}, + {&s.countRegisteredUserByTypeStmt, countRegisteredUserByTypeStmt}, + {&s.dbEngineVersionStmt, queryDBEngineVersion}, + }.Prepare(db) +} + +func (s *statsStatements) startTimers() { + var updateStatsFunc func() + updateStatsFunc = func() { + logrus.Infof("Executing UpdateUserDailyVisits") + if err := s.UpdateUserDailyVisits(context.Background(), nil, time.Now(), s.lastUpdate); err != nil { + logrus.WithError(err).Error("failed to update daily user visits") + } + time.AfterFunc(time.Hour*3, updateStatsFunc) + } + time.AfterFunc(time.Minute*5, updateStatsFunc) +} + +func (s *statsStatements) allUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUserByAccountTypeStmt) + err = stmt.QueryRowContext(ctx, + pq.Int64Array{ + int64(api.AccountTypeUser), + int64(api.AccountTypeGuest), + int64(api.AccountTypeAdmin), + int64(api.AccountTypeAppService), + }, + ).Scan(&result) + return +} + +func (s *statsStatements) nonBridgedUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUserByAccountTypeStmt) + err = stmt.QueryRowContext(ctx, + pq.Int64Array{ + int64(api.AccountTypeUser), + int64(api.AccountTypeGuest), + int64(api.AccountTypeAdmin), + }, + ).Scan(&result) + return +} + +func (s *statsStatements) registeredUserByType(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + stmt := sqlutil.TxStmt(txn, s.countRegisteredUserByTypeStmt) + registeredAfter := time.Now().AddDate(0, 0, -30) + + rows, err := stmt.QueryContext(ctx, + pq.Int64Array{ + int64(api.AccountTypeUser), + int64(api.AccountTypeAdmin), + int64(api.AccountTypeAppService), + }, + api.AccountTypeGuest, + gomatrixserverlib.AsTimestamp(registeredAfter), + ) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "RegisteredUserByType: failed to close rows") + + var userType string + var count int64 + var result = make(map[string]int64) + for rows.Next() { + if err = rows.Scan(&userType, &count); err != nil { + return nil, err + } + result[userType] = count + } + + return result, rows.Err() +} + +func (s *statsStatements) dailyUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUsersLastSeenAfterStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -1) + err = stmt.QueryRowContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + ).Scan(&result) + return +} + +func (s *statsStatements) monthlyUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUsersLastSeenAfterStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -30) + err = stmt.QueryRowContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + ).Scan(&result) + return +} + +/* +R30Users counts the number of 30 day retained users, defined as: +- Users who have created their accounts more than 30 days ago +- Where last seen at most 30 days ago +- Where account creation and last_seen are > 30 days apart +*/ +func (s *statsStatements) r30Users(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + stmt := sqlutil.TxStmt(txn, s.countR30UsersStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -30) + diff := time.Hour * 24 * 30 + + rows, err := stmt.QueryContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + diff.Milliseconds(), + ) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "R30Users: failed to close rows") + + var platform string + var count int64 + var result = make(map[string]int64) + for rows.Next() { + if err = rows.Scan(&platform, &count); err != nil { + return nil, err + } + if platform == "unknown" { + continue + } + result["all"] += count + result[platform] = count + } + + return result, rows.Err() +} + +/* +R30UsersV2 counts the number of 30 day retained users, defined as users that: +- Appear more than once in the past 60 days +- Have more than 30 days between the most and least recent appearances that occurred in the past 60 days. +*/ +func (s *statsStatements) r30UsersV2(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + stmt := sqlutil.TxStmt(txn, s.countR30UsersV2Stmt) + sixtyDaysAgo := time.Now().AddDate(0, 0, -60) + diff := time.Hour * 24 * 30 + tomorrow := time.Now().Add(time.Hour * 24) + + rows, err := stmt.QueryContext(ctx, + gomatrixserverlib.AsTimestamp(sixtyDaysAgo), + gomatrixserverlib.AsTimestamp(tomorrow), + diff.Milliseconds(), + ) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "R30UsersV2: failed to close rows") + + var platform string + var count int64 + var result = map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + } + for rows.Next() { + if err = rows.Scan(&platform, &count); err != nil { + return nil, err + } + if _, ok := result[platform]; !ok { + continue + } + result["all"] += count + result[platform] = count + } + + return result, rows.Err() +} + +// UserStatistics collects some information about users on this instance. +// Returns the stats itself as well as the database engine version and type. +// On error, returns the stats collected up to the error. +func (s *statsStatements) UserStatistics(ctx context.Context, txn *sql.Tx) (*types.UserStatistics, *types.DatabaseEngine, error) { + var ( + stats = &types.UserStatistics{ + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + }, + R30Users: map[string]int64{}, + RegisteredUsersByType: map[string]int64{}, + } + dbEngine = &types.DatabaseEngine{Engine: "Postgres", Version: "unknown"} + err error + ) + stats.AllUsers, err = s.allUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.DailyUsers, err = s.dailyUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.MonthlyUsers, err = s.monthlyUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.R30Users, err = s.r30Users(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.R30UsersV2, err = s.r30UsersV2(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.NonBridgedUsers, err = s.nonBridgedUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.RegisteredUsersByType, err = s.registeredUserByType(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + + stmt := sqlutil.TxStmt(txn, s.dbEngineVersionStmt) + err = stmt.QueryRowContext(ctx).Scan(&dbEngine.Version) + return stats, dbEngine, err +} + +func (s *statsStatements) UpdateUserDailyVisits( + ctx context.Context, txn *sql.Tx, + startTime, lastUpdate time.Time, +) error { + stmt := sqlutil.TxStmt(txn, s.updateUserDailyVisitsStmt) + startTime = startTime.Truncate(time.Hour * 24) + + // edge case + if startTime.After(s.lastUpdate) { + startTime = startTime.AddDate(0, 0, -1) + } + _, err := stmt.ExecContext(ctx, + gomatrixserverlib.AsTimestamp(startTime), + gomatrixserverlib.AsTimestamp(lastUpdate), + gomatrixserverlib.AsTimestamp(time.Now()), + ) + if err == nil { + s.lastUpdate = time.Now() + } + return err +} diff --git a/userapi/storage/postgres/storage.go b/userapi/storage/postgres/storage.go index 3f33eed73..7d3b9b6a5 100644 --- a/userapi/storage/postgres/storage.go +++ b/userapi/storage/postgres/storage.go @@ -21,6 +21,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/storage/shared" @@ -29,8 +30,8 @@ import ( ) // NewDatabase creates a new accounts and profiles database -func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) { - db, err := sqlutil.Open(dbProperties) +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewDummyWriter()) if err != nil { return nil, err } @@ -79,6 +80,10 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver if err != nil { return nil, fmt.Errorf("NewPostgresNotificationTable: %w", err) } + statsTable, err := NewPostgresStatsTable(db, serverName) + if err != nil { + return nil, fmt.Errorf("NewPostgresStatsTable: %w", err) + } return &shared.Database{ AccountDatas: accountDataTable, Accounts: accountsTable, @@ -91,9 +96,10 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver ThreePIDs: threePIDTable, Pushers: pusherTable, Notifications: notificationsTable, + Stats: statsTable, ServerName: serverName, DB: db, - Writer: sqlutil.NewDummyWriter(), + Writer: writer, LoginTokenLifetime: loginTokenLifetime, BcryptCost: bcryptCost, OpenIDTokenLifetimeMS: openIDTokenLifetimeMS, diff --git a/userapi/storage/shared/storage.go b/userapi/storage/shared/storage.go index 72ae96ecc..0cf713dac 100644 --- a/userapi/storage/shared/storage.go +++ b/userapi/storage/shared/storage.go @@ -26,6 +26,7 @@ import ( "strings" "time" + "github.com/matrix-org/dendrite/userapi/types" "github.com/matrix-org/gomatrixserverlib" "golang.org/x/crypto/bcrypt" @@ -51,6 +52,7 @@ type Database struct { LoginTokens tables.LoginTokenTable Notifications tables.NotificationTable Pushers tables.PusherTable + Stats tables.StatsTable LoginTokenLifetime time.Duration ServerName gomatrixserverlib.ServerName BcryptCost int @@ -577,21 +579,6 @@ func (d *Database) UpdateDevice( }) } -// RemoveDevice revokes a device by deleting the entry in the database -// matching with the given device ID and user ID localpart. -// If the device doesn't exist, it will not return an error -// If something went wrong during the deletion, it will return the SQL error. -func (d *Database) RemoveDevice( - ctx context.Context, deviceID, localpart string, -) error { - return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { - if err := d.Devices.DeleteDevice(ctx, txn, deviceID, localpart); err != sql.ErrNoRows { - return err - } - return nil - }) -} - // RemoveDevices revokes one or more devices by deleting the entry in the database // matching with the given device IDs and user ID localpart. // If the devices don't exist, it will not return an error @@ -626,10 +613,10 @@ func (d *Database) RemoveAllDevices( return } -// UpdateDeviceLastSeen updates a the last seen timestamp and the ip address -func (d *Database) UpdateDeviceLastSeen(ctx context.Context, localpart, deviceID, ipAddr string) error { +// UpdateDeviceLastSeen updates a last seen timestamp and the ip address. +func (d *Database) UpdateDeviceLastSeen(ctx context.Context, localpart, deviceID, ipAddr, userAgent string) error { return d.Writer.Do(d.DB, nil, func(txn *sql.Tx) error { - return d.Devices.UpdateDeviceLastSeen(ctx, txn, localpart, deviceID, ipAddr) + return d.Devices.UpdateDeviceLastSeen(ctx, txn, localpart, deviceID, ipAddr, userAgent) }) } @@ -771,3 +758,8 @@ func (d *Database) RemovePushers( return d.Pushers.DeletePushers(ctx, txn, appid, pushkey) }) } + +// UserStatistics populates types.UserStatistics, used in reports. +func (d *Database) UserStatistics(ctx context.Context) (*types.UserStatistics, *types.DatabaseEngine, error) { + return d.Stats.UserStatistics(ctx, nil) +} diff --git a/userapi/storage/sqlite3/accounts_table.go b/userapi/storage/sqlite3/accounts_table.go index b817087f4..eded371fa 100644 --- a/userapi/storage/sqlite3/accounts_table.go +++ b/userapi/storage/sqlite3/accounts_table.go @@ -66,7 +66,7 @@ const selectPasswordHashSQL = "" + "SELECT password_hash FROM account_accounts WHERE localpart = $1 AND is_deactivated = 0" const selectNewNumericLocalpartSQL = "" + - "SELECT COUNT(localpart) FROM account_accounts" + "SELECT COALESCE(MAX(CAST(localpart AS INT)), 0) FROM account_accounts WHERE CAST(localpart AS INT) <> 0" type accountsStatements struct { db *sql.DB @@ -139,6 +139,7 @@ func (s *accountsStatements) InsertAccount( UserID: userutil.MakeUserID(localpart, s.serverName), ServerName: s.serverName, AppServiceID: appserviceID, + AccountType: accountType, }, nil } @@ -195,5 +196,8 @@ func (s *accountsStatements) SelectNewNumericLocalpart( stmt = sqlutil.TxStmt(txn, stmt) } err = stmt.QueryRowContext(ctx).Scan(&id) - return + if err == sql.ErrNoRows { + return 1, nil + } + return id + 1, err } diff --git a/userapi/storage/sqlite3/devices_table.go b/userapi/storage/sqlite3/devices_table.go index a12087959..fed785d25 100644 --- a/userapi/storage/sqlite3/devices_table.go +++ b/userapi/storage/sqlite3/devices_table.go @@ -61,10 +61,10 @@ const selectDeviceByTokenSQL = "" + "SELECT session_id, device_id, localpart FROM device_devices WHERE access_token = $1" const selectDeviceByIDSQL = "" + - "SELECT display_name FROM device_devices WHERE localpart = $1 and device_id = $2" + "SELECT display_name, last_seen_ts, ip FROM device_devices WHERE localpart = $1 and device_id = $2" const selectDevicesByLocalpartSQL = "" + - "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2" + "SELECT device_id, display_name, last_seen_ts, ip, user_agent FROM device_devices WHERE localpart = $1 AND device_id != $2 ORDER BY last_seen_ts DESC" const updateDeviceNameSQL = "" + "UPDATE device_devices SET display_name = $1 WHERE localpart = $2 AND device_id = $3" @@ -79,10 +79,10 @@ const deleteDevicesSQL = "" + "DELETE FROM device_devices WHERE localpart = $1 AND device_id IN ($2)" const selectDevicesByIDSQL = "" + - "SELECT device_id, localpart, display_name FROM device_devices WHERE device_id IN ($1)" + "SELECT device_id, localpart, display_name, last_seen_ts FROM device_devices WHERE device_id IN ($1) ORDER BY last_seen_ts DESC" const updateDeviceLastSeen = "" + - "UPDATE device_devices SET last_seen_ts = $1, ip = $2 WHERE localpart = $3 AND device_id = $4" + "UPDATE device_devices SET last_seen_ts = $1, ip = $2, user_agent = $3 WHERE localpart = $4 AND device_id = $5" type devicesStatements struct { db *sql.DB @@ -222,15 +222,22 @@ func (s *devicesStatements) SelectDeviceByID( ctx context.Context, localpart, deviceID string, ) (*api.Device, error) { var dev api.Device - var displayName sql.NullString + var displayName, ip sql.NullString stmt := s.selectDeviceByIDStmt - err := stmt.QueryRowContext(ctx, localpart, deviceID).Scan(&displayName) + var lastseenTS sql.NullInt64 + err := stmt.QueryRowContext(ctx, localpart, deviceID).Scan(&displayName, &lastseenTS, &ip) if err == nil { dev.ID = deviceID dev.UserID = userutil.MakeUserID(localpart, s.serverName) if displayName.Valid { dev.DisplayName = displayName.String } + if lastseenTS.Valid { + dev.LastSeenTS = lastseenTS.Int64 + } + if ip.Valid { + dev.LastSeenIP = ip.String + } } return &dev, err } @@ -245,10 +252,10 @@ func (s *devicesStatements) SelectDevicesByLocalpart( return devices, err } + var dev api.Device + var lastseents sql.NullInt64 + var id, displayname, ip, useragent sql.NullString for rows.Next() { - var dev api.Device - var lastseents sql.NullInt64 - var id, displayname, ip, useragent sql.NullString err = rows.Scan(&id, &displayname, &lastseents, &ip, &useragent) if err != nil { return devices, err @@ -289,25 +296,29 @@ func (s *devicesStatements) SelectDevicesByID(ctx context.Context, deviceIDs []s } defer internal.CloseAndLogIfError(ctx, rows, "selectDevicesByID: rows.close() failed") var devices []api.Device + var dev api.Device + var localpart string + var displayName sql.NullString + var lastseents sql.NullInt64 for rows.Next() { - var dev api.Device - var localpart string - var displayName sql.NullString - if err := rows.Scan(&dev.ID, &localpart, &displayName); err != nil { + if err := rows.Scan(&dev.ID, &localpart, &displayName, &lastseents); err != nil { return nil, err } if displayName.Valid { dev.DisplayName = displayName.String } + if lastseents.Valid { + dev.LastSeenTS = lastseents.Int64 + } dev.UserID = userutil.MakeUserID(localpart, s.serverName) devices = append(devices, dev) } return devices, rows.Err() } -func (s *devicesStatements) UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr string) error { +func (s *devicesStatements) UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr, userAgent string) error { lastSeenTs := time.Now().UnixNano() / 1000000 stmt := sqlutil.TxStmt(txn, s.updateDeviceLastSeenStmt) - _, err := stmt.ExecContext(ctx, lastSeenTs, ipAddr, localpart, deviceID) + _, err := stmt.ExecContext(ctx, lastSeenTs, ipAddr, userAgent, localpart, deviceID) return err } diff --git a/userapi/storage/sqlite3/stats_table.go b/userapi/storage/sqlite3/stats_table.go new file mode 100644 index 000000000..e00ed417b --- /dev/null +++ b/userapi/storage/sqlite3/stats_table.go @@ -0,0 +1,452 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sqlite3 + +import ( + "context" + "database/sql" + "strings" + "time" + + "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/storage/tables" + "github.com/matrix-org/dendrite/userapi/types" + "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" +) + +const userDailyVisitsSchema = ` +CREATE TABLE IF NOT EXISTS userapi_daily_visits ( + localpart TEXT NOT NULL, + device_id TEXT NOT NULL, + timestamp BIGINT NOT NULL, + user_agent TEXT +); + +-- Device IDs and timestamp must be unique for a given user per day +CREATE UNIQUE INDEX IF NOT EXISTS userapi_daily_visits_localpart_device_timestamp_idx ON userapi_daily_visits(localpart, device_id, timestamp); +CREATE INDEX IF NOT EXISTS userapi_daily_visits_timestamp_idx ON userapi_daily_visits(timestamp); +CREATE INDEX IF NOT EXISTS userapi_daily_visits_localpart_timestamp_idx ON userapi_daily_visits(localpart, timestamp); +` + +const countUsersLastSeenAfterSQL = "" + + "SELECT COUNT(*) FROM (" + + " SELECT localpart FROM device_devices WHERE last_seen_ts > $1 " + + " GROUP BY localpart" + + " ) u" + +// Note on the following countR30UsersSQL and countR30UsersV2SQL: The different checks are intentional. +// This is to ensure the values reported by Dendrite are the same as by Synapse. +// Queries are taken from: https://github.com/matrix-org/synapse/blob/9ce51a47f6e37abd0a1275281806399d874eb026/synapse/storage/databases/main/stats.py + +/* +R30Users counts the number of 30 day retained users, defined as: +- Users who have created their accounts more than 30 days ago +- Where last seen at most 30 days ago +- Where account creation and last_seen are > 30 days apart +*/ +const countR30UsersSQL = ` +SELECT platform, COUNT(*) FROM ( + SELECT users.localpart, platform, users.created_ts, MAX(uip.last_seen_ts) + FROM account_accounts users + INNER JOIN + (SELECT + localpart, last_seen_ts, + CASE + WHEN user_agent LIKE '%%Android%%' THEN 'android' + WHEN user_agent LIKE '%%iOS%%' THEN 'ios' + WHEN user_agent LIKE '%%Electron%%' THEN 'electron' + WHEN user_agent LIKE '%%Mozilla%%' THEN 'web' + WHEN user_agent LIKE '%%Gecko%%' THEN 'web' + ELSE 'unknown' + END + AS platform + FROM device_devices + ) uip + ON users.localpart = uip.localpart + AND users.account_type <> 4 + AND users.created_ts < $1 + AND uip.last_seen_ts > $2 + AND (uip.last_seen_ts) - users.created_ts > $3 + GROUP BY users.localpart, platform, users.created_ts + ) u GROUP BY PLATFORM +` + +// Note on the following countR30UsersSQL and countR30UsersV2SQL: The different checks are intentional. +// This is to ensure the values reported are the same as Synapse reports. +// Queries are taken from: https://github.com/matrix-org/synapse/blob/9ce51a47f6e37abd0a1275281806399d874eb026/synapse/storage/databases/main/stats.py + +/* +R30UsersV2 counts the number of 30 day retained users, defined as users that: +- Appear more than once in the past 60 days +- Have more than 30 days between the most and least recent appearances that occurred in the past 60 days. +*/ +const countR30UsersV2SQL = ` +SELECT + client_type, + count(client_type) +FROM + ( + SELECT + localpart, + CASE + WHEN + LOWER(user_agent) LIKE '%%riot%%' OR + LOWER(user_agent) LIKE '%%element%%' + THEN CASE + WHEN LOWER(user_agent) LIKE '%%electron%%' THEN 'electron' + WHEN LOWER(user_agent) LIKE '%%android%%' THEN 'android' + WHEN LOWER(user_agent) LIKE '%%ios%%' THEN 'ios' + ELSE 'unknown' + END + WHEN LOWER(user_agent) LIKE '%%mozilla%%' OR LOWER(user_agent) LIKE '%%gecko%%' THEN 'web' + ELSE 'unknown' + END as client_type + FROM userapi_daily_visits + WHERE timestamp > $1 AND timestamp < $2 + GROUP BY localpart, client_type + HAVING max(timestamp) - min(timestamp) > $3 + ) AS temp +GROUP BY client_type +` + +const countUserByAccountTypeSQL = ` +SELECT COUNT(*) FROM account_accounts WHERE account_type IN ($1) +` + +// $1 = Guest AccountType +// $3 & $4 = All non guest AccountType IDs +const countRegisteredUserByTypeSQL = ` +SELECT user_type, COUNT(*) AS count FROM ( + SELECT + CASE + WHEN account_type IN ($1) AND appservice_id IS NULL THEN 'native' + WHEN account_type = $4 AND appservice_id IS NULL THEN 'guest' + WHEN account_type IN ($5) AND appservice_id IS NOT NULL THEN 'bridged' + END AS user_type + FROM account_accounts + WHERE created_ts > $8 +) AS t GROUP BY user_type +` + +// account_type 1 = users; 3 = admins +const updateUserDailyVisitsSQL = ` +INSERT INTO userapi_daily_visits(localpart, device_id, timestamp, user_agent) + SELECT u.localpart, u.device_id, $1, MAX(u.user_agent) + FROM device_devices AS u + LEFT JOIN ( + SELECT localpart, device_id, timestamp FROM userapi_daily_visits + WHERE timestamp = $1 + ) udv + ON u.localpart = udv.localpart AND u.device_id = udv.device_id + INNER JOIN device_devices d ON d.localpart = u.localpart + INNER JOIN account_accounts a ON a.localpart = u.localpart + WHERE $2 <= d.last_seen_ts AND d.last_seen_ts < $3 + AND a.account_type in (1, 3) + GROUP BY u.localpart, u.device_id +ON CONFLICT (localpart, device_id, timestamp) DO NOTHING +; +` + +const queryDBEngineVersion = "select sqlite_version();" + +type statsStatements struct { + serverName gomatrixserverlib.ServerName + db *sql.DB + lastUpdate time.Time + countUsersLastSeenAfterStmt *sql.Stmt + countR30UsersStmt *sql.Stmt + countR30UsersV2Stmt *sql.Stmt + updateUserDailyVisitsStmt *sql.Stmt + countUserByAccountTypeStmt *sql.Stmt + countRegisteredUserByTypeStmt *sql.Stmt + dbEngineVersionStmt *sql.Stmt +} + +func NewSQLiteStatsTable(db *sql.DB, serverName gomatrixserverlib.ServerName) (tables.StatsTable, error) { + s := &statsStatements{ + serverName: serverName, + lastUpdate: time.Now(), + db: db, + } + + _, err := db.Exec(userDailyVisitsSchema) + if err != nil { + return nil, err + } + go s.startTimers() + return s, sqlutil.StatementList{ + {&s.countUsersLastSeenAfterStmt, countUsersLastSeenAfterSQL}, + {&s.countR30UsersStmt, countR30UsersSQL}, + {&s.countR30UsersV2Stmt, countR30UsersV2SQL}, + {&s.updateUserDailyVisitsStmt, updateUserDailyVisitsSQL}, + {&s.countUserByAccountTypeStmt, countUserByAccountTypeSQL}, + {&s.countRegisteredUserByTypeStmt, countRegisteredUserByTypeSQL}, + {&s.dbEngineVersionStmt, queryDBEngineVersion}, + }.Prepare(db) +} + +func (s *statsStatements) startTimers() { + var updateStatsFunc func() + updateStatsFunc = func() { + logrus.Infof("Executing UpdateUserDailyVisits") + if err := s.UpdateUserDailyVisits(context.Background(), nil, time.Now(), s.lastUpdate); err != nil { + logrus.WithError(err).Error("failed to update daily user visits") + } + time.AfterFunc(time.Hour*3, updateStatsFunc) + } + time.AfterFunc(time.Minute*5, updateStatsFunc) +} + +func (s *statsStatements) allUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + query := strings.Replace(countUserByAccountTypeSQL, "($1)", sqlutil.QueryVariadic(4), 1) + queryStmt, err := s.db.Prepare(query) + if err != nil { + return 0, err + } + stmt := sqlutil.TxStmt(txn, queryStmt) + err = stmt.QueryRowContext(ctx, + 1, 2, 3, 4, + ).Scan(&result) + return +} + +func (s *statsStatements) nonBridgedUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + query := strings.Replace(countUserByAccountTypeSQL, "($1)", sqlutil.QueryVariadic(3), 1) + queryStmt, err := s.db.Prepare(query) + if err != nil { + return 0, err + } + stmt := sqlutil.TxStmt(txn, queryStmt) + err = stmt.QueryRowContext(ctx, + 1, 2, 3, + ).Scan(&result) + return +} + +func (s *statsStatements) registeredUserByType(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + // $1 = Guest AccountType; $2 = timestamp + // $3 & $4 = All non guest AccountType IDs + nonGuests := []api.AccountType{api.AccountTypeUser, api.AccountTypeAdmin, api.AccountTypeAppService} + countSQL := strings.Replace(countRegisteredUserByTypeSQL, "($1)", sqlutil.QueryVariadicOffset(len(nonGuests), 0), 1) + countSQL = strings.Replace(countSQL, "($5)", sqlutil.QueryVariadicOffset(len(nonGuests), 1+len(nonGuests)), 1) + queryStmt, err := s.db.Prepare(countSQL) + if err != nil { + return nil, err + } + stmt := sqlutil.TxStmt(txn, queryStmt) + registeredAfter := time.Now().AddDate(0, 0, -30) + + params := make([]interface{}, len(nonGuests)*2+2) + // nonGuests is used twice + for i, v := range nonGuests { + params[i] = v // i: 0 1 2 => ($1, $2, $3) + params[i+1+len(nonGuests)] = v // i: 4 5 6 => ($5, $6, $7) + } + params[3] = api.AccountTypeGuest // $4 + params[7] = gomatrixserverlib.AsTimestamp(registeredAfter) // $8 + + rows, err := stmt.QueryContext(ctx, params...) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "RegisteredUserByType: failed to close rows") + + var userType string + var count int64 + var result = make(map[string]int64) + for rows.Next() { + if err = rows.Scan(&userType, &count); err != nil { + return nil, err + } + result[userType] = count + } + + return result, rows.Err() +} + +func (s *statsStatements) dailyUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUsersLastSeenAfterStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -1) + err = stmt.QueryRowContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + ).Scan(&result) + return +} + +func (s *statsStatements) monthlyUsers(ctx context.Context, txn *sql.Tx) (result int64, err error) { + stmt := sqlutil.TxStmt(txn, s.countUsersLastSeenAfterStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -30) + err = stmt.QueryRowContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + ).Scan(&result) + return +} + +/* R30Users counts the number of 30 day retained users, defined as: +- Users who have created their accounts more than 30 days ago +- Where last seen at most 30 days ago +- Where account creation and last_seen are > 30 days apart +*/ +func (s *statsStatements) r30Users(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + stmt := sqlutil.TxStmt(txn, s.countR30UsersStmt) + lastSeenAfter := time.Now().AddDate(0, 0, -30) + diff := time.Hour * 24 * 30 + + rows, err := stmt.QueryContext(ctx, + gomatrixserverlib.AsTimestamp(lastSeenAfter), + gomatrixserverlib.AsTimestamp(lastSeenAfter), + diff.Milliseconds(), + ) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "R30Users: failed to close rows") + + var platform string + var count int64 + var result = make(map[string]int64) + for rows.Next() { + if err = rows.Scan(&platform, &count); err != nil { + return nil, err + } + if platform == "unknown" { + continue + } + result["all"] += count + result[platform] = count + } + + return result, rows.Err() +} + +/* R30UsersV2 counts the number of 30 day retained users, defined as users that: +- Appear more than once in the past 60 days +- Have more than 30 days between the most and least recent appearances that occurred in the past 60 days. +*/ +func (s *statsStatements) r30UsersV2(ctx context.Context, txn *sql.Tx) (map[string]int64, error) { + stmt := sqlutil.TxStmt(txn, s.countR30UsersV2Stmt) + sixtyDaysAgo := time.Now().AddDate(0, 0, -60) + diff := time.Hour * 24 * 30 + tomorrow := time.Now().Add(time.Hour * 24) + + rows, err := stmt.QueryContext(ctx, + gomatrixserverlib.AsTimestamp(sixtyDaysAgo), + gomatrixserverlib.AsTimestamp(tomorrow), + diff.Milliseconds(), + ) + if err != nil { + return nil, err + } + defer internal.CloseAndLogIfError(ctx, rows, "R30UsersV2: failed to close rows") + + var platform string + var count int64 + var result = map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + } + for rows.Next() { + if err = rows.Scan(&platform, &count); err != nil { + return nil, err + } + if _, ok := result[platform]; !ok { + continue + } + result["all"] += count + result[platform] = count + } + return result, rows.Err() +} + +// UserStatistics collects some information about users on this instance. +// Returns the stats itself as well as the database engine version and type. +// On error, returns the stats collected up to the error. +func (s *statsStatements) UserStatistics(ctx context.Context, txn *sql.Tx) (*types.UserStatistics, *types.DatabaseEngine, error) { + var ( + stats = &types.UserStatistics{ + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + }, + R30Users: map[string]int64{}, + RegisteredUsersByType: map[string]int64{}, + } + dbEngine = &types.DatabaseEngine{Engine: "SQLite", Version: "unknown"} + err error + ) + stats.AllUsers, err = s.allUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.DailyUsers, err = s.dailyUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.MonthlyUsers, err = s.monthlyUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.R30Users, err = s.r30Users(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.R30UsersV2, err = s.r30UsersV2(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.NonBridgedUsers, err = s.nonBridgedUsers(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + stats.RegisteredUsersByType, err = s.registeredUserByType(ctx, txn) + if err != nil { + return stats, dbEngine, err + } + + stmt := sqlutil.TxStmt(txn, s.dbEngineVersionStmt) + err = stmt.QueryRowContext(ctx).Scan(&dbEngine.Version) + return stats, dbEngine, err +} + +func (s *statsStatements) UpdateUserDailyVisits( + ctx context.Context, txn *sql.Tx, + startTime, lastUpdate time.Time, +) error { + stmt := sqlutil.TxStmt(txn, s.updateUserDailyVisitsStmt) + startTime = startTime.Truncate(time.Hour * 24) + + // edge case + if startTime.After(s.lastUpdate) { + startTime = startTime.AddDate(0, 0, -1) + } + _, err := stmt.ExecContext(ctx, + gomatrixserverlib.AsTimestamp(startTime), + gomatrixserverlib.AsTimestamp(lastUpdate), + gomatrixserverlib.AsTimestamp(time.Now()), + ) + if err == nil { + s.lastUpdate = time.Now() + } + return err +} diff --git a/userapi/storage/sqlite3/storage.go b/userapi/storage/sqlite3/storage.go index 7b3dfd5b2..78b7ce588 100644 --- a/userapi/storage/sqlite3/storage.go +++ b/userapi/storage/sqlite3/storage.go @@ -21,14 +21,15 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/storage/shared" ) // NewDatabase creates a new accounts and profiles database -func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) { - db, err := sqlutil.Open(dbProperties) +func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (*shared.Database, error) { + db, writer, err := base.DatabaseConnection(dbProperties, sqlutil.NewExclusiveWriter()) if err != nil { return nil, err } @@ -77,6 +78,10 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver if err != nil { return nil, fmt.Errorf("NewPostgresNotificationTable: %w", err) } + statsTable, err := NewSQLiteStatsTable(db, serverName) + if err != nil { + return nil, fmt.Errorf("NewSQLiteStatsTable: %w", err) + } return &shared.Database{ AccountDatas: accountDataTable, Accounts: accountsTable, @@ -89,9 +94,10 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver ThreePIDs: threePIDTable, Pushers: pusherTable, Notifications: notificationsTable, + Stats: statsTable, ServerName: serverName, DB: db, - Writer: sqlutil.NewExclusiveWriter(), + Writer: writer, LoginTokenLifetime: loginTokenLifetime, BcryptCost: bcryptCost, OpenIDTokenLifetimeMS: openIDTokenLifetimeMS, diff --git a/userapi/storage/storage.go b/userapi/storage/storage.go index f372fe7dc..42221e752 100644 --- a/userapi/storage/storage.go +++ b/userapi/storage/storage.go @@ -23,19 +23,20 @@ import ( "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/storage/postgres" "github.com/matrix-org/dendrite/userapi/storage/sqlite3" ) -// NewDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme) +// NewUserAPIDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme) // and sets postgres connection parameters -func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (Database, error) { +func NewUserAPIDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, openIDTokenLifetimeMS int64, loginTokenLifetime time.Duration, serverNoticesLocalpart string) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) + return sqlite3.NewDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) case dbProperties.ConnectionString.IsPostgres(): - return postgres.NewDatabase(dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) + return postgres.NewDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) default: return nil, fmt.Errorf("unexpected database type") } diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go new file mode 100644 index 000000000..5bee880d3 --- /dev/null +++ b/userapi/storage/storage_test.go @@ -0,0 +1,539 @@ +package storage_test + +import ( + "context" + "encoding/json" + "fmt" + "testing" + "time" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/internal/pushrules" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/storage" + "github.com/matrix-org/dendrite/userapi/storage/tables" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/bcrypt" +) + +const loginTokenLifetime = time.Minute + +var ( + openIDLifetimeMS = time.Minute.Milliseconds() + ctx = context.Background() +) + +func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) { + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := storage.NewUserAPIDatabase(nil, &config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, "localhost", bcrypt.MinCost, openIDLifetimeMS, loginTokenLifetime, "_server") + if err != nil { + t.Fatalf("NewUserAPIDatabase returned %s", err) + } + return db, close +} + +// Tests storing and getting account data +func Test_AccountData(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + alice := test.NewUser(t) + localpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + + room := test.NewRoom(t, alice) + events := room.Events() + + contentRoom := json.RawMessage(fmt.Sprintf(`{"event_id":"%s"}`, events[len(events)-1].EventID())) + err = db.SaveAccountData(ctx, localpart, room.ID, "m.fully_read", contentRoom) + assert.NoError(t, err, "unable to save account data") + + contentGlobal := json.RawMessage(fmt.Sprintf(`{"recent_rooms":["%s"]}`, room.ID)) + err = db.SaveAccountData(ctx, localpart, "", "im.vector.setting.breadcrumbs", contentGlobal) + assert.NoError(t, err, "unable to save account data") + + accountData, err := db.GetAccountDataByType(ctx, localpart, room.ID, "m.fully_read") + assert.NoError(t, err, "unable to get account data by type") + assert.Equal(t, contentRoom, accountData) + + globalData, roomData, err := db.GetAccountData(ctx, localpart) + assert.NoError(t, err) + assert.Equal(t, contentRoom, roomData[room.ID]["m.fully_read"]) + assert.Equal(t, contentGlobal, globalData["im.vector.setting.breadcrumbs"]) + }) +} + +// Tests the creation of accounts +func Test_Accounts(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + alice := test.NewUser(t) + aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + + accAlice, err := db.CreateAccount(ctx, aliceLocalpart, "testing", "", api.AccountTypeAdmin) + assert.NoError(t, err, "failed to create account") + // verify the newly create account is the same as returned by CreateAccount + var accGet *api.Account + accGet, err = db.GetAccountByPassword(ctx, aliceLocalpart, "testing") + assert.NoError(t, err, "failed to get account by password") + assert.Equal(t, accAlice, accGet) + accGet, err = db.GetAccountByLocalpart(ctx, aliceLocalpart) + assert.NoError(t, err, "failed to get account by localpart") + assert.Equal(t, accAlice, accGet) + + // check account availability + available, err := db.CheckAccountAvailability(ctx, aliceLocalpart) + assert.NoError(t, err, "failed to checkout account availability") + assert.Equal(t, false, available) + + available, err = db.CheckAccountAvailability(ctx, "unusedname") + assert.NoError(t, err, "failed to checkout account availability") + assert.Equal(t, true, available) + + // get guest account numeric aliceLocalpart + first, err := db.GetNewNumericLocalpart(ctx) + assert.NoError(t, err, "failed to get new numeric localpart") + // Create a new account to verify the numeric localpart is updated + _, err = db.CreateAccount(ctx, "", "testing", "", api.AccountTypeGuest) + assert.NoError(t, err, "failed to create account") + second, err := db.GetNewNumericLocalpart(ctx) + assert.NoError(t, err) + assert.Greater(t, second, first) + + // update password for alice + err = db.SetPassword(ctx, aliceLocalpart, "newPassword") + assert.NoError(t, err, "failed to update password") + accGet, err = db.GetAccountByPassword(ctx, aliceLocalpart, "newPassword") + assert.NoError(t, err, "failed to get account by new password") + assert.Equal(t, accAlice, accGet) + + // deactivate account + err = db.DeactivateAccount(ctx, aliceLocalpart) + assert.NoError(t, err, "failed to deactivate account") + // This should fail now, as the account is deactivated + _, err = db.GetAccountByPassword(ctx, aliceLocalpart, "newPassword") + assert.Error(t, err, "expected an error, got none") + + _, err = db.GetAccountByLocalpart(ctx, "unusename") + assert.Error(t, err, "expected an error for non existent localpart") + }) +} + +func Test_Devices(t *testing.T) { + alice := test.NewUser(t) + localpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + deviceID := util.RandomString(8) + accessToken := util.RandomString(16) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + deviceWithID, err := db.CreateDevice(ctx, localpart, &deviceID, accessToken, nil, "", "") + assert.NoError(t, err, "unable to create deviceWithoutID") + + gotDevice, err := db.GetDeviceByID(ctx, localpart, deviceID) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, deviceWithID.ID, gotDevice.ID) // GetDeviceByID doesn't populate all fields + + gotDeviceAccessToken, err := db.GetDeviceByAccessToken(ctx, accessToken) + assert.NoError(t, err, "unable to get device by access token") + assert.Equal(t, deviceWithID.ID, gotDeviceAccessToken.ID) // GetDeviceByAccessToken doesn't populate all fields + + // create a device without existing device ID + accessToken = util.RandomString(16) + deviceWithoutID, err := db.CreateDevice(ctx, localpart, nil, accessToken, nil, "", "") + assert.NoError(t, err, "unable to create deviceWithoutID") + gotDeviceWithoutID, err := db.GetDeviceByID(ctx, localpart, deviceWithoutID.ID) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, deviceWithoutID.ID, gotDeviceWithoutID.ID) // GetDeviceByID doesn't populate all fields + + // Get devices + devices, err := db.GetDevicesByLocalpart(ctx, localpart) + assert.NoError(t, err, "unable to get devices by localpart") + assert.Equal(t, 2, len(devices)) + deviceIDs := make([]string, 0, len(devices)) + for _, dev := range devices { + deviceIDs = append(deviceIDs, dev.ID) + } + + devices2, err := db.GetDevicesByID(ctx, deviceIDs) + assert.NoError(t, err, "unable to get devices by id") + assert.ElementsMatch(t, devices, devices2) + + // Update device + newName := "new display name" + err = db.UpdateDevice(ctx, localpart, deviceWithID.ID, &newName) + assert.NoError(t, err, "unable to update device displayname") + err = db.UpdateDeviceLastSeen(ctx, localpart, deviceWithID.ID, "127.0.0.1", "Element Web") + assert.NoError(t, err, "unable to update device last seen") + + deviceWithID.DisplayName = newName + deviceWithID.LastSeenIP = "127.0.0.1" + deviceWithID.LastSeenTS = int64(gomatrixserverlib.AsTimestamp(time.Now().Truncate(time.Second))) + gotDevice, err = db.GetDeviceByID(ctx, localpart, deviceWithID.ID) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, 2, len(devices)) + assert.Equal(t, deviceWithID.DisplayName, gotDevice.DisplayName) + assert.Equal(t, deviceWithID.LastSeenIP, gotDevice.LastSeenIP) + truncatedTime := gomatrixserverlib.Timestamp(gotDevice.LastSeenTS).Time().Truncate(time.Second) + assert.Equal(t, gomatrixserverlib.Timestamp(deviceWithID.LastSeenTS), gomatrixserverlib.AsTimestamp(truncatedTime)) + + // create one more device and remove the devices step by step + newDeviceID := util.RandomString(16) + accessToken = util.RandomString(16) + _, err = db.CreateDevice(ctx, localpart, &newDeviceID, accessToken, nil, "", "") + assert.NoError(t, err, "unable to create new device") + + devices, err = db.GetDevicesByLocalpart(ctx, localpart) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, 3, len(devices)) + + err = db.RemoveDevices(ctx, localpart, deviceIDs) + assert.NoError(t, err, "unable to remove devices") + devices, err = db.GetDevicesByLocalpart(ctx, localpart) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, 1, len(devices)) + + deleted, err := db.RemoveAllDevices(ctx, localpart, "") + assert.NoError(t, err, "unable to remove all devices") + assert.Equal(t, 1, len(deleted)) + assert.Equal(t, newDeviceID, deleted[0].ID) + }) +} + +func Test_KeyBackup(t *testing.T) { + alice := test.NewUser(t) + room := test.NewRoom(t, alice) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + wantAuthData := json.RawMessage("my auth data") + wantVersion, err := db.CreateKeyBackup(ctx, alice.ID, "dummyAlgo", wantAuthData) + assert.NoError(t, err, "unable to create key backup") + // get key backup by version + gotVersion, gotAlgo, gotAuthData, _, _, err := db.GetKeyBackup(ctx, alice.ID, wantVersion) + assert.NoError(t, err, "unable to get key backup") + assert.Equal(t, wantVersion, gotVersion, "backup version mismatch") + assert.Equal(t, "dummyAlgo", gotAlgo, "backup algorithm mismatch") + assert.Equal(t, wantAuthData, gotAuthData, "backup auth data mismatch") + + // get any key backup + gotVersion, gotAlgo, gotAuthData, _, _, err = db.GetKeyBackup(ctx, alice.ID, "") + assert.NoError(t, err, "unable to get key backup") + assert.Equal(t, wantVersion, gotVersion, "backup version mismatch") + assert.Equal(t, "dummyAlgo", gotAlgo, "backup algorithm mismatch") + assert.Equal(t, wantAuthData, gotAuthData, "backup auth data mismatch") + + err = db.UpdateKeyBackupAuthData(ctx, alice.ID, wantVersion, json.RawMessage("my updated auth data")) + assert.NoError(t, err, "unable to update key backup auth data") + + uploads := []api.InternalKeyBackupSession{ + { + KeyBackupSession: api.KeyBackupSession{ + IsVerified: true, + SessionData: wantAuthData, + }, + RoomID: room.ID, + SessionID: "1", + }, + { + KeyBackupSession: api.KeyBackupSession{}, + RoomID: room.ID, + SessionID: "2", + }, + } + count, _, err := db.UpsertBackupKeys(ctx, wantVersion, alice.ID, uploads) + assert.NoError(t, err, "unable to upsert backup keys") + assert.Equal(t, int64(len(uploads)), count, "unexpected backup count") + + // do it again to update a key + uploads[1].IsVerified = true + count, _, err = db.UpsertBackupKeys(ctx, wantVersion, alice.ID, uploads[1:]) + assert.NoError(t, err, "unable to upsert backup keys") + assert.Equal(t, int64(len(uploads)), count, "unexpected backup count") + + // get backup keys by session id + gotBackupKeys, err := db.GetBackupKeys(ctx, wantVersion, alice.ID, room.ID, "1") + assert.NoError(t, err, "unable to get backup keys") + assert.Equal(t, uploads[0].KeyBackupSession, gotBackupKeys[room.ID]["1"]) + + // get backup keys by room id + gotBackupKeys, err = db.GetBackupKeys(ctx, wantVersion, alice.ID, room.ID, "") + assert.NoError(t, err, "unable to get backup keys") + assert.Equal(t, uploads[0].KeyBackupSession, gotBackupKeys[room.ID]["1"]) + + gotCount, err := db.CountBackupKeys(ctx, wantVersion, alice.ID) + assert.NoError(t, err, "unable to get backup keys count") + assert.Equal(t, count, gotCount, "unexpected backup count") + + // finally delete a key + exists, err := db.DeleteKeyBackup(ctx, alice.ID, wantVersion) + assert.NoError(t, err, "unable to delete key backup") + assert.True(t, exists) + + // this key should not exist + exists, err = db.DeleteKeyBackup(ctx, alice.ID, "3") + assert.NoError(t, err, "unable to delete key backup") + assert.False(t, exists) + }) +} + +func Test_LoginToken(t *testing.T) { + alice := test.NewUser(t) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + // create a new token + wantLoginToken := &api.LoginTokenData{UserID: alice.ID} + + gotMetadata, err := db.CreateLoginToken(ctx, wantLoginToken) + assert.NoError(t, err, "unable to create login token") + assert.NotNil(t, gotMetadata) + assert.Equal(t, time.Now().Add(loginTokenLifetime).Truncate(loginTokenLifetime), gotMetadata.Expiration.Truncate(loginTokenLifetime)) + + // get the new token + gotLoginToken, err := db.GetLoginTokenDataByToken(ctx, gotMetadata.Token) + assert.NoError(t, err, "unable to get login token") + assert.NotNil(t, gotLoginToken) + assert.Equal(t, wantLoginToken, gotLoginToken, "unexpected login token") + + // remove the login token again + err = db.RemoveLoginToken(ctx, gotMetadata.Token) + assert.NoError(t, err, "unable to remove login token") + + // check if the token was actually deleted + _, err = db.GetLoginTokenDataByToken(ctx, gotMetadata.Token) + assert.Error(t, err, "expected an error, but got none") + }) +} + +func Test_OpenID(t *testing.T) { + alice := test.NewUser(t) + token := util.RandomString(24) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + expiresAtMS := time.Now().UnixNano()/int64(time.Millisecond) + openIDLifetimeMS + expires, err := db.CreateOpenIDToken(ctx, token, alice.ID) + assert.NoError(t, err, "unable to create OpenID token") + assert.Equal(t, expiresAtMS, expires) + + attributes, err := db.GetOpenIDTokenAttributes(ctx, token) + assert.NoError(t, err, "unable to get OpenID token attributes") + assert.Equal(t, alice.ID, attributes.UserID) + assert.Equal(t, expiresAtMS, attributes.ExpiresAtMS) + }) +} + +func Test_Profile(t *testing.T) { + alice := test.NewUser(t) + aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + // create account, which also creates a profile + _, err = db.CreateAccount(ctx, aliceLocalpart, "testing", "", api.AccountTypeAdmin) + assert.NoError(t, err, "failed to create account") + + gotProfile, err := db.GetProfileByLocalpart(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get profile by localpart") + wantProfile := &authtypes.Profile{Localpart: aliceLocalpart} + assert.Equal(t, wantProfile, gotProfile) + + // set avatar & displayname + wantProfile.DisplayName = "Alice" + wantProfile.AvatarURL = "mxc://aliceAvatar" + err = db.SetDisplayName(ctx, aliceLocalpart, "Alice") + assert.NoError(t, err, "unable to set displayname") + err = db.SetAvatarURL(ctx, aliceLocalpart, "mxc://aliceAvatar") + assert.NoError(t, err, "unable to set avatar url") + // verify profile + gotProfile, err = db.GetProfileByLocalpart(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get profile by localpart") + assert.Equal(t, wantProfile, gotProfile) + + // search profiles + searchRes, err := db.SearchProfiles(ctx, "Alice", 2) + assert.NoError(t, err, "unable to search profiles") + assert.Equal(t, 1, len(searchRes)) + assert.Equal(t, *wantProfile, searchRes[0]) + }) +} + +func Test_Pusher(t *testing.T) { + alice := test.NewUser(t) + aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + + appID := util.RandomString(8) + var pushKeys []string + var gotPushers []api.Pusher + for i := 0; i < 2; i++ { + pushKey := util.RandomString(8) + + wantPusher := api.Pusher{ + PushKey: pushKey, + Kind: api.HTTPKind, + AppID: appID, + AppDisplayName: util.RandomString(8), + DeviceDisplayName: util.RandomString(8), + ProfileTag: util.RandomString(8), + Language: util.RandomString(2), + } + err = db.UpsertPusher(ctx, wantPusher, aliceLocalpart) + assert.NoError(t, err, "unable to upsert pusher") + + // check it was actually persisted + gotPushers, err = db.GetPushers(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get pushers") + assert.Equal(t, i+1, len(gotPushers)) + assert.Equal(t, wantPusher, gotPushers[i]) + pushKeys = append(pushKeys, pushKey) + } + + // remove single pusher + err = db.RemovePusher(ctx, appID, pushKeys[0], aliceLocalpart) + assert.NoError(t, err, "unable to remove pusher") + gotPushers, err := db.GetPushers(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get pushers") + assert.Equal(t, 1, len(gotPushers)) + + // remove last pusher + err = db.RemovePushers(ctx, appID, pushKeys[1]) + assert.NoError(t, err, "unable to remove pusher") + gotPushers, err = db.GetPushers(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get pushers") + assert.Equal(t, 0, len(gotPushers)) + }) +} + +func Test_ThreePID(t *testing.T) { + alice := test.NewUser(t) + aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + threePID := util.RandomString(8) + medium := util.RandomString(8) + err = db.SaveThreePIDAssociation(ctx, threePID, aliceLocalpart, medium) + assert.NoError(t, err, "unable to save threepid association") + + // get the stored threepid + gotLocalpart, err := db.GetLocalpartForThreePID(ctx, threePID, medium) + assert.NoError(t, err, "unable to get localpart for threepid") + assert.Equal(t, aliceLocalpart, gotLocalpart) + + threepids, err := db.GetThreePIDsForLocalpart(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get threepids for localpart") + assert.Equal(t, 1, len(threepids)) + assert.Equal(t, authtypes.ThreePID{ + Address: threePID, + Medium: medium, + }, threepids[0]) + + // remove threepid association + err = db.RemoveThreePIDAssociation(ctx, threePID, medium) + assert.NoError(t, err, "unexpected error") + + // verify it was deleted + threepids, err = db.GetThreePIDsForLocalpart(ctx, aliceLocalpart) + assert.NoError(t, err, "unable to get threepids for localpart") + assert.Equal(t, 0, len(threepids)) + }) +} + +func Test_Notification(t *testing.T) { + alice := test.NewUser(t) + aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) + assert.NoError(t, err) + room := test.NewRoom(t, alice) + room2 := test.NewRoom(t, alice) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, close := mustCreateDatabase(t, dbType) + defer close() + // generate some dummy notifications + for i := 0; i < 10; i++ { + eventID := util.RandomString(16) + roomID := room.ID + ts := time.Now() + if i > 5 { + roomID = room2.ID + // create some old notifications to test DeleteOldNotifications + ts = ts.AddDate(0, -2, 0) + } + notification := &api.Notification{ + Actions: []*pushrules.Action{ + {}, + }, + Event: gomatrixserverlib.ClientEvent{ + Content: gomatrixserverlib.RawJSON("{}"), + }, + Read: false, + RoomID: roomID, + TS: gomatrixserverlib.AsTimestamp(ts), + } + err = db.InsertNotification(ctx, aliceLocalpart, eventID, int64(i+1), nil, notification) + assert.NoError(t, err, "unable to insert notification") + } + + // get notifications + count, err := db.GetNotificationCount(ctx, aliceLocalpart, tables.AllNotifications) + assert.NoError(t, err, "unable to get notification count") + assert.Equal(t, int64(10), count) + notifs, count, err := db.GetNotifications(ctx, aliceLocalpart, 0, 15, tables.AllNotifications) + assert.NoError(t, err, "unable to get notifications") + assert.Equal(t, int64(10), count) + assert.Equal(t, 10, len(notifs)) + // ... for a specific room + total, _, err := db.GetRoomNotificationCounts(ctx, aliceLocalpart, room2.ID) + assert.NoError(t, err, "unable to get notifications for room") + assert.Equal(t, int64(4), total) + + // mark notification as read + affected, err := db.SetNotificationsRead(ctx, aliceLocalpart, room2.ID, 7, true) + assert.NoError(t, err, "unable to set notifications read") + assert.True(t, affected) + + // this should delete 2 notifications + affected, err = db.DeleteNotificationsUpTo(ctx, aliceLocalpart, room2.ID, 8) + assert.NoError(t, err, "unable to set notifications read") + assert.True(t, affected) + + total, _, err = db.GetRoomNotificationCounts(ctx, aliceLocalpart, room2.ID) + assert.NoError(t, err, "unable to get notifications for room") + assert.Equal(t, int64(2), total) + + // delete old notifications + err = db.DeleteOldNotifications(ctx) + assert.NoError(t, err) + + // this should now return 0 notifications + total, _, err = db.GetRoomNotificationCounts(ctx, aliceLocalpart, room2.ID) + assert.NoError(t, err, "unable to get notifications for room") + assert.Equal(t, int64(0), total) + }) +} diff --git a/userapi/storage/storage_wasm.go b/userapi/storage/storage_wasm.go index 779f77568..5d5d292e6 100644 --- a/userapi/storage/storage_wasm.go +++ b/userapi/storage/storage_wasm.go @@ -18,12 +18,14 @@ import ( "fmt" "time" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/storage/sqlite3" "github.com/matrix-org/gomatrixserverlib" ) -func NewDatabase( +func NewUserAPIDatabase( + base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, @@ -33,7 +35,7 @@ func NewDatabase( ) (Database, error) { switch { case dbProperties.ConnectionString.IsSQLite(): - return sqlite3.NewDatabase(dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) + return sqlite3.NewDatabase(base, dbProperties, serverName, bcryptCost, openIDTokenLifetimeMS, loginTokenLifetime, serverNoticesLocalpart) case dbProperties.ConnectionString.IsPostgres(): return nil, fmt.Errorf("can't use Postgres implementation") default: diff --git a/userapi/storage/tables/interface.go b/userapi/storage/tables/interface.go index eb0cae314..2fe955670 100644 --- a/userapi/storage/tables/interface.go +++ b/userapi/storage/tables/interface.go @@ -18,9 +18,11 @@ import ( "context" "database/sql" "encoding/json" + "time" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/types" ) type AccountDataTable interface { @@ -48,7 +50,7 @@ type DevicesTable interface { SelectDeviceByID(ctx context.Context, localpart, deviceID string) (*api.Device, error) SelectDevicesByLocalpart(ctx context.Context, txn *sql.Tx, localpart, exceptDeviceID string) ([]api.Device, error) SelectDevicesByID(ctx context.Context, deviceIDs []string) ([]api.Device, error) - UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr string) error + UpdateDeviceLastSeen(ctx context.Context, txn *sql.Tx, localpart, deviceID, ipAddr, userAgent string) error } type KeyBackupTable interface { @@ -111,6 +113,11 @@ type NotificationTable interface { SelectRoomCounts(ctx context.Context, txn *sql.Tx, localpart, roomID string) (total int64, highlight int64, _ error) } +type StatsTable interface { + UserStatistics(ctx context.Context, txn *sql.Tx) (*types.UserStatistics, *types.DatabaseEngine, error) + UpdateUserDailyVisits(ctx context.Context, txn *sql.Tx, startTime, lastUpdate time.Time) error +} + type NotificationFilter uint32 const ( diff --git a/userapi/storage/tables/stats_table_test.go b/userapi/storage/tables/stats_table_test.go new file mode 100644 index 000000000..11521c8b0 --- /dev/null +++ b/userapi/storage/tables/stats_table_test.go @@ -0,0 +1,319 @@ +package tables_test + +import ( + "context" + "database/sql" + "fmt" + "reflect" + "testing" + "time" + + "github.com/matrix-org/dendrite/internal/sqlutil" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/dendrite/userapi/storage/postgres" + "github.com/matrix-org/dendrite/userapi/storage/sqlite3" + "github.com/matrix-org/dendrite/userapi/storage/tables" + "github.com/matrix-org/dendrite/userapi/types" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" +) + +func mustMakeDBs(t *testing.T, dbType test.DBType) ( + *sql.DB, tables.AccountsTable, tables.DevicesTable, tables.StatsTable, func(), +) { + t.Helper() + + var ( + accTable tables.AccountsTable + devTable tables.DevicesTable + statsTable tables.StatsTable + err error + ) + + connStr, close := test.PrepareDBConnectionString(t, dbType) + db, err := sqlutil.Open(&config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, nil) + if err != nil { + t.Fatalf("failed to open db: %s", err) + } + + switch dbType { + case test.DBTypeSQLite: + accTable, err = sqlite3.NewSQLiteAccountsTable(db, "localhost") + if err != nil { + t.Fatalf("unable to create acc db: %v", err) + } + devTable, err = sqlite3.NewSQLiteDevicesTable(db, "localhost") + if err != nil { + t.Fatalf("unable to open device db: %v", err) + } + statsTable, err = sqlite3.NewSQLiteStatsTable(db, "localhost") + if err != nil { + t.Fatalf("unable to open stats db: %v", err) + } + case test.DBTypePostgres: + accTable, err = postgres.NewPostgresAccountsTable(db, "localhost") + if err != nil { + t.Fatalf("unable to create acc db: %v", err) + } + devTable, err = postgres.NewPostgresDevicesTable(db, "localhost") + if err != nil { + t.Fatalf("unable to open device db: %v", err) + } + statsTable, err = postgres.NewPostgresStatsTable(db, "localhost") + if err != nil { + t.Fatalf("unable to open stats db: %v", err) + } + } + + return db, accTable, devTable, statsTable, close +} + +func mustMakeAccountAndDevice( + t *testing.T, + ctx context.Context, + accDB tables.AccountsTable, + devDB tables.DevicesTable, + localpart string, + accType api.AccountType, + userAgent string, +) { + t.Helper() + + appServiceID := "" + if accType == api.AccountTypeAppService { + appServiceID = util.RandomString(16) + } + + _, err := accDB.InsertAccount(ctx, nil, localpart, "", appServiceID, accType) + if err != nil { + t.Fatalf("unable to create account: %v", err) + } + _, err = devDB.InsertDevice(ctx, nil, "deviceID", localpart, util.RandomString(16), nil, "", userAgent) + if err != nil { + t.Fatalf("unable to create device: %v", err) + } +} + +func mustUpdateDeviceLastSeen( + t *testing.T, + ctx context.Context, + db *sql.DB, + localpart string, + timestamp time.Time, +) { + t.Helper() + _, err := db.ExecContext(ctx, "UPDATE device_devices SET last_seen_ts = $1 WHERE localpart = $2", gomatrixserverlib.AsTimestamp(timestamp), localpart) + if err != nil { + t.Fatalf("unable to update device last seen") + } +} + +func mustUserUpdateRegistered( + t *testing.T, + ctx context.Context, + db *sql.DB, + localpart string, + timestamp time.Time, +) { + _, err := db.ExecContext(ctx, "UPDATE account_accounts SET created_ts = $1 WHERE localpart = $2", gomatrixserverlib.AsTimestamp(timestamp), localpart) + if err != nil { + t.Fatalf("unable to update device last seen") + } +} + +// These tests must run sequentially, as they build up on each other +func Test_UserStatistics(t *testing.T) { + + ctx := context.Background() + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + db, accDB, devDB, statsDB, close := mustMakeDBs(t, dbType) + defer close() + wantType := "SQLite" + if dbType == test.DBTypePostgres { + wantType = "Postgres" + } + + t.Run(fmt.Sprintf("want %s database engine", wantType), func(t *testing.T) { + _, gotDB, err := statsDB.UserStatistics(ctx, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if wantType != gotDB.Engine { // can't use DeepEqual, as the Version might differ + t.Errorf("UserStatistics() got DB engine = %+v, want %s", gotDB.Engine, wantType) + } + }) + + t.Run("Want Users", func(t *testing.T) { + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user1", api.AccountTypeUser, "Element Android") + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user2", api.AccountTypeUser, "Element iOS") + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user3", api.AccountTypeUser, "Element web") + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user4", api.AccountTypeGuest, "Element Electron") + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user5", api.AccountTypeAdmin, "gecko") + mustMakeAccountAndDevice(t, ctx, accDB, devDB, "user6", api.AccountTypeAppService, "gecko") + gotStats, _, err := statsDB.UserStatistics(ctx, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wantStats := &types.UserStatistics{ + RegisteredUsersByType: map[string]int64{ + "native": 4, + "guest": 1, + "bridged": 1, + }, + R30Users: map[string]int64{}, + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + }, + AllUsers: 6, + NonBridgedUsers: 5, + DailyUsers: 6, + MonthlyUsers: 6, + } + if !reflect.DeepEqual(gotStats, wantStats) { + t.Errorf("UserStatistics() gotStats = \n%+v\nwant\n%+v", gotStats, wantStats) + } + }) + + t.Run("Users not active for one/two month", func(t *testing.T) { + mustUpdateDeviceLastSeen(t, ctx, db, "user1", time.Now().AddDate(0, -2, 0)) + mustUpdateDeviceLastSeen(t, ctx, db, "user2", time.Now().AddDate(0, -1, 0)) + gotStats, _, err := statsDB.UserStatistics(ctx, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wantStats := &types.UserStatistics{ + RegisteredUsersByType: map[string]int64{ + "native": 4, + "guest": 1, + "bridged": 1, + }, + R30Users: map[string]int64{}, + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + }, + AllUsers: 6, + NonBridgedUsers: 5, + DailyUsers: 4, + MonthlyUsers: 4, + } + if !reflect.DeepEqual(gotStats, wantStats) { + t.Errorf("UserStatistics() gotStats = \n%+v\nwant\n%+v", gotStats, wantStats) + } + }) + + /* R30Users counts the number of 30 day retained users, defined as: + - Users who have created their accounts more than 30 days ago + - Where last seen at most 30 days ago + - Where account creation and last_seen are > 30 days apart + */ + t.Run("R30Users tests", func(t *testing.T) { + mustUserUpdateRegistered(t, ctx, db, "user1", time.Now().AddDate(0, -2, 0)) + mustUpdateDeviceLastSeen(t, ctx, db, "user1", time.Now()) + mustUserUpdateRegistered(t, ctx, db, "user4", time.Now().AddDate(0, -2, 0)) + mustUpdateDeviceLastSeen(t, ctx, db, "user4", time.Now()) + startTime := time.Now().AddDate(0, 0, -2) + err := statsDB.UpdateUserDailyVisits(ctx, nil, startTime, startTime.Truncate(time.Hour*24).Add(time.Hour)) + if err != nil { + t.Fatalf("unable to update daily visits stats: %v", err) + } + + gotStats, _, err := statsDB.UserStatistics(ctx, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wantStats := &types.UserStatistics{ + RegisteredUsersByType: map[string]int64{ + "native": 3, + "bridged": 1, + }, + R30Users: map[string]int64{ + "all": 2, + "android": 1, + "electron": 1, + }, + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 0, + "web": 0, + "electron": 0, + "all": 0, + }, + AllUsers: 6, + NonBridgedUsers: 5, + DailyUsers: 5, + MonthlyUsers: 5, + } + if !reflect.DeepEqual(gotStats, wantStats) { + t.Errorf("UserStatistics() gotStats = \n%+v\nwant\n%+v", gotStats, wantStats) + } + }) + + /* + R30UsersV2 counts the number of 30 day retained users, defined as users that: + - Appear more than once in the past 60 days + - Have more than 30 days between the most and least recent appearances that occurred in the past 60 days. + most recent -> neueste + least recent -> älteste + + */ + t.Run("R30UsersV2 tests", func(t *testing.T) { + // generate some data + for i := 100; i > 0; i-- { + mustUpdateDeviceLastSeen(t, ctx, db, "user1", time.Now().AddDate(0, 0, -i)) + mustUpdateDeviceLastSeen(t, ctx, db, "user5", time.Now().AddDate(0, 0, -i)) + startTime := time.Now().AddDate(0, 0, -i) + err := statsDB.UpdateUserDailyVisits(ctx, nil, startTime, startTime.Truncate(time.Hour*24).Add(time.Hour)) + if err != nil { + t.Fatalf("unable to update daily visits stats: %v", err) + } + } + gotStats, _, err := statsDB.UserStatistics(ctx, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + wantStats := &types.UserStatistics{ + RegisteredUsersByType: map[string]int64{ + "native": 3, + "bridged": 1, + }, + R30Users: map[string]int64{ + "all": 2, + "android": 1, + "electron": 1, + }, + R30UsersV2: map[string]int64{ + "ios": 0, + "android": 1, + "web": 1, + "electron": 0, + "all": 2, + }, + AllUsers: 6, + NonBridgedUsers: 5, + DailyUsers: 3, + MonthlyUsers: 5, + } + if !reflect.DeepEqual(gotStats, wantStats) { + t.Errorf("UserStatistics() gotStats = \n%+v\nwant\n%+v", gotStats, wantStats) + } + }) + }) + +} diff --git a/userapi/types/statistics.go b/userapi/types/statistics.go new file mode 100644 index 000000000..09564f78f --- /dev/null +++ b/userapi/types/statistics.go @@ -0,0 +1,30 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type UserStatistics struct { + RegisteredUsersByType map[string]int64 + R30Users map[string]int64 + R30UsersV2 map[string]int64 + AllUsers int64 + NonBridgedUsers int64 + DailyUsers int64 + MonthlyUsers int64 +} + +type DatabaseEngine struct { + Engine string + Version string +} diff --git a/userapi/userapi.go b/userapi/userapi.go index e91ce3a7a..603b416bf 100644 --- a/userapi/userapi.go +++ b/userapi/userapi.go @@ -30,6 +30,7 @@ import ( "github.com/matrix-org/dendrite/userapi/inthttp" "github.com/matrix-org/dendrite/userapi/producers" "github.com/matrix-org/dendrite/userapi/storage" + "github.com/matrix-org/dendrite/userapi/util" "github.com/sirupsen/logrus" ) @@ -42,11 +43,24 @@ func AddInternalRoutes(router *mux.Router, intAPI api.UserInternalAPI) { // NewInternalAPI returns a concerete implementation of the internal API. Callers // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( - base *base.BaseDendrite, db storage.Database, cfg *config.UserAPI, - appServices []config.ApplicationService, keyAPI keyapi.KeyInternalAPI, - rsAPI rsapi.RoomserverInternalAPI, pgClient pushgateway.Client, + base *base.BaseDendrite, cfg *config.UserAPI, + appServices []config.ApplicationService, keyAPI keyapi.UserKeyAPI, + rsAPI rsapi.UserRoomserverAPI, pgClient pushgateway.Client, ) api.UserInternalAPI { - js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + + db, err := storage.NewUserAPIDatabase( + base, + &cfg.AccountDatabase, + cfg.Matrix.ServerName, + cfg.BCryptCost, + cfg.OpenIDTokenLifetimeMS, + api.DefaultLoginTokenLifetime, + cfg.Matrix.ServerNotices.LocalPart, + ) + if err != nil { + logrus.WithError(err).Panicf("failed to connect to accounts db") + } syncProducer := producers.NewSyncAPI( db, js, @@ -91,5 +105,9 @@ func NewInternalAPI( } time.AfterFunc(time.Minute, cleanOldNotifs) + if base.Cfg.Global.ReportStats.Enabled { + go util.StartPhoneHomeCollector(time.Now(), base.Cfg, db) + } + return userAPI } diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go index 8c3608bd8..40e37c5d6 100644 --- a/userapi/userapi_test.go +++ b/userapi/userapi_test.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package userapi +package userapi_test import ( "context" @@ -23,15 +23,16 @@ import ( "time" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/internal/httputil" + "github.com/matrix-org/dendrite/test" + "github.com/matrix-org/dendrite/userapi" + "github.com/matrix-org/dendrite/userapi/inthttp" "github.com/matrix-org/gomatrixserverlib" "golang.org/x/crypto/bcrypt" - "github.com/matrix-org/dendrite/internal/httputil" - "github.com/matrix-org/dendrite/internal/test" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/dendrite/userapi/internal" - "github.com/matrix-org/dendrite/userapi/inthttp" "github.com/matrix-org/dendrite/userapi/storage" ) @@ -43,16 +44,15 @@ type apiTestOpts struct { loginTokenLifetime time.Duration } -func MustMakeInternalAPI(t *testing.T, opts apiTestOpts) (api.UserInternalAPI, storage.Database) { +func MustMakeInternalAPI(t *testing.T, opts apiTestOpts, dbType test.DBType) (api.UserInternalAPI, storage.Database, func()) { if opts.loginTokenLifetime == 0 { opts.loginTokenLifetime = api.DefaultLoginTokenLifetime * time.Millisecond } - dbopts := &config.DatabaseOptions{ - ConnectionString: "file::memory:", - MaxOpenConnections: 1, - MaxIdleConnections: 1, - } - accountDB, err := storage.NewDatabase(dbopts, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") + connStr, close := test.PrepareDBConnectionString(t, dbType) + + accountDB, err := storage.NewUserAPIDatabase(nil, &config.DatabaseOptions{ + ConnectionString: config.DataSource(connStr), + }, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") if err != nil { t.Fatalf("failed to create account DB: %s", err) } @@ -66,13 +66,15 @@ func MustMakeInternalAPI(t *testing.T, opts apiTestOpts) (api.UserInternalAPI, s return &internal.UserInternalAPI{ DB: accountDB, ServerName: cfg.Matrix.ServerName, - }, accountDB + }, accountDB, close } func TestQueryProfile(t *testing.T) { aliceAvatarURL := "mxc://example.com/alice" aliceDisplayName := "Alice" - userAPI, accountDB := MustMakeInternalAPI(t, apiTestOpts{}) + // only one DBType, since userapi.AddInternalRoutes complains about multiple prometheus counters added + userAPI, accountDB, close := MustMakeInternalAPI(t, apiTestOpts{}, test.DBTypeSQLite) + defer close() _, err := accountDB.CreateAccount(context.TODO(), "alice", "foobar", "", api.AccountTypeUser) if err != nil { t.Fatalf("failed to make account: %s", err) @@ -131,7 +133,7 @@ func TestQueryProfile(t *testing.T) { t.Run("HTTP API", func(t *testing.T) { router := mux.NewRouter().PathPrefix(httputil.InternalPathPrefix).Subrouter() - AddInternalRoutes(router, userAPI) + userapi.AddInternalRoutes(router, userAPI) apiURL, cancel := test.ListenAndServe(t, router, false) defer cancel() httpAPI, err := inthttp.NewUserAPIClient(apiURL, &http.Client{}) @@ -149,110 +151,120 @@ func TestLoginToken(t *testing.T) { ctx := context.Background() t.Run("tokenLoginFlow", func(t *testing.T) { - userAPI, accountDB := MustMakeInternalAPI(t, apiTestOpts{}) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + userAPI, accountDB, close := MustMakeInternalAPI(t, apiTestOpts{}, dbType) + defer close() + _, err := accountDB.CreateAccount(ctx, "auser", "apassword", "", api.AccountTypeUser) + if err != nil { + t.Fatalf("failed to make account: %s", err) + } - _, err := accountDB.CreateAccount(ctx, "auser", "apassword", "", api.AccountTypeUser) - if err != nil { - t.Fatalf("failed to make account: %s", err) - } + t.Log("Creating a login token like the SSO callback would...") - t.Log("Creating a login token like the SSO callback would...") + creq := api.PerformLoginTokenCreationRequest{ + Data: api.LoginTokenData{UserID: "@auser:example.com"}, + } + var cresp api.PerformLoginTokenCreationResponse + if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { + t.Fatalf("PerformLoginTokenCreation failed: %v", err) + } - creq := api.PerformLoginTokenCreationRequest{ - Data: api.LoginTokenData{UserID: "@auser:example.com"}, - } - var cresp api.PerformLoginTokenCreationResponse - if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { - t.Fatalf("PerformLoginTokenCreation failed: %v", err) - } + if cresp.Metadata.Token == "" { + t.Errorf("PerformLoginTokenCreation Token: got %q, want non-empty", cresp.Metadata.Token) + } + if cresp.Metadata.Expiration.Before(time.Now()) { + t.Errorf("PerformLoginTokenCreation Expiration: got %v, want non-expired", cresp.Metadata.Expiration) + } - if cresp.Metadata.Token == "" { - t.Errorf("PerformLoginTokenCreation Token: got %q, want non-empty", cresp.Metadata.Token) - } - if cresp.Metadata.Expiration.Before(time.Now()) { - t.Errorf("PerformLoginTokenCreation Expiration: got %v, want non-expired", cresp.Metadata.Expiration) - } + t.Log("Querying the login token like /login with m.login.token would...") - t.Log("Querying the login token like /login with m.login.token would...") + qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} + var qresp api.QueryLoginTokenResponse + if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { + t.Fatalf("QueryLoginToken failed: %v", err) + } - qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} - var qresp api.QueryLoginTokenResponse - if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { - t.Fatalf("QueryLoginToken failed: %v", err) - } + if qresp.Data == nil { + t.Errorf("QueryLoginToken Data: got %v, want non-nil", qresp.Data) + } else if want := "@auser:example.com"; qresp.Data.UserID != want { + t.Errorf("QueryLoginToken UserID: got %q, want %q", qresp.Data.UserID, want) + } - if qresp.Data == nil { - t.Errorf("QueryLoginToken Data: got %v, want non-nil", qresp.Data) - } else if want := "@auser:example.com"; qresp.Data.UserID != want { - t.Errorf("QueryLoginToken UserID: got %q, want %q", qresp.Data.UserID, want) - } + t.Log("Deleting the login token like /login with m.login.token would...") - t.Log("Deleting the login token like /login with m.login.token would...") - - dreq := api.PerformLoginTokenDeletionRequest{Token: cresp.Metadata.Token} - var dresp api.PerformLoginTokenDeletionResponse - if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { - t.Fatalf("PerformLoginTokenDeletion failed: %v", err) - } + dreq := api.PerformLoginTokenDeletionRequest{Token: cresp.Metadata.Token} + var dresp api.PerformLoginTokenDeletionResponse + if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { + t.Fatalf("PerformLoginTokenDeletion failed: %v", err) + } + }) }) t.Run("expiredTokenIsNotReturned", func(t *testing.T) { - userAPI, _ := MustMakeInternalAPI(t, apiTestOpts{loginTokenLifetime: -1 * time.Second}) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + userAPI, _, close := MustMakeInternalAPI(t, apiTestOpts{loginTokenLifetime: -1 * time.Second}, dbType) + defer close() - creq := api.PerformLoginTokenCreationRequest{ - Data: api.LoginTokenData{UserID: "@auser:example.com"}, - } - var cresp api.PerformLoginTokenCreationResponse - if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { - t.Fatalf("PerformLoginTokenCreation failed: %v", err) - } + creq := api.PerformLoginTokenCreationRequest{ + Data: api.LoginTokenData{UserID: "@auser:example.com"}, + } + var cresp api.PerformLoginTokenCreationResponse + if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { + t.Fatalf("PerformLoginTokenCreation failed: %v", err) + } - qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} - var qresp api.QueryLoginTokenResponse - if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { - t.Fatalf("QueryLoginToken failed: %v", err) - } + qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} + var qresp api.QueryLoginTokenResponse + if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { + t.Fatalf("QueryLoginToken failed: %v", err) + } - if qresp.Data != nil { - t.Errorf("QueryLoginToken Data: got %v, want nil", qresp.Data) - } + if qresp.Data != nil { + t.Errorf("QueryLoginToken Data: got %v, want nil", qresp.Data) + } + }) }) t.Run("deleteWorks", func(t *testing.T) { - userAPI, _ := MustMakeInternalAPI(t, apiTestOpts{}) + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + userAPI, _, close := MustMakeInternalAPI(t, apiTestOpts{}, dbType) + defer close() - creq := api.PerformLoginTokenCreationRequest{ - Data: api.LoginTokenData{UserID: "@auser:example.com"}, - } - var cresp api.PerformLoginTokenCreationResponse - if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { - t.Fatalf("PerformLoginTokenCreation failed: %v", err) - } + creq := api.PerformLoginTokenCreationRequest{ + Data: api.LoginTokenData{UserID: "@auser:example.com"}, + } + var cresp api.PerformLoginTokenCreationResponse + if err := userAPI.PerformLoginTokenCreation(ctx, &creq, &cresp); err != nil { + t.Fatalf("PerformLoginTokenCreation failed: %v", err) + } - dreq := api.PerformLoginTokenDeletionRequest{Token: cresp.Metadata.Token} - var dresp api.PerformLoginTokenDeletionResponse - if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { - t.Fatalf("PerformLoginTokenDeletion failed: %v", err) - } + dreq := api.PerformLoginTokenDeletionRequest{Token: cresp.Metadata.Token} + var dresp api.PerformLoginTokenDeletionResponse + if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { + t.Fatalf("PerformLoginTokenDeletion failed: %v", err) + } - qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} - var qresp api.QueryLoginTokenResponse - if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { - t.Fatalf("QueryLoginToken failed: %v", err) - } + qreq := api.QueryLoginTokenRequest{Token: cresp.Metadata.Token} + var qresp api.QueryLoginTokenResponse + if err := userAPI.QueryLoginToken(ctx, &qreq, &qresp); err != nil { + t.Fatalf("QueryLoginToken failed: %v", err) + } - if qresp.Data != nil { - t.Errorf("QueryLoginToken Data: got %v, want nil", qresp.Data) - } + if qresp.Data != nil { + t.Errorf("QueryLoginToken Data: got %v, want nil", qresp.Data) + } + }) }) t.Run("deleteUnknownIsNoOp", func(t *testing.T) { - userAPI, _ := MustMakeInternalAPI(t, apiTestOpts{}) - - dreq := api.PerformLoginTokenDeletionRequest{Token: "non-existent token"} - var dresp api.PerformLoginTokenDeletionResponse - if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { - t.Fatalf("PerformLoginTokenDeletion failed: %v", err) - } + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + userAPI, _, close := MustMakeInternalAPI(t, apiTestOpts{}, dbType) + defer close() + dreq := api.PerformLoginTokenDeletionRequest{Token: "non-existent token"} + var dresp api.PerformLoginTokenDeletionResponse + if err := userAPI.PerformLoginTokenDeletion(ctx, &dreq, &dresp); err != nil { + t.Fatalf("PerformLoginTokenDeletion failed: %v", err) + } + }) }) } diff --git a/userapi/util/phonehomestats.go b/userapi/util/phonehomestats.go new file mode 100644 index 000000000..ad93a50e3 --- /dev/null +++ b/userapi/util/phonehomestats.go @@ -0,0 +1,160 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "bytes" + "context" + "encoding/json" + "math" + "net/http" + "runtime" + "syscall" + "time" + + "github.com/matrix-org/dendrite/internal" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/userapi/storage" + "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" +) + +type phoneHomeStats struct { + prevData timestampToRUUsage + stats map[string]interface{} + serverName gomatrixserverlib.ServerName + startTime time.Time + cfg *config.Dendrite + db storage.Statistics + isMonolith bool + client *http.Client +} + +type timestampToRUUsage struct { + timestamp int64 + usage syscall.Rusage +} + +func StartPhoneHomeCollector(startTime time.Time, cfg *config.Dendrite, statsDB storage.Statistics) { + + p := phoneHomeStats{ + startTime: startTime, + serverName: cfg.Global.ServerName, + cfg: cfg, + db: statsDB, + isMonolith: cfg.IsMonolith, + client: &http.Client{ + Timeout: time.Second * 30, + Transport: http.DefaultTransport, + }, + } + + // start initial run after 5min + time.AfterFunc(time.Minute*5, p.collect) + + // run every 3 hours + ticker := time.NewTicker(time.Hour * 3) + for range ticker.C { + p.collect() + } +} + +func (p *phoneHomeStats) collect() { + p.stats = make(map[string]interface{}) + // general information + p.stats["homeserver"] = p.serverName + p.stats["monolith"] = p.isMonolith + p.stats["version"] = internal.VersionString() + p.stats["timestamp"] = time.Now().Unix() + p.stats["go_version"] = runtime.Version() + p.stats["go_arch"] = runtime.GOARCH + p.stats["go_os"] = runtime.GOOS + p.stats["num_cpu"] = runtime.NumCPU() + p.stats["num_go_routine"] = runtime.NumGoroutine() + p.stats["uptime_seconds"] = math.Floor(time.Since(p.startTime).Seconds()) + + ctx, cancel := context.WithTimeout(context.TODO(), time.Minute*1) + defer cancel() + + // cpu and memory usage information + err := getMemoryStats(p) + if err != nil { + logrus.WithError(err).Warn("unable to get memory/cpu stats, using defaults") + } + + // configuration information + p.stats["federation_disabled"] = p.cfg.Global.DisableFederation + p.stats["nats_embedded"] = true + p.stats["nats_in_memory"] = p.cfg.Global.JetStream.InMemory + if len(p.cfg.Global.JetStream.Addresses) > 0 { + p.stats["nats_embedded"] = false + p.stats["nats_in_memory"] = false // probably + } + if len(p.cfg.Logging) > 0 { + p.stats["log_level"] = p.cfg.Logging[0].Level + } else { + p.stats["log_level"] = "info" + } + + // message and room stats + // TODO: Find a solution to actually set these values + p.stats["total_room_count"] = 0 + p.stats["daily_messages"] = 0 + p.stats["daily_sent_messages"] = 0 + p.stats["daily_e2ee_messages"] = 0 + p.stats["daily_sent_e2ee_messages"] = 0 + + // user stats and DB engine + userStats, db, err := p.db.UserStatistics(ctx) + if err != nil { + logrus.WithError(err).Warn("unable to query userstats, using default values") + } + p.stats["database_engine"] = db.Engine + p.stats["database_server_version"] = db.Version + p.stats["total_users"] = userStats.AllUsers + p.stats["total_nonbridged_users"] = userStats.NonBridgedUsers + p.stats["daily_active_users"] = userStats.DailyUsers + p.stats["monthly_active_users"] = userStats.MonthlyUsers + for t, c := range userStats.RegisteredUsersByType { + p.stats["daily_user_type_"+t] = c + } + for t, c := range userStats.R30Users { + p.stats["r30_users_"+t] = c + } + for t, c := range userStats.R30UsersV2 { + p.stats["r30v2_users_"+t] = c + } + + output := bytes.Buffer{} + if err = json.NewEncoder(&output).Encode(p.stats); err != nil { + logrus.WithError(err).Error("unable to encode anonymous stats") + return + } + + logrus.Infof("Reporting stats to %s: %s", p.cfg.Global.ReportStats.Endpoint, output.String()) + + request, err := http.NewRequestWithContext(ctx, http.MethodPost, p.cfg.Global.ReportStats.Endpoint, &output) + if err != nil { + logrus.WithError(err).Error("unable to create anonymous stats request") + return + } + request.Header.Set("User-Agent", "Dendrite/"+internal.VersionString()) + + _, err = p.client.Do(request) + if err != nil { + logrus.WithError(err).Error("unable to send anonymous stats") + return + } +} diff --git a/userapi/util/stats.go b/userapi/util/stats.go new file mode 100644 index 000000000..22ef12aad --- /dev/null +++ b/userapi/util/stats.go @@ -0,0 +1,47 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !wasm && !windows +// +build !wasm,!windows + +package util + +import ( + "syscall" + "time" + + "github.com/sirupsen/logrus" +) + +func getMemoryStats(p *phoneHomeStats) error { + oldUsage := p.prevData + newUsage := syscall.Rusage{} + if err := syscall.Getrusage(syscall.RUSAGE_SELF, &newUsage); err != nil { + logrus.WithError(err).Error("unable to get usage") + return err + } + newData := timestampToRUUsage{timestamp: time.Now().Unix(), usage: newUsage} + p.prevData = newData + + usedCPUTime := (newUsage.Utime.Sec + newUsage.Stime.Sec) - (oldUsage.usage.Utime.Sec + oldUsage.usage.Stime.Sec) + + if usedCPUTime == 0 || newData.timestamp == oldUsage.timestamp { + p.stats["cpu_average"] = 0 + } else { + // conversion to int64 required for GOARCH=386 + p.stats["cpu_average"] = int64(usedCPUTime) / (newData.timestamp - oldUsage.timestamp) * 100 + } + p.stats["memory_rss"] = newUsage.Maxrss + return nil +} diff --git a/userapi/util/stats_wasm.go b/userapi/util/stats_wasm.go new file mode 100644 index 000000000..a182e4e6e --- /dev/null +++ b/userapi/util/stats_wasm.go @@ -0,0 +1,20 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +// stub, since WASM doesn't support syscall.Getrusage +func getMemoryStats(p *phoneHomeStats) error { + return nil +} diff --git a/userapi/util/stats_windows.go b/userapi/util/stats_windows.go new file mode 100644 index 000000000..0b3f8d013 --- /dev/null +++ b/userapi/util/stats_windows.go @@ -0,0 +1,29 @@ +// Copyright 2022 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !wasm +// +build !wasm + +package util + +import ( + "runtime" +) + +func getMemoryStats(p *phoneHomeStats) error { + var memStats runtime.MemStats + runtime.ReadMemStats(&memStats) + p.stats["memory_rss"] = memStats.Alloc + return nil +}