From 6d78c4d67d5096f5bb2c7cd56f15ab79949edc86 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 22 Apr 2022 14:58:24 +0100 Subject: [PATCH 001/103] Fix retrieving cross-signing signatures in `/user/devices/{userId}` (#2368) * Fix retrieving cross-signing signatures in `/user/devices/{userId}` We need to know the target device IDs in order to get the signatures and we weren't populating those. * Fix up signature retrieval * Fix SQLite * Always include the target's own signatures as well as the requesting user --- federationapi/routing/devices.go | 3 ++ keyserver/internal/cross_signing.go | 21 ++++++------- keyserver/internal/internal.go | 30 ++++++++++++++++--- keyserver/storage/interface.go | 2 +- .../postgres/cross_signing_sigs_table.go | 6 ++-- keyserver/storage/shared/storage.go | 6 ++-- .../sqlite3/cross_signing_sigs_table.go | 8 ++--- keyserver/storage/tables/interface.go | 2 +- 8 files changed, 52 insertions(+), 26 deletions(-) diff --git a/federationapi/routing/devices.go b/federationapi/routing/devices.go index 4cd199960..8890eac4b 100644 --- a/federationapi/routing/devices.go +++ b/federationapi/routing/devices.go @@ -43,6 +43,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{ diff --git a/keyserver/internal/cross_signing.go b/keyserver/internal/cross_signing.go index 0d083b4ba..2281f4bbf 100644 --- a/keyserver/internal/cross_signing.go +++ b/keyserver/internal/cross_signing.go @@ -455,10 +455,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 +469,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 +491,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 +501,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 +546,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/internal.go b/keyserver/internal/internal.go index a05476f5f..e70de7671 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -313,9 +313,31 @@ 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 { + 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 +361,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 } } } 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..40633c05c 100644 --- a/keyserver/storage/postgres/cross_signing_sigs_table.go +++ b/keyserver/storage/postgres/cross_signing_sigs_table.go @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs ( 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)" + @@ -72,9 +72,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/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..29ee889fd 100644 --- a/keyserver/storage/sqlite3/cross_signing_sigs_table.go +++ b/keyserver/storage/sqlite3/cross_signing_sigs_table.go @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS keyserver_cross_signing_sigs ( 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)" + @@ -71,13 +71,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/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 } From 67fb086c13b6845e6a76ab89d314895306e14e96 Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Sun, 24 Apr 2022 20:26:20 +0200 Subject: [PATCH 002/103] Update README --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index cbb35ad59..75c827c33 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,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 +21,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. @@ -78,7 +78,7 @@ $ ./bin/dendrite-monolith-server --tls-cert server.crt --tls-key server.key --co 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 From 446819e4ac405393ae7834107adc5761afce8a34 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 25 Apr 2022 11:56:50 +0200 Subject: [PATCH 003/103] Store the EDU type in the database (#2370) --- federationapi/queue/destinationqueue.go | 1 + federationapi/storage/interface.go | 2 +- federationapi/storage/shared/storage_edus.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/federationapi/queue/destinationqueue.go b/federationapi/queue/destinationqueue.go index 09814b31f..a5f8c03b9 100644 --- a/federationapi/queue/destinationqueue.go +++ b/federationapi/queue/destinationqueue.go @@ -125,6 +125,7 @@ func (oq *destinationQueue) sendEDU(event *gomatrixserverlib.EDU, receipt *share context.TODO(), 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 diff --git a/federationapi/storage/interface.go b/federationapi/storage/interface.go index 3fa8d1f7a..e3038651b 100644 --- a/federationapi/storage/interface.go +++ b/federationapi/storage/interface.go @@ -39,7 +39,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/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 { From aad81b7b4dcf971508cde266c5ae99e35261bf27 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 25 Apr 2022 14:22:46 +0100 Subject: [PATCH 004/103] Only call key update process functions if there are updates, don't send things to ourselves over federation --- federationapi/queue/queue.go | 2 ++ federationapi/routing/send.go | 49 +++++++++++++++++++++++++++++----- keyserver/internal/internal.go | 9 +++++-- 3 files changed, 51 insertions(+), 9 deletions(-) diff --git a/federationapi/queue/queue.go b/federationapi/queue/queue.go index 5b5481274..c45bbd1d4 100644 --- a/federationapi/queue/queue.go +++ b/federationapi/queue/queue.go @@ -210,6 +210,7 @@ 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 { @@ -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* diff --git a/federationapi/routing/send.go b/federationapi/routing/send.go index f2b902b6f..2c01afb1b 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -124,6 +124,7 @@ func Send( t := txnReq{ rsAPI: rsAPI, keys: keys, + ourServerName: cfg.Matrix.ServerName, federation: federation, servers: servers, keyAPI: keyAPI, @@ -183,6 +184,7 @@ type txnReq struct { gomatrixserverlib.Transaction rsAPI api.RoomserverInternalAPI keyAPI keyapi.KeyInternalAPI + 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/keyserver/internal/internal.go b/keyserver/internal/internal.go index e70de7671..e571c7e56 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -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) { @@ -663,6 +667,7 @@ func (a *KeyInternalAPI) uploadLocalDeviceKeys(ctx context.Context, req *api.Per // add the display name field from keysToStore into existingKeys keysToStore = appendDisplayNames(existingKeys, keysToStore) } + // store the device keys and emit changes err = a.DB.StoreLocalDeviceKeys(ctx, keysToStore) if err != nil { From e95fc5c5e3e4945949fbc1e9036c599687925a4d Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 25 Apr 2022 19:04:46 +0200 Subject: [PATCH 005/103] Use provided filter for account_data (#2372) * Reuse IncrementalSync, use provided filter * Inform SyncAPI about newly created push_rules --- syncapi/streams/stream_accountdata.go | 35 ++------------------------- sytest-blacklist | 4 --- sytest-whitelist | 2 +- userapi/internal/api.go | 7 ++++++ 4 files changed, 10 insertions(+), 38 deletions(-) diff --git a/syncapi/streams/stream_accountdata.go b/syncapi/streams/stream_accountdata.go index 105d85260..094c51485 100644 --- a/syncapi/streams/stream_accountdata.go +++ b/syncapi/streams/stream_accountdata.go @@ -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, + ctx, req.Device.UserID, r, &req.Filter.AccountData, ) if err != nil { req.Log.WithError(err).Error("p.DB.GetAccountDataInRange failed") diff --git a/sytest-blacklist b/sytest-blacklist index f1bd60db1..713a5b631 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 diff --git a/sytest-whitelist b/sytest-whitelist index 979f12bf6..2052185fe 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 diff --git a/userapi/internal/api.go b/userapi/internal/api.go index d1c12f05f..be58e2d8d 100644 --- a/userapi/internal/api.go +++ b/userapi/internal/api.go @@ -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 From e8ab2154aa57d46bffa2db1e7c1333c2fa40cb21 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 25 Apr 2022 19:05:01 +0200 Subject: [PATCH 006/103] Return M_NOT_FOUND for rejected events (#2371) * Return M_NOT_FOUND for rejected events * Add passing tests --- federationapi/routing/state.go | 7 +++++++ roomserver/api/query.go | 2 ++ roomserver/internal/query/query.go | 24 +++++++++++++++++------- sytest-whitelist | 6 +++++- 4 files changed, 31 insertions(+), 8 deletions(-) diff --git a/federationapi/routing/state.go b/federationapi/routing/state.go index 37cbb9d1e..a202c92c2 100644 --- a/federationapi/routing/state.go +++ b/federationapi/routing/state.go @@ -129,6 +129,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/roomserver/api/query.go b/roomserver/api/query.go index 8f84edcb5..ef2e6bb57 100644 --- a/roomserver/api/query.go +++ b/roomserver/api/query.go @@ -230,6 +230,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/internal/query/query.go b/roomserver/internal/query/query.go index 7e4d56684..5b33ec3c3 100644 --- a/roomserver/internal/query/query.go +++ b/roomserver/internal/query/query.go @@ -441,11 +441,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 +480,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 +505,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/sytest-whitelist b/sytest-whitelist index 2052185fe..5d67aee6c 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -709,4 +709,8 @@ 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 \ No newline at end of file From 7df5d69a5b606e226c5d83c23aa9dd90785c0b2d Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Tue, 26 Apr 2022 08:07:27 +0200 Subject: [PATCH 007/103] Checkout correct branch for Sytest --- .github/workflows/dendrite.yml | 1 + 1 file changed, 1 insertion(+) 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 From feac9db43fc459f1efa10424dfc96f8d54b55c64 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:28:41 +0200 Subject: [PATCH 008/103] Add transactionsCache to redact endpoint (#2375) --- clientapi/routing/redaction.go | 20 +++++++++++++++++++- clientapi/routing/routing.go | 5 +++-- sytest-whitelist | 3 ++- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/clientapi/routing/redaction.go b/clientapi/routing/redaction.go index 01ea818ab..e8d14ce34 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" @@ -40,12 +41,21 @@ type redactionResponse struct { func SendRedaction( req *http.Request, device *userapi.Device, roomID, eventID string, cfg *config.ClientAPI, rsAPI roomserverAPI.RoomserverInternalAPI, + 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/routing.go b/clientapi/routing/routing.go index 37d825b80..f370b4f8c 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -479,7 +479,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 +488,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) diff --git a/sytest-whitelist b/sytest-whitelist index 5d67aee6c..91304bd71 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -713,4 +713,5 @@ 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 \ No newline at end of file +/state_ids returns M_NOT_FOUND for a rejected state event +PUT /rooms/:room_id/redact/:event_id/:txn_id is idempotent \ No newline at end of file From e8be2b234f616c8422372665c845d9a7a1af245f Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Tue, 26 Apr 2022 10:53:17 +0200 Subject: [PATCH 009/103] Add heroes to the room summary (#2373) * Implement room summary heroes * Add passing tests * Move MembershipCount to addRoomSummary * Add comments, close Statement --- syncapi/storage/interface.go | 1 + syncapi/storage/postgres/memberships_table.go | 38 ++++++++++--- syncapi/storage/shared/syncserver.go | 4 ++ syncapi/storage/sqlite3/memberships_table.go | 51 ++++++++++++++--- syncapi/storage/tables/interface.go | 1 + syncapi/streams/stream_pdu.go | 56 +++++++++++++++---- syncapi/streams/streams.go | 1 + sytest-whitelist | 5 +- 8 files changed, 131 insertions(+), 26 deletions(-) diff --git a/syncapi/storage/interface.go b/syncapi/storage/interface.go index 14cb08a52..0fea88da6 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) diff --git a/syncapi/storage/postgres/memberships_table.go b/syncapi/storage/postgres/memberships_table.go index 39fa656cb..8c049977f 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 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/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index 2143fd672..3c431db48 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) } 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/tables/interface.go b/syncapi/storage/tables/interface.go index 993e2022b..ac713dd5c 100644 --- a/syncapi/storage/tables/interface.go +++ b/syncapi/storage/tables/interface.go @@ -170,6 +170,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 { diff --git a/syncapi/streams/stream_pdu.go b/syncapi/streams/stream_pdu.go index df5fb8e08..0d033095d 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" ) @@ -30,6 +33,7 @@ type PDUStreamProvider struct { workers atomic.Int32 // userID+deviceID -> lazy loading cache lazyLoadCache *caching.LazyLoadCache + rsAPI roomserverAPI.RoomserverInternalAPI } 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/streams.go b/syncapi/streams/streams.go index d3195b78f..a18a0cc41 100644 --- a/syncapi/streams/streams.go +++ b/syncapi/streams/streams.go @@ -33,6 +33,7 @@ func NewSyncStreamProviders( PDUStreamProvider: &PDUStreamProvider{ StreamProvider: StreamProvider{DB: d}, lazyLoadCache: lazyLoadCache, + rsAPI: rsAPI, }, TypingStreamProvider: &TypingStreamProvider{ StreamProvider: StreamProvider{DB: d}, diff --git a/sytest-whitelist b/sytest-whitelist index 91304bd71..c9829606f 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -714,4 +714,7 @@ Presence can be set from sync /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 \ No newline at end of file +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 From 5306c73b008567d855ca548d195abf3dfaf8917c Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 26 Apr 2022 13:08:54 +0100 Subject: [PATCH 010/103] Fix bug when uploading device signatures (#2377) * Find the complete key ID when uploading signatures * Try that again * Try splitting the right thing * Don't do it for device keys * Refactor `QuerySignatures` * Revert "Refactor `QuerySignatures`" This reverts commit c02832a3e92569f64f180dec1555056dc8f8c3e3. * Both requested key IDs and master/self/user keys * Fix uniqueness * Try tweaking GMSL * Update GMSL again * Revert "Update GMSL again" This reverts commit bd6916cc379dd8d9e3f38d979c6550bd658938aa. * Revert "Try tweaking GMSL" This reverts commit 2a054524da9d64c6a2a5228262fbba5fde28798c. * Database migrations --- keyserver/internal/cross_signing.go | 7 ++ .../postgres/cross_signing_sigs_table.go | 6 +- .../deltas/2022042612000000_xsigning_idx.go | 52 +++++++++++++ keyserver/storage/postgres/storage.go | 1 + .../sqlite3/cross_signing_sigs_table.go | 4 +- .../deltas/2022042612000000_xsigning_idx.go | 76 +++++++++++++++++++ keyserver/storage/sqlite3/storage.go | 1 + 7 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 keyserver/storage/postgres/deltas/2022042612000000_xsigning_idx.go create mode 100644 keyserver/storage/sqlite3/deltas/2022042612000000_xsigning_idx.go diff --git a/keyserver/internal/cross_signing.go b/keyserver/internal/cross_signing.go index 2281f4bbf..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( diff --git a/keyserver/storage/postgres/cross_signing_sigs_table.go b/keyserver/storage/postgres/cross_signing_sigs_table.go index 40633c05c..b101e7ce5 100644 --- a/keyserver/storage/postgres/cross_signing_sigs_table.go +++ b/keyserver/storage/postgres/cross_signing_sigs_table.go @@ -33,8 +33,10 @@ 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 = "" + @@ -44,7 +46,7 @@ const selectCrossSigningSigsForTargetSQL = "" + 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" 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..12956e3b4 --- /dev/null +++ b/keyserver/storage/postgres/deltas/2022042612000000_xsigning_idx.go @@ -0,0 +1,52 @@ +// 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 ( + "database/sql" + "fmt" + + "github.com/matrix-org/dendrite/internal/sqlutil" +) + +func LoadFixCrossSigningSignatureIndexes(m *sqlutil.Migrations) { + m.AddMigration(UpFixCrossSigningSignatureIndexes, DownFixCrossSigningSignatureIndexes) +} + +func UpFixCrossSigningSignatureIndexes(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(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/storage.go b/keyserver/storage/postgres/storage.go index 136986885..d4c7e2cc7 100644 --- a/keyserver/storage/postgres/storage.go +++ b/keyserver/storage/postgres/storage.go @@ -54,6 +54,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) } m := sqlutil.NewMigrations() deltas.LoadRefactorKeyChanges(m) + deltas.LoadFixCrossSigningSignatureIndexes(m) if err = m.RunDeltas(db, dbProperties); err != nil { return nil, err } diff --git a/keyserver/storage/sqlite3/cross_signing_sigs_table.go b/keyserver/storage/sqlite3/cross_signing_sigs_table.go index 29ee889fd..36d562b8a 100644 --- a/keyserver/storage/sqlite3/cross_signing_sigs_table.go +++ b/keyserver/storage/sqlite3/cross_signing_sigs_table.go @@ -33,8 +33,10 @@ 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 = "" + 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..230e39fef --- /dev/null +++ b/keyserver/storage/sqlite3/deltas/2022042612000000_xsigning_idx.go @@ -0,0 +1,76 @@ +// 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 ( + "database/sql" + "fmt" + + "github.com/matrix-org/dendrite/internal/sqlutil" +) + +func LoadFixCrossSigningSignatureIndexes(m *sqlutil.Migrations) { + m.AddMigration(UpFixCrossSigningSignatureIndexes, DownFixCrossSigningSignatureIndexes) +} + +func UpFixCrossSigningSignatureIndexes(tx *sql.Tx) error { + _, err := tx.Exec(` + 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(tx *sql.Tx) error { + _, err := tx.Exec(` + 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/storage.go b/keyserver/storage/sqlite3/storage.go index 0e0adceef..84d4cdf55 100644 --- a/keyserver/storage/sqlite3/storage.go +++ b/keyserver/storage/sqlite3/storage.go @@ -53,6 +53,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions) (*shared.Database, error) m := sqlutil.NewMigrations() deltas.LoadRefactorKeyChanges(m) + deltas.LoadFixCrossSigningSignatureIndexes(m) if err = m.RunDeltas(db, dbProperties); err != nil { return nil, err } From 4c19f22725b8f534163ad37845650005b32172ad Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Tue, 26 Apr 2022 15:50:56 +0200 Subject: [PATCH 011/103] Fix account_data not correctly send in a complete sync (#2379) * Return the StreamPosition from the database and not the latest * Fix linter issue --- syncapi/storage/interface.go | 2 +- syncapi/storage/postgres/account_data_table.go | 18 +++++++++++------- syncapi/storage/shared/syncserver.go | 2 +- syncapi/storage/sqlite3/account_data_table.go | 18 +++++++++++------- syncapi/storage/tables/interface.go | 2 +- syncapi/streams/stream_accountdata.go | 4 ++-- 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/syncapi/storage/interface.go b/syncapi/storage/interface.go index 0fea88da6..13065fa6b 100644 --- a/syncapi/storage/interface.go +++ b/syncapi/storage/interface.go @@ -81,7 +81,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) diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index 25bdb1da3..22bb4d7fa 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,11 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } + if id > pos { + pos = id + } } - return data, rows.Err() + return data, pos, rows.Err() } func (s *accountDataStatements) SelectMaxAccountDataID( diff --git a/syncapi/storage/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index 3c431db48..69bceb624 100644 --- a/syncapi/storage/shared/syncserver.go +++ b/syncapi/storage/shared/syncserver.go @@ -265,7 +265,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) } diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index 71a098177..e0d97ec32 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,12 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } + if id > pos { + pos = id + } } - return data, nil + return data, pos, nil } func (s *accountDataStatements) SelectMaxAccountDataID( diff --git a/syncapi/storage/tables/interface.go b/syncapi/storage/tables/interface.go index ac713dd5c..32b1c34ef 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) } diff --git a/syncapi/streams/stream_accountdata.go b/syncapi/streams/stream_accountdata.go index 094c51485..99cd4a92a 100644 --- a/syncapi/streams/stream_accountdata.go +++ b/syncapi/streams/stream_accountdata.go @@ -43,7 +43,7 @@ func (p *AccountDataStreamProvider) IncrementalSync( To: to, } - dataTypes, err := p.DB.GetAccountDataInRange( + dataTypes, pos, err := p.DB.GetAccountDataInRange( ctx, req.Device.UserID, r, &req.Filter.AccountData, ) if err != nil { @@ -95,5 +95,5 @@ func (p *AccountDataStreamProvider) IncrementalSync( } } - return to + return pos } From 6892e0f0e02466be3cac6fc6f17267aeecb5961b Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 26 Apr 2022 16:02:21 +0100 Subject: [PATCH 012/103] Start account data ID from `from` --- syncapi/storage/postgres/account_data_table.go | 2 +- syncapi/storage/sqlite3/account_data_table.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index 22bb4d7fa..0a7146913 100644 --- a/syncapi/storage/postgres/account_data_table.go +++ b/syncapi/storage/postgres/account_data_table.go @@ -118,7 +118,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string - var id types.StreamPosition + id := r.From for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index e0d97ec32..d84159ac8 100644 --- a/syncapi/storage/sqlite3/account_data_table.go +++ b/syncapi/storage/sqlite3/account_data_table.go @@ -114,7 +114,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string - var id types.StreamPosition + id := r.From for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { From f6d07768a82cdea07c56cf4ae463449292fa9fe4 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 26 Apr 2022 16:07:13 +0100 Subject: [PATCH 013/103] Fix account data position --- syncapi/storage/postgres/account_data_table.go | 3 ++- syncapi/storage/sqlite3/account_data_table.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index 0a7146913..ec1919fca 100644 --- a/syncapi/storage/postgres/account_data_table.go +++ b/syncapi/storage/postgres/account_data_table.go @@ -105,6 +105,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( accountDataEventFilter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { data = make(map[string][]string) + pos = r.Low() rows, err := s.selectAccountDataInRangeStmt.QueryContext(ctx, userID, r.Low(), r.High(), pq.StringArray(filterConvertTypeWildcardToSQL(accountDataEventFilter.Types)), @@ -118,7 +119,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string - id := r.From + var id types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index d84159ac8..2c7272ea8 100644 --- a/syncapi/storage/sqlite3/account_data_table.go +++ b/syncapi/storage/sqlite3/account_data_table.go @@ -96,6 +96,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( r types.Range, filter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { + pos = r.Low() data = make(map[string][]string) stmt, params, err := prepareWithFilters( s.db, nil, selectAccountDataInRangeSQL, @@ -114,7 +115,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string - id := r.From + var id types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { From b527e33c16d70ee6f94ac12c077b43283ff1fd86 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 26 Apr 2022 16:58:20 +0100 Subject: [PATCH 014/103] Send all account data on complete sync by default Squashed commit of the following: commit 0ec8de57261d573a5f88577aa9d7a1174d3999b9 Author: Neil Alexander Date: Tue Apr 26 16:56:30 2022 +0100 Select filter onto provided target filter commit da40b6fffbf5737864b223f49900048f557941f9 Author: Neil Alexander Date: Tue Apr 26 16:48:00 2022 +0100 Specify other field too commit ffc0b0801f63bb4d3061b6813e3ce5f3b4c8fbcb Author: Neil Alexander Date: Tue Apr 26 16:45:44 2022 +0100 Send as much account data as possible during complete sync --- syncapi/routing/filter.go | 4 ++-- syncapi/storage/interface.go | 6 +++--- syncapi/storage/postgres/filter_table.go | 13 ++++++------- syncapi/storage/shared/syncserver.go | 6 +++--- syncapi/storage/sqlite3/filter_table.go | 13 ++++++------- syncapi/storage/tables/interface.go | 2 +- syncapi/sync/request.go | 12 +++++++++--- 7 files changed, 30 insertions(+), 26 deletions(-) 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/storage/interface.go b/syncapi/storage/interface.go index 13065fa6b..43aaa3588 100644 --- a/syncapi/storage/interface.go +++ b/syncapi/storage/interface.go @@ -125,10 +125,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. 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/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index 69bceb624..25aca50ae 100644 --- a/syncapi/storage/shared/syncserver.go +++ b/syncapi/storage/shared/syncserver.go @@ -513,9 +513,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( 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/tables/interface.go b/syncapi/storage/tables/interface.go index 32b1c34ef..4ff4689ed 100644 --- a/syncapi/storage/tables/interface.go +++ b/syncapi/storage/tables/interface.go @@ -157,7 +157,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) } diff --git a/syncapi/sync/request.go b/syncapi/sync/request.go index f04f172d3..c9ee8e4a8 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.MaxInt + filter.Room.AccountData.Limit = math.MaxInt + } 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 } } } From 6c5c6d73d771e0ea5cc325e4251bcbfc48b7d55e Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 26 Apr 2022 17:05:31 +0100 Subject: [PATCH 015/103] Use a value that is Go 1.16-friendly --- syncapi/sync/request.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/syncapi/sync/request.go b/syncapi/sync/request.go index c9ee8e4a8..9d4740e93 100644 --- a/syncapi/sync/request.go +++ b/syncapi/sync/request.go @@ -52,8 +52,8 @@ func newSyncRequest(req *http.Request, device userapi.Device, syncDB storage.Dat // 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.MaxInt - filter.Room.AccountData.Limit = math.MaxInt + filter.AccountData.Limit = math.MaxInt32 + filter.Room.AccountData.Limit = math.MaxInt32 } filterQuery := req.URL.Query().Get("filter") if filterQuery != "" { From 66b397b3c60c51bba36e4bce858733b2fda26f6a Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 11:25:07 +0100 Subject: [PATCH 016/103] Don't create fictitious presence entries (#2381) * Don't create fictitious presence entries for users that don't have any * Update whitelist, since that test probably shouldn't be passing * Fix panics --- syncapi/consumers/presence.go | 9 ++++++++- syncapi/storage/postgres/presence_table.go | 3 +++ syncapi/storage/sqlite3/presence_table.go | 3 +++ syncapi/streams/stream_presence.go | 12 +++++------ syncapi/sync/requestpool.go | 23 ++++++++++++---------- sytest-whitelist | 2 -- 6 files changed, 33 insertions(+), 19 deletions(-) diff --git a/syncapi/consumers/presence.go b/syncapi/consumers/presence.go index b198b2292..6bcca48f4 100644 --- a/syncapi/consumers/presence.go +++ b/syncapi/consumers/presence.go @@ -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 { diff --git a/syncapi/storage/postgres/presence_table.go b/syncapi/storage/postgres/presence_table.go index 49336c4eb..9f1e37f79 100644 --- a/syncapi/storage/postgres/presence_table.go +++ b/syncapi/storage/postgres/presence_table.go @@ -127,6 +127,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 } diff --git a/syncapi/storage/sqlite3/presence_table.go b/syncapi/storage/sqlite3/presence_table.go index 00b16458d..177a01bf3 100644 --- a/syncapi/storage/sqlite3/presence_table.go +++ b/syncapi/storage/sqlite3/presence_table.go @@ -142,6 +142,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 } diff --git a/syncapi/streams/stream_presence.go b/syncapi/streams/stream_presence.go index 9a6c5c130..614b88d48 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" @@ -80,11 +79,10 @@ 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 } @@ -93,8 +91,10 @@ func (p *PresenceStreamProvider) IncrementalSync( } lastPos := to - for i := range presences { - presence := presences[i] + 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 diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 703340997..76d550a65 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -127,14 +127,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,13 +154,7 @@ 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 } diff --git a/sytest-whitelist b/sytest-whitelist index c9829606f..6af8d89ff 100644 --- a/sytest-whitelist +++ b/sytest-whitelist @@ -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. From dca4afd2f0871ed53109121dff17048e69cc4935 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 12:03:34 +0100 Subject: [PATCH 017/103] Don't send account data or receipts for left/forgotten rooms (#2382) * Only include account data and receipts for rooms in a complete sync that we care about * Fix global account data --- syncapi/streams/stream_accountdata.go | 6 ++++++ syncapi/streams/stream_receipt.go | 6 ++++++ syncapi/types/provider.go | 17 +++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/syncapi/streams/stream_accountdata.go b/syncapi/streams/stream_accountdata.go index 99cd4a92a..2cddbcf04 100644 --- a/syncapi/streams/stream_accountdata.go +++ b/syncapi/streams/stream_accountdata.go @@ -53,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{ 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/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() From 54ff4cf690918886c7e7a59a65ccff970c3aa1fc Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 12:23:55 +0100 Subject: [PATCH 018/103] Don't try to federated-join via ourselves (#2383) --- federationapi/internal/perform.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/federationapi/internal/perform.go b/federationapi/internal/perform.go index 8cd944346..aac36cc76 100644 --- a/federationapi/internal/perform.go +++ b/federationapi/internal/perform.go @@ -75,7 +75,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 From d7cc187ec00410b949ffae1625835f8ac9f36c29 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 13:36:40 +0100 Subject: [PATCH 019/103] Prevent JetStream from handling OS signals, allow running as a Windows service (#2385) * Prevent JetStream from handling OS signals, allow running as a Windows service (fixes #2374) * Remove double import --- go.mod | 1 + go.sum | 1 + setup/base/base.go | 9 +++++++-- setup/jetstream/nats.go | 1 + 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index ba222ed8f..d51c3f75d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ 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 // indirect 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 diff --git a/go.sum b/go.sum index 8bb306a82..f8daca79e 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= diff --git a/setup/base/base.go b/setup/base/base.go index 43d613b0c..281153444 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -42,6 +42,7 @@ import ( 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" @@ -462,7 +463,8 @@ func (b *BaseDendrite) SetupAndServeHTTP( }() } - <-b.ProcessContext.WaitForShutdown() + minwinsvc.SetOnExit(b.ProcessContext.ShutdownDendrite) + b.WaitForShutdown() ctx, cancel := context.WithCancel(context.Background()) cancel() @@ -475,7 +477,10 @@ func (b *BaseDendrite) SetupAndServeHTTP( 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/jetstream/nats.go b/setup/jetstream/nats.go index 1c8a89e8d..8d5289697 100644 --- a/setup/jetstream/nats.go +++ b/setup/jetstream/nats.go @@ -44,6 +44,7 @@ func Prepare(process *process.ProcessContext, cfg *config.JetStream) (natsclient StoreDir: string(cfg.StoragePath), NoSystemAccount: true, MaxPayload: 16 * 1024 * 1024, + NoSigs: true, }) if err != nil { panic(err) From f023cdf8c42cc1a4bb850b478dbbf7d901b5e1bd Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Wed, 27 Apr 2022 15:05:49 +0200 Subject: [PATCH 020/103] Add UserAPI storage tests (#2384) * Add tests for parts of the userapi storage * Add tests for keybackup * Add LoginToken tests * Add OpenID tests * Add profile tests * Add pusher tests * Add ThreePID tests * Add notification tests * Add more device tests, fix numeric localpart query * Fix failing CI * Fix numeric local part query --- go.mod | 1 + setup/base/base.go | 5 +- userapi/storage/interface.go | 91 ++-- userapi/storage/postgres/accounts_table.go | 6 +- userapi/storage/postgres/devices_table.go | 22 +- userapi/storage/shared/storage.go | 15 - userapi/storage/sqlite3/accounts_table.go | 8 +- userapi/storage/sqlite3/devices_table.go | 22 +- userapi/storage/storage.go | 4 +- userapi/storage/storage_test.go | 539 +++++++++++++++++++++ userapi/storage/storage_wasm.go | 2 +- userapi/userapi_test.go | 2 +- 12 files changed, 640 insertions(+), 77 deletions(-) create mode 100644 userapi/storage/storage_test.go diff --git a/go.mod b/go.mod index d51c3f75d..a7caadfb5 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/pressly/goose v2.7.0+incompatible github.com/prometheus/client_golang v1.12.1 github.com/sirupsen/logrus v1.8.1 + github.com/stretchr/testify v1.7.0 github.com/tidwall/gjson v1.14.0 github.com/tidwall/sjson v1.2.4 github.com/uber/jaeger-client-go v2.30.0+incompatible diff --git a/setup/base/base.go b/setup/base/base.go index 281153444..dbc5d2394 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -21,6 +21,7 @@ import ( "io" "net" "net/http" + _ "net/http/pprof" "os" "os/signal" "syscall" @@ -56,8 +57,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 @@ -273,7 +272,7 @@ func (b *BaseDendrite) PushGatewayHTTPClient() pushgateway.Client { // 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( + db, err := userdb.NewUserAPIDatabase( &b.Cfg.UserAPI.AccountDatabase, b.Cfg.Global.ServerName, b.Cfg.UserAPI.BCryptCost, diff --git a/userapi/storage/interface.go b/userapi/storage/interface.go index b15470dd4..a4562cf19 100644 --- a/userapi/storage/interface.go +++ b/userapi/storage/interface.go @@ -27,18 +27,24 @@ import ( 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 +52,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) @@ -79,11 +68,22 @@ type Database interface { 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 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 +94,50 @@ 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 + ThreePID +} + // 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 92311d56d..f86812f17 100644 --- a/userapi/storage/postgres/accounts_table.go +++ b/userapi/storage/postgres/accounts_table.go @@ -47,8 +47,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 = "" + @@ -67,7 +65,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 @@ -178,5 +176,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 7bc5dc69b..fe8c54e04 100644 --- a/userapi/storage/postgres/devices_table.go +++ b/userapi/storage/postgres/devices_table.go @@ -78,7 +78,7 @@ const selectDeviceByIDSQL = "" + "SELECT display_name 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" @@ -93,7 +93,7 @@ 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" @@ -235,16 +235,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) } @@ -262,10 +266,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 diff --git a/userapi/storage/shared/storage.go b/userapi/storage/shared/storage.go index 72ae96ecc..f7212e030 100644 --- a/userapi/storage/shared/storage.go +++ b/userapi/storage/shared/storage.go @@ -577,21 +577,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 diff --git a/userapi/storage/sqlite3/accounts_table.go b/userapi/storage/sqlite3/accounts_table.go index e6c37e58e..6c5fe3071 100644 --- a/userapi/storage/sqlite3/accounts_table.go +++ b/userapi/storage/sqlite3/accounts_table.go @@ -65,7 +65,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 @@ -121,6 +121,7 @@ func (s *accountsStatements) InsertAccount( UserID: userutil.MakeUserID(localpart, s.serverName), ServerName: s.serverName, AppServiceID: appserviceID, + AccountType: accountType, }, nil } @@ -177,5 +178,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 423640e90..7860bd6a2 100644 --- a/userapi/storage/sqlite3/devices_table.go +++ b/userapi/storage/sqlite3/devices_table.go @@ -63,7 +63,7 @@ const selectDeviceByIDSQL = "" + "SELECT display_name 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" @@ -78,7 +78,7 @@ 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" @@ -235,10 +235,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 @@ -279,16 +279,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 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) } diff --git a/userapi/storage/storage.go b/userapi/storage/storage.go index f372fe7dc..faf1ce75c 100644 --- a/userapi/storage/storage.go +++ b/userapi/storage/storage.go @@ -28,9 +28,9 @@ import ( "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(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) diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go new file mode 100644 index 000000000..e6c7d35fc --- /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(&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() + 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() + 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() + 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.Equal(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") + 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))) + devices, err = db.GetDevicesByLocalpart(ctx, localpart) + assert.NoError(t, err, "unable to get device by id") + assert.Equal(t, 2, len(devices)) + assert.Equal(t, deviceWithID.DisplayName, devices[0].DisplayName) + assert.Equal(t, deviceWithID.LastSeenIP, devices[0].LastSeenIP) + truncatedTime := gomatrixserverlib.Timestamp(devices[0].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() + 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() + 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() + 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() + 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() + 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() + 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() + 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..a8e6f031c 100644 --- a/userapi/storage/storage_wasm.go +++ b/userapi/storage/storage_wasm.go @@ -23,7 +23,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" ) -func NewDatabase( +func NewUserAPIDatabase( dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go index 8c3608bd8..076b4f3c6 100644 --- a/userapi/userapi_test.go +++ b/userapi/userapi_test.go @@ -52,7 +52,7 @@ func MustMakeInternalAPI(t *testing.T, opts apiTestOpts) (api.UserInternalAPI, s MaxOpenConnections: 1, MaxIdleConnections: 1, } - accountDB, err := storage.NewDatabase(dbopts, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") + accountDB, err := storage.NewUserAPIDatabase(dbopts, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") if err != nil { t.Fatalf("failed to create account DB: %s", err) } From 6ee8507955f2b9674649acc928768b1a4d96f7c0 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 14:45:51 +0100 Subject: [PATCH 021/103] Correct account data position mapping --- syncapi/storage/postgres/account_data_table.go | 10 +++++++--- syncapi/storage/sqlite3/account_data_table.go | 11 +++++++---- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index ec1919fca..7c0d03030 100644 --- a/syncapi/storage/postgres/account_data_table.go +++ b/syncapi/storage/postgres/account_data_table.go @@ -105,7 +105,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( accountDataEventFilter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { data = make(map[string][]string) - pos = r.Low() + pos = r.High() rows, err := s.selectAccountDataInRangeStmt.QueryContext(ctx, userID, r.Low(), r.High(), pq.StringArray(filterConvertTypeWildcardToSQL(accountDataEventFilter.Types)), @@ -120,6 +120,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string var id types.StreamPosition + var highest types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { @@ -131,10 +132,13 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } - if id > pos { - pos = id + if id > highest { + highest = id } } + if highest < pos { + pos = highest + } return data, pos, rows.Err() } diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index 2c7272ea8..1bbfe9c96 100644 --- a/syncapi/storage/sqlite3/account_data_table.go +++ b/syncapi/storage/sqlite3/account_data_table.go @@ -96,7 +96,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( r types.Range, filter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { - pos = r.Low() + pos = r.High() data = make(map[string][]string) stmt, params, err := prepareWithFilters( s.db, nil, selectAccountDataInRangeSQL, @@ -116,6 +116,7 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string var id types.StreamPosition + var highest types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { @@ -127,11 +128,13 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } - if id > pos { - pos = id + if id > highest { + highest = id } } - + if highest < pos { + pos = highest + } return data, pos, nil } From 655ac3e8fb83e1cb9b670ab420a0f661dc19786e Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 14:53:11 +0100 Subject: [PATCH 022/103] Try that again --- syncapi/storage/postgres/account_data_table.go | 10 ++++------ syncapi/storage/sqlite3/account_data_table.go | 10 ++++------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/syncapi/storage/postgres/account_data_table.go b/syncapi/storage/postgres/account_data_table.go index 7c0d03030..e9c72058b 100644 --- a/syncapi/storage/postgres/account_data_table.go +++ b/syncapi/storage/postgres/account_data_table.go @@ -105,7 +105,6 @@ func (s *accountDataStatements) SelectAccountDataInRange( accountDataEventFilter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { data = make(map[string][]string) - pos = r.High() rows, err := s.selectAccountDataInRangeStmt.QueryContext(ctx, userID, r.Low(), r.High(), pq.StringArray(filterConvertTypeWildcardToSQL(accountDataEventFilter.Types)), @@ -120,7 +119,6 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string var id types.StreamPosition - var highest types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { @@ -132,12 +130,12 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } - if id > highest { - highest = id + if id > pos { + pos = id } } - if highest < pos { - pos = highest + if pos == 0 { + pos = r.High() } return data, pos, rows.Err() } diff --git a/syncapi/storage/sqlite3/account_data_table.go b/syncapi/storage/sqlite3/account_data_table.go index 1bbfe9c96..21a16dcd3 100644 --- a/syncapi/storage/sqlite3/account_data_table.go +++ b/syncapi/storage/sqlite3/account_data_table.go @@ -96,7 +96,6 @@ func (s *accountDataStatements) SelectAccountDataInRange( r types.Range, filter *gomatrixserverlib.EventFilter, ) (data map[string][]string, pos types.StreamPosition, err error) { - pos = r.High() data = make(map[string][]string) stmt, params, err := prepareWithFilters( s.db, nil, selectAccountDataInRangeSQL, @@ -116,7 +115,6 @@ func (s *accountDataStatements) SelectAccountDataInRange( var dataType string var roomID string var id types.StreamPosition - var highest types.StreamPosition for rows.Next() { if err = rows.Scan(&id, &roomID, &dataType); err != nil { @@ -128,12 +126,12 @@ func (s *accountDataStatements) SelectAccountDataInRange( } else { data[roomID] = []string{dataType} } - if id > highest { - highest = id + if id > pos { + pos = id } } - if highest < pos { - pos = highest + if pos == 0 { + pos = r.High() } return data, pos, nil } From cafa2853c5d67b3dd4d247abdd1ad5806f0c951b Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 15:01:57 +0100 Subject: [PATCH 023/103] Use process context as base context for all HTTP --- setup/base/base.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/setup/base/base.go b/setup/base/base.go index dbc5d2394..51c43198a 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -346,6 +346,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 +364,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() + }, } } From 103795d33a09728d7619e73014d507505ff121e2 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 15:06:20 +0100 Subject: [PATCH 024/103] Defer cancel on shutdown context --- setup/base/base.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup/base/base.go b/setup/base/base.go index 51c43198a..03ea2ad7e 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -472,7 +472,7 @@ func (b *BaseDendrite) SetupAndServeHTTP( b.WaitForShutdown() ctx, cancel := context.WithCancel(context.Background()) - cancel() + defer cancel() _ = internalServ.Shutdown(ctx) _ = externalServ.Shutdown(ctx) From 923f789ca3174a685bd53ce5e64a5e86cabd38cb Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 15:29:49 +0100 Subject: [PATCH 025/103] Fix graceful shutdown --- federationapi/queue/destinationqueue.go | 13 ++++++++----- federationapi/queue/queue.go | 13 ++++++------- setup/base/base.go | 12 ++++++------ setup/jetstream/helpers.go | 16 +++++++++++++--- 4 files changed, 33 insertions(+), 21 deletions(-) diff --git a/federationapi/queue/destinationqueue.go b/federationapi/queue/destinationqueue.go index a5f8c03b9..747940403 100644 --- a/federationapi/queue/destinationqueue.go +++ b/federationapi/queue/destinationqueue.go @@ -78,7 +78,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,7 +122,7 @@ 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, @@ -177,7 +177,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() @@ -271,6 +271,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 @@ -420,13 +423,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 c45bbd1d4..d152886f5 100644 --- a/federationapi/queue/queue.go +++ b/federationapi/queue/queue.go @@ -15,7 +15,6 @@ package queue import ( - "context" "crypto/ed25519" "encoding/json" "fmt" @@ -105,14 +104,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{}{} } @@ -215,7 +214,7 @@ func (oqs *OutgoingQueues) SendEvent( // 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, @@ -238,7 +237,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) } @@ -286,7 +285,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, @@ -310,7 +309,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/setup/base/base.go b/setup/base/base.go index 03ea2ad7e..e67b034a3 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -469,14 +469,14 @@ func (b *BaseDendrite) SetupAndServeHTTP( } minwinsvc.SetOnExit(b.ProcessContext.ShutdownDendrite) - b.WaitForShutdown() - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - _ = internalServ.Shutdown(ctx) - _ = externalServ.Shutdown(ctx) + <-b.ProcessContext.WaitForShutdown() + logrus.Infof("Stopping HTTP listeners") + _ = internalServ.Shutdown(context.Background()) + _ = externalServ.Shutdown(context.Background()) logrus.Infof("Stopped HTTP listeners") + + b.WaitForShutdown() } func (b *BaseDendrite) WaitForShutdown() { 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) } From 34221938ccb1f3a885ac9e5a36b79d3d74850d38 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 27 Apr 2022 16:04:11 +0100 Subject: [PATCH 026/103] Version 0.8.2 (#2386) * Version 0.8.2 * Correct account data position mapping * Try that again * Don't duplicate wait-for-shutdowns --- CHANGES.md | 29 +++++++++++++++++++++++++++++ internal/version.go | 2 +- setup/base/base.go | 4 +--- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 831a8969d..6278bcba4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,34 @@ # Changelog +## 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/internal/version.go b/internal/version.go index 5227a03bf..2477bc9ac 100644 --- a/internal/version.go +++ b/internal/version.go @@ -17,7 +17,7 @@ var build string const ( VersionMajor = 0 VersionMinor = 8 - VersionPatch = 1 + VersionPatch = 2 VersionTag = "" // example: "rc1" ) diff --git a/setup/base/base.go b/setup/base/base.go index e67b034a3..7091c6ba5 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -469,14 +469,12 @@ func (b *BaseDendrite) SetupAndServeHTTP( } minwinsvc.SetOnExit(b.ProcessContext.ShutdownDendrite) - <-b.ProcessContext.WaitForShutdown() + logrus.Infof("Stopping HTTP listeners") _ = internalServ.Shutdown(context.Background()) _ = externalServ.Shutdown(context.Background()) logrus.Infof("Stopped HTTP listeners") - - b.WaitForShutdown() } func (b *BaseDendrite) WaitForShutdown() { From 8d69e2f0b897dd1ecd99c7bd348ad8bfc8999c4e Mon Sep 17 00:00:00 2001 From: 0x1a8510f2 Date: Wed, 27 Apr 2022 20:19:46 +0100 Subject: [PATCH 027/103] Use Go 1.18 to build Docker images (#2391) Go 1.18 has now been released for a while and the CI already tests Dendrite with Go 1.18 so there should be no issues. Go 1.18 brings some performance improvements for ARM via the register calling convention so it makes sense to switch to it. --- build/docker/Dockerfile.monolith | 4 ++-- build/docker/Dockerfile.polylith | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) 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"] From 74259f296f225510e9fbb6c5aae191c3f86c729e Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Wed, 27 Apr 2022 21:31:30 +0200 Subject: [PATCH 028/103] Fix #2390 (#2392) Fix duplicate heroes in `/sync` response. --- syncapi/storage/postgres/memberships_table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncapi/storage/postgres/memberships_table.go b/syncapi/storage/postgres/memberships_table.go index 8c049977f..00223c57a 100644 --- a/syncapi/storage/postgres/memberships_table.go +++ b/syncapi/storage/postgres/memberships_table.go @@ -64,7 +64,7 @@ const selectMembershipCountSQL = "" + ") t WHERE t.membership = $3" const selectHeroesSQL = "" + - "SELECT user_id FROM syncapi_memberships WHERE room_id = $1 AND user_id != $2 AND membership = ANY($3) LIMIT 5" + "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 From 2ff75b7c806829b211be0c310497728055e898cc Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 28 Apr 2022 11:34:19 +0100 Subject: [PATCH 029/103] Ensure signature map exists (fixes #2393) (#2397) --- keyserver/internal/internal.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/keyserver/internal/internal.go b/keyserver/internal/internal.go index e571c7e56..1677cf8e3 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -319,6 +319,9 @@ func (a *KeyInternalAPI) QueryKeys(ctx context.Context, req *api.QueryKeysReques // JSON, add the signatures and marshal it again, for some reason? 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 { From 6deb10f3f61e4da0360a8910f6b5375a2d31f182 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 28 Apr 2022 11:45:56 +0100 Subject: [PATCH 030/103] Don't answer expensive federation requests for rooms we no longer belong to (#2398) This includes `/state`, `/state_ids`, `/get_missing_events` and `/backfill`. This should fix #2396. --- federationapi/routing/backfill.go | 6 ++++++ federationapi/routing/eventauth.go | 6 ++++++ federationapi/routing/missingevents.go | 6 ++++++ federationapi/routing/routing.go | 27 ++++++++++++++++++++++++++ federationapi/routing/state.go | 6 ++++++ 5 files changed, 51 insertions(+) diff --git a/federationapi/routing/backfill.go b/federationapi/routing/backfill.go index 31005209f..82f6cbabf 100644 --- a/federationapi/routing/backfill.go +++ b/federationapi/routing/backfill.go @@ -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/eventauth.go b/federationapi/routing/eventauth.go index 0a03a0cb4..e83cb8ad2 100644 --- a/federationapi/routing/eventauth.go +++ b/federationapi/routing/eventauth.go @@ -30,6 +30,12 @@ func GetEventAuth( 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/missingevents.go b/federationapi/routing/missingevents.go index dd3df7aa9..b826d69c4 100644 --- a/federationapi/routing/missingevents.go +++ b/federationapi/routing/missingevents.go @@ -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/routing.go b/federationapi/routing/routing.go index a085ed780..6d24c8b40 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -15,6 +15,8 @@ package routing import ( + "context" + "fmt" "net/http" "github.com/gorilla/mux" @@ -24,6 +26,7 @@ import ( "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" @@ -491,3 +494,27 @@ func Setup( }), ).Methods(http.MethodGet) } + +func ErrorIfLocalServerNotInRoom( + ctx context.Context, + rsAPI api.RoomserverInternalAPI, + 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 +} diff --git a/federationapi/routing/state.go b/federationapi/routing/state.go index a202c92c2..e2b67776a 100644 --- a/federationapi/routing/state.go +++ b/federationapi/routing/state.go @@ -101,6 +101,12 @@ func getState( 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 From 65034d1f227de45e88d39ec5a3e83d854e840875 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 28 Apr 2022 11:46:15 +0100 Subject: [PATCH 031/103] Unlist test since it no longer seems to be flakey (hopefully?) --- sytest-blacklist | 1 - 1 file changed, 1 deletion(-) diff --git a/sytest-blacklist b/sytest-blacklist index 713a5b631..be0826eee 100644 --- a/sytest-blacklist +++ b/sytest-blacklist @@ -48,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 From 8683ff78b1bee6b7c35e7befb9903c794a17510c Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Thu, 28 Apr 2022 15:06:34 +0200 Subject: [PATCH 032/103] Make tests more reliable --- userapi/storage/postgres/devices_table.go | 13 ++++++++++--- userapi/storage/sqlite3/devices_table.go | 13 ++++++++++--- userapi/storage/storage_test.go | 8 ++++---- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/userapi/storage/postgres/devices_table.go b/userapi/storage/postgres/devices_table.go index fe8c54e04..6c777982f 100644 --- a/userapi/storage/postgres/devices_table.go +++ b/userapi/storage/postgres/devices_table.go @@ -75,7 +75,7 @@ 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 ORDER BY last_seen_ts DESC" @@ -215,15 +215,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 } diff --git a/userapi/storage/sqlite3/devices_table.go b/userapi/storage/sqlite3/devices_table.go index 7860bd6a2..b86ed1cc2 100644 --- a/userapi/storage/sqlite3/devices_table.go +++ b/userapi/storage/sqlite3/devices_table.go @@ -60,7 +60,7 @@ 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 ORDER BY last_seen_ts DESC" @@ -212,15 +212,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 } diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go index e6c7d35fc..2eb57d0bc 100644 --- a/userapi/storage/storage_test.go +++ b/userapi/storage/storage_test.go @@ -180,12 +180,12 @@ func Test_Devices(t *testing.T) { deviceWithID.DisplayName = newName deviceWithID.LastSeenIP = "127.0.0.1" deviceWithID.LastSeenTS = int64(gomatrixserverlib.AsTimestamp(time.Now().Truncate(time.Second))) - devices, err = db.GetDevicesByLocalpart(ctx, localpart) + 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, devices[0].DisplayName) - assert.Equal(t, deviceWithID.LastSeenIP, devices[0].LastSeenIP) - truncatedTime := gomatrixserverlib.Timestamp(devices[0].LastSeenTS).Time().Truncate(time.Second) + 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 From 21ee5b36a41f2cb3960f63ef6f19106d36312aae Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Thu, 28 Apr 2022 16:12:40 +0200 Subject: [PATCH 033/103] Limit presence in `/sync` responses (#2394) * Use filter and limit presence count * More limiting * More limiting * Fix unit test * Also limit presence by last_active_ts * Update query, use "from" as the initial lastPos * Get 1000 presence events, they are filtered later Co-authored-by: Neil Alexander --- syncapi/storage/interface.go | 2 +- syncapi/storage/postgres/presence_table.go | 9 ++++++--- syncapi/storage/shared/syncserver.go | 4 ++-- syncapi/storage/sqlite3/presence_table.go | 10 ++++++---- syncapi/storage/tables/interface.go | 2 +- syncapi/streams/stream_presence.go | 12 ++++++++++-- syncapi/sync/requestpool_test.go | 2 +- 7 files changed, 27 insertions(+), 14 deletions(-) diff --git a/syncapi/storage/interface.go b/syncapi/storage/interface.go index 43aaa3588..486978598 100644 --- a/syncapi/storage/interface.go +++ b/syncapi/storage/interface.go @@ -159,6 +159,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/presence_table.go b/syncapi/storage/postgres/presence_table.go index 9f1e37f79..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 @@ -144,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/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index 25aca50ae..b7d2d3a29 100644 --- a/syncapi/storage/shared/syncserver.go +++ b/syncapi/storage/shared/syncserver.go @@ -1056,8 +1056,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/presence_table.go b/syncapi/storage/sqlite3/presence_table.go index 177a01bf3..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 @@ -158,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/tables/interface.go b/syncapi/storage/tables/interface.go index 4ff4689ed..6fdd9483e 100644 --- a/syncapi/storage/tables/interface.go +++ b/syncapi/storage/tables/interface.go @@ -188,5 +188,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/streams/stream_presence.go b/syncapi/streams/stream_presence.go index 614b88d48..675a7a178 100644 --- a/syncapi/streams/stream_presence.go +++ b/syncapi/streams/stream_presence.go @@ -53,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 @@ -72,6 +73,7 @@ func (p *PresenceStreamProvider) IncrementalSync( 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 { @@ -86,11 +88,14 @@ func (p *PresenceStreamProvider) IncrementalSync( req.Log.WithError(err).Error("unable to query presence for user") return from } + if len(presences) > req.Filter.Presence.Limit { + break NewlyJoinedLoop + } } } } - lastPos := to + lastPos := from for _, presence := range presences { if presence == nil { continue @@ -135,6 +140,9 @@ 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) } diff --git a/syncapi/sync/requestpool_test.go b/syncapi/sync/requestpool_test.go index a80089945..5e52bc7c9 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 } From c6ea2c9ff26ca6ae4c799db08a3f72c6b4d99256 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 28 Apr 2022 16:02:30 +0100 Subject: [PATCH 034/103] Add `/_dendrite/admin/evacuateRoom/{roomID}` (#2401) * Add new endpoint to allow admins to evacuate the local server from the room * Guard endpoint * Use right prefix * Auth API * More useful return error rather than a panic * More useful return value again * Update the path * Try using inputer instead * oh provide the config * Try that again * Return affected user IDs * Don't create so many forward extremities * Add missing `Path` to name Co-authored-by: Till <2353100+S7evinK@users.noreply.github.com> --- build/gobind-pinecone/monolith.go | 1 + build/gobind-yggdrasil/monolith.go | 1 + clientapi/clientapi.go | 4 +- clientapi/routing/routing.go | 42 ++++- cmd/dendrite-demo-pinecone/main.go | 1 + cmd/dendrite-demo-yggdrasil/main.go | 1 + cmd/dendrite-monolith-server/main.go | 1 + .../personalities/clientapi.go | 6 +- cmd/dendritejs-pinecone/main.go | 1 + roomserver/api/api.go | 6 + roomserver/api/api_trace.go | 9 + roomserver/api/perform.go | 9 + roomserver/internal/api.go | 7 + roomserver/internal/perform/perform_admin.go | 162 ++++++++++++++++++ roomserver/inthttp/client.go | 38 ++-- roomserver/inthttp/server.go | 11 ++ setup/monolith.go | 4 +- 17 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 roomserver/internal/perform/perform_admin.go diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index 9cc94d650..6b2533491 100644 --- a/build/gobind-pinecone/monolith.go +++ b/build/gobind-pinecone/monolith.go @@ -314,6 +314,7 @@ func (m *DendriteMonolith) Start() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath() diff --git a/build/gobind-yggdrasil/monolith.go b/build/gobind-yggdrasil/monolith.go index 87dcad2e8..b9c6c1b78 100644 --- a/build/gobind-yggdrasil/monolith.go +++ b/build/gobind-yggdrasil/monolith.go @@ -152,6 +152,7 @@ func (m *DendriteMonolith) Start() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) httpRouter := mux.NewRouter() diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index e2f8d3f32..ad277056c 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -36,6 +36,7 @@ func AddPublicRoutes( process *process.ProcessContext, router *mux.Router, synapseAdminRouter *mux.Router, + dendriteAdminRouter *mux.Router, cfg *config.ClientAPI, federation *gomatrixserverlib.FederationClient, rsAPI roomserverAPI.RoomserverInternalAPI, @@ -62,7 +63,8 @@ func AddPublicRoutes( } routing.Setup( - router, synapseAdminRouter, cfg, rsAPI, asAPI, + router, synapseAdminRouter, dendriteAdminRouter, + cfg, rsAPI, asAPI, userAPI, userDirectoryProvider, federation, syncProducer, transactionsCache, fsAPI, keyAPI, extRoomsProvider, mscCfg, natsClient, diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index f370b4f8c..ec90b80db 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -48,7 +48,8 @@ import ( // applied: // nolint: gocyclo func Setup( - publicAPIMux, synapseAdminRouter *mux.Router, cfg *config.ClientAPI, + publicAPIMux, synapseAdminRouter, dendriteAdminRouter *mux.Router, + cfg *config.ClientAPI, rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, userAPI userapi.UserInternalAPI, @@ -119,6 +120,45 @@ 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 { + 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, + }, + } + }), + ).Methods(http.MethodGet, http.MethodOptions) + // server notifications if cfg.Matrix.ServerNotices.Enabled { logrus.Info("Enabling server notices at /_synapse/admin/v1/send_server_notice") diff --git a/cmd/dendrite-demo-pinecone/main.go b/cmd/dendrite-demo-pinecone/main.go index dd1ab3697..7ec810c95 100644 --- a/cmd/dendrite-demo-pinecone/main.go +++ b/cmd/dendrite-demo-pinecone/main.go @@ -193,6 +193,7 @@ func main() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) wsUpgrader := websocket.Upgrader{ diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go index b840eb2b8..54231f30c 100644 --- a/cmd/dendrite-demo-yggdrasil/main.go +++ b/cmd/dendrite-demo-yggdrasil/main.go @@ -150,6 +150,7 @@ func main() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) 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..5fd5c0b5d 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -153,6 +153,7 @@ func main() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) if len(base.Cfg.MSCs.MSCs) > 0 { diff --git a/cmd/dendrite-polylith-multi/personalities/clientapi.go b/cmd/dendrite-polylith-multi/personalities/clientapi.go index 1e509f88a..7ed2075aa 100644 --- a/cmd/dendrite-polylith-multi/personalities/clientapi.go +++ b/cmd/dendrite-polylith-multi/personalities/clientapi.go @@ -31,8 +31,10 @@ 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, + base.ProcessContext, base.PublicClientAPIMux, + base.SynapseAdminMux, base.DendriteAdminMux, + &base.Cfg.ClientAPI, federation, rsAPI, asQuery, + transactions.New(), fsAPI, userAPI, userAPI, keyAPI, nil, &cfg.MSCs, ) diff --git a/cmd/dendritejs-pinecone/main.go b/cmd/dendritejs-pinecone/main.go index 211b3e131..0d4b2fbc5 100644 --- a/cmd/dendritejs-pinecone/main.go +++ b/cmd/dendritejs-pinecone/main.go @@ -220,6 +220,7 @@ func startup() { base.PublicWellKnownAPIMux, base.PublicMediaAPIMux, base.SynapseAdminMux, + base.DendriteAdminMux, ) httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath() diff --git a/roomserver/api/api.go b/roomserver/api/api.go index fb77423f8..f0ca8a615 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -66,6 +66,12 @@ type RoomserverInternalAPI interface { res *PerformInboundPeekResponse, ) error + PerformAdminEvacuateRoom( + ctx context.Context, + req *PerformAdminEvacuateRoomRequest, + res *PerformAdminEvacuateRoomResponse, + ) + QueryPublishedRooms( ctx context.Context, req *QueryPublishedRoomsRequest, diff --git a/roomserver/api/api_trace.go b/roomserver/api/api_trace.go index ec7211ef8..61c06e886 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -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, 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/internal/api.go b/roomserver/internal/api.go index 59f485cf7..267cd4099 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 @@ -164,6 +165,12 @@ 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") 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/inthttp/client.go b/roomserver/inthttp/client.go index d55805a91..3b29001e9 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" @@ -299,6 +300,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..c5159a63c 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 { diff --git a/setup/monolith.go b/setup/monolith.go index 32f1a6494..c86ec7b69 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -54,13 +54,13 @@ type Monolith struct { } // 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(process *process.ProcessContext, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux, dendriteMux *mux.Router) { userDirectoryProvider := m.ExtUserDirectoryProvider if userDirectoryProvider == nil { userDirectoryProvider = m.UserAPI } clientapi.AddPublicRoutes( - process, csMux, synapseMux, &m.Config.ClientAPI, + process, csMux, synapseMux, dendriteMux, &m.Config.ClientAPI, m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(), m.FederationAPI, m.UserAPI, userDirectoryProvider, m.KeyAPI, From 2a5b8e0306a283aa8ca64822d59d71479ffba59a Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Thu, 28 Apr 2022 18:53:28 +0200 Subject: [PATCH 035/103] Only load members of newly joined rooms (#2389) * Only load members of newly joined rooms * Comment that the query is prepared at runtime Co-authored-by: Neil Alexander --- syncapi/notifier/notifier.go | 14 ++++++ syncapi/storage/interface.go | 3 ++ .../postgres/current_room_state_table.go | 34 ++++++++++++- syncapi/storage/shared/syncserver.go | 4 ++ .../sqlite3/current_room_state_table.go | 49 +++++++++++++++++-- syncapi/storage/tables/interface.go | 2 + syncapi/streams/stream_presence.go | 5 +- 7 files changed, 102 insertions(+), 9 deletions(-) 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/storage/interface.go b/syncapi/storage/interface.go index 486978598..5a036d889 100644 --- a/syncapi/storage/interface.go +++ b/syncapi/storage/interface.go @@ -52,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. 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/shared/syncserver.go b/syncapi/storage/shared/syncserver.go index b7d2d3a29..ec5edd355 100644 --- a/syncapi/storage/shared/syncserver.go +++ b/syncapi/storage/shared/syncserver.go @@ -168,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) } 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/tables/interface.go b/syncapi/storage/tables/interface.go index 6fdd9483e..ccdebfdbd 100644 --- a/syncapi/storage/tables/interface.go +++ b/syncapi/storage/tables/interface.go @@ -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. diff --git a/syncapi/streams/stream_presence.go b/syncapi/streams/stream_presence.go index 675a7a178..a84d19878 100644 --- a/syncapi/streams/stream_presence.go +++ b/syncapi/streams/stream_presence.go @@ -67,9 +67,8 @@ 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 } From 26a1512808282c954a141b3376c47b05ef1e6ab4 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Fri, 29 Apr 2022 09:31:11 +0200 Subject: [PATCH 036/103] Add restrictions for open registration (#2402) * Add restrications for open registration * Make enable open registration a parameter * Enable registration for CI * Update error message * Shuffle things around a bit * Add a warning at every startup just to be extra annoying * Ignore shared secret when warning about open registration, since it's not strictly required when it is set if registration is otherwise enabled * Make CI happy? * Add missing parameter; try new parameter in upgrade-test Co-authored-by: Neil Alexander --- build/docker/config/dendrite.yaml | 2 +- build/gobind-pinecone/monolith.go | 2 ++ build/gobind-yggdrasil/monolith.go | 2 ++ build/scripts/Complement.Dockerfile | 2 +- build/scripts/ComplementLocal.Dockerfile | 2 +- build/scripts/ComplementPostgres.Dockerfile | 2 +- cmd/dendrite-demo-pinecone/main.go | 2 ++ cmd/dendrite-demo-yggdrasil/main.go | 2 ++ cmd/dendrite-upgrade-tests/main.go | 3 ++- cmd/dendritejs-pinecone/main.go | 2 ++ cmd/generate-config/main.go | 2 ++ dendrite-config.yaml | 2 +- setup/base/base.go | 4 ++++ setup/config/config_clientapi.go | 23 ++++++++++++++++++++- setup/flags.go | 9 ++++++-- 15 files changed, 52 insertions(+), 9 deletions(-) diff --git a/build/docker/config/dendrite.yaml b/build/docker/config/dendrite.yaml index e3a0316dc..94dcf4558 100644 --- a/build/docker/config/dendrite.yaml +++ b/build/docker/config/dendrite.yaml @@ -140,7 +140,7 @@ client_api: # 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 # If set, allows registration by anyone who knows the shared secret, regardless of # whether registration is otherwise disabled. diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index 6b2533491..d047f3fff 100644 --- a/build/gobind-pinecone/monolith.go +++ b/build/gobind-pinecone/monolith.go @@ -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) } diff --git a/build/gobind-yggdrasil/monolith.go b/build/gobind-yggdrasil/monolith.go index b9c6c1b78..4e95e3972 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) } 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/cmd/dendrite-demo-pinecone/main.go b/cmd/dendrite-demo-pinecone/main.go index 7ec810c95..785e7b460 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) } diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go index 54231f30c..f9234319a 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) } diff --git a/cmd/dendrite-upgrade-tests/main.go b/cmd/dendrite-upgrade-tests/main.go index 3241234ac..b7e7da07d 100644 --- a/cmd/dendrite-upgrade-tests/main.go +++ b/cmd/dendrite-upgrade-tests/main.go @@ -83,7 +83,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 0d4b2fbc5..5ecf1f2fb 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) 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/dendrite-config.yaml b/dendrite-config.yaml index 47f08c4fd..1c11ef96d 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -159,7 +159,7 @@ client_api: # 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. diff --git a/setup/base/base.go b/setup/base/base.go index 7091c6ba5..4b771aa36 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -126,6 +126,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") diff --git a/setup/config/config_clientapi.go b/setup/config/config_clientapi.go index 4590e752b..6104ed8b9 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,7 +61,8 @@ 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() } @@ -72,6 +79,20 @@ func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { } c.TURN.Verify(configErrs) c.RateLimiting.Verify(configErrs) + + // 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.", + ) + } + } } type TURN struct { 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 } From 1e083794ef0d2968eae9c874e621d199a2b62238 Mon Sep 17 00:00:00 2001 From: Brian Meek Date: Fri, 29 Apr 2022 00:55:35 -0700 Subject: [PATCH 037/103] Update golangci-lint, how it's installed, and added to the PATH (#2403) * switch to dendrite server * minor refactor to merge store code * Fix issue where m.room.name is being filtered by the dendrite server * refresh dendrite main * refresh dendrite main * missing merges from the last dendrite refresh * revert unwanted changes in dendrite.yaml * Update golangci-lint, how it's installed, and added to the PATH Co-authored-by: Tak Wai Wong Co-authored-by: tak-slashtalk <64229756+tak-slashtalk@users.noreply.github.com> --- build/scripts/find-lint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From 0d4b8eadaa45ff65cddd4b882a156de33c4bed7e Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Fri, 29 Apr 2022 10:00:28 +0200 Subject: [PATCH 038/103] Add create-account to Getting started --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 75c827c33..2a8c36508 100644 --- a/README.md +++ b/README.md @@ -74,6 +74,10 @@ $ cp dendrite-config.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`. From 2a4517f8e6c7f68d8bd94f0b4640b142f2d3ac52 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 29 Apr 2022 09:10:08 +0100 Subject: [PATCH 039/103] Move admin functions into their own file in the client API --- clientapi/routing/admin.go | 49 ++++++++++++++++++++++++++++++++++++ clientapi/routing/routing.go | 35 +------------------------- 2 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 clientapi/routing/admin.go diff --git a/clientapi/routing/admin.go b/clientapi/routing/admin.go new file mode 100644 index 000000000..31e431c78 --- /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.RoomserverInternalAPI) 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/routing.go b/clientapi/routing/routing.go index ec90b80db..ba1c76b81 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -122,40 +122,7 @@ func Setup( dendriteAdminRouter.Handle("/admin/evacuateRoom/{roomID}", httputil.MakeAuthAPI("admin_evacuate_room", userAPI, func(req *http.Request, device *userapi.Device) 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, - }, - } + return AdminEvacuateRoom(req, device, rsAPI) }), ).Methods(http.MethodGet, http.MethodOptions) From d28d0ee66e22402bebd791a46de33c8bf3169e26 Mon Sep 17 00:00:00 2001 From: Brian Meek Date: Fri, 29 Apr 2022 09:32:58 +0100 Subject: [PATCH 040/103] Fix `TestThumbnailsStorage` failing when media results come back in non-deterministic order; silence expected error when tests are run multiple times against the same postgres database (cherry-picked from #2395) Signed-off-by: Brian Meek --- mediaapi/storage/storage_test.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/mediaapi/storage/storage_test.go b/mediaapi/storage/storage_test.go index 8d3403045..fa88cd8e7 100644 --- a/mediaapi/storage/storage_test.go +++ b/mediaapi/storage/storage_test.go @@ -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) + } } }) From 31799a3b2a733192a656cfa77662599bd1568bdc Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 29 Apr 2022 16:02:55 +0100 Subject: [PATCH 041/103] Device list display name fixes (#2405) * Get device names from `unsigned` in `/user/devices` * Fix display name updates * Fix bug * Fix another bug --- federationapi/routing/devices.go | 8 ++- keyserver/internal/internal.go | 95 ++++++++++++++++---------------- 2 files changed, 54 insertions(+), 49 deletions(-) diff --git a/federationapi/routing/devices.go b/federationapi/routing/devices.go index 8890eac4b..57286fa90 100644 --- a/federationapi/routing/devices.go +++ b/federationapi/routing/devices.go @@ -20,6 +20,7 @@ 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 @@ -69,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/keyserver/internal/internal.go b/keyserver/internal/internal.go index 1677cf8e3..e556f44b0 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -632,43 +632,55 @@ 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 @@ -764,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 -} From 987d7adc5d4e34827759b206ac4cc17cce78e221 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Sat, 30 Apr 2022 00:07:50 +0200 Subject: [PATCH 042/103] Return "to", if we didn't return any presence events (#2407) Return correct stream position, if we didn't return any presence events --- syncapi/streams/stream_presence.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/syncapi/streams/stream_presence.go b/syncapi/streams/stream_presence.go index a84d19878..35ce53cb6 100644 --- a/syncapi/streams/stream_presence.go +++ b/syncapi/streams/stream_presence.go @@ -145,6 +145,10 @@ func (p *PresenceStreamProvider) IncrementalSync( p.cache.Store(cacheKey, presence) } + if len(req.Response.Presence.Events) == 0 { + return to + } + return lastPos } From bfa344e83191c49bdc9917ab7a8ec31b93c202e9 Mon Sep 17 00:00:00 2001 From: Brian Meek Date: Fri, 29 Apr 2022 15:23:11 -0700 Subject: [PATCH 043/103] Test_Devices, sqlite may return devices in different order, test should still pass (#2406) --- userapi/storage/storage_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go index 2eb57d0bc..ef25b8000 100644 --- a/userapi/storage/storage_test.go +++ b/userapi/storage/storage_test.go @@ -168,7 +168,7 @@ func Test_Devices(t *testing.T) { devices2, err := db.GetDevicesByID(ctx, deviceIDs) assert.NoError(t, err, "unable to get devices by id") - assert.Equal(t, devices, devices2) + assert.ElementsMatch(t, devices, devices2) // Update device newName := "new display name" From 979a551f1e2aeb9f3417df5e52a7279230b7a3ba Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 2 May 2022 10:47:16 +0200 Subject: [PATCH 044/103] Return `null` if MaxFileSizeBytes is 0 (#2409) * Return "null" if MaxFileSizeBytes is 0 * Add comment and nil check (better save than sorry) * Simplify config --- mediaapi/routing/download.go | 2 +- mediaapi/routing/routing.go | 8 ++++++-- mediaapi/routing/upload.go | 18 +++++++++--------- mediaapi/routing/upload_test.go | 5 ++--- setup/config/config_mediaapi.go | 6 +++--- 5 files changed, 21 insertions(+), 18 deletions(-) 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..97dfd3341 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 @@ -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..e04c010f8 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, @@ -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/setup/config/config_mediaapi.go b/setup/config/config_mediaapi.go index 9a7d84969..c85020d2a 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"` @@ -48,7 +48,7 @@ func (c *MediaAPI) Defaults(generate bool) { c.BasePath = "./media_store" } - c.MaxFileSizeBytes = &DefaultMaxFileSizeBytes + c.MaxFileSizeBytes = DefaultMaxFileSizeBytes c.MaxThumbnailGenerators = 10 } @@ -61,7 +61,7 @@ func (c *MediaAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { 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 { From 4ad5f9c982fe5dc9e306a9269621ead8c31248cf Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 3 May 2022 16:35:06 +0100 Subject: [PATCH 045/103] Global database connection pool (for monolith mode) (#2411) * Allow monolith components to share a single database pool * Don't yell about missing connection strings * Rename field * Setup tweaks * Fix panic * Improve configuration checks * Update config * Fix lint errors * Update comments --- appservice/appservice.go | 2 +- appservice/storage/postgres/storage.go | 6 +- appservice/storage/sqlite3/storage.go | 6 +- appservice/storage/storage.go | 7 +- appservice/storage/storage_wasm.go | 5 +- build/gobind-pinecone/monolith.go | 6 +- build/gobind-yggdrasil/monolith.go | 6 +- cmd/create-account/main.go | 16 ++++- cmd/dendrite-demo-pinecone/main.go | 6 +- cmd/dendrite-demo-yggdrasil/main.go | 6 +- cmd/dendrite-monolith-server/main.go | 6 +- .../personalities/mediaapi.go | 5 +- .../personalities/syncapi.go | 2 +- .../personalities/userapi.go | 4 +- cmd/dendritejs-pinecone/main.go | 6 +- cmd/resolve-state/main.go | 2 +- dendrite-config.yaml | 10 +++ federationapi/federationapi.go | 2 +- federationapi/storage/postgres/storage.go | 6 +- federationapi/storage/sqlite3/storage.go | 6 +- federationapi/storage/storage.go | 7 +- federationapi/storage/storage_wasm.go | 5 +- internal/sqlutil/sqlutil.go | 51 ++++++++++++++ internal/sqlutil/trace.go | 44 ------------ keyserver/keyserver.go | 2 +- keyserver/storage/postgres/storage.go | 7 +- keyserver/storage/sqlite3/storage.go | 7 +- keyserver/storage/storage.go | 7 +- keyserver/storage/storage_test.go | 2 +- keyserver/storage/storage_wasm.go | 5 +- mediaapi/mediaapi.go | 4 +- mediaapi/routing/upload_test.go | 2 +- mediaapi/storage/postgres/mediaapi.go | 7 +- mediaapi/storage/sqlite3/mediaapi.go | 7 +- mediaapi/storage/storage.go | 7 +- mediaapi/storage/storage_test.go | 2 +- mediaapi/storage/storage_wasm.go | 5 +- roomserver/internal/input/input_test.go | 1 + roomserver/roomserver.go | 2 +- roomserver/storage/postgres/storage.go | 13 ++-- roomserver/storage/sqlite3/storage.go | 18 ++--- roomserver/storage/storage.go | 7 +- roomserver/storage/storage_wasm.go | 5 +- setup/base/base.go | 69 +++++++++++++------ setup/config/config_appservice.go | 4 +- setup/config/config_federationapi.go | 4 +- setup/config/config_global.go | 7 ++ setup/config/config_keyserver.go | 4 +- setup/config/config_mediaapi.go | 4 +- setup/config/config_mscs.go | 4 +- setup/config/config_roomserver.go | 4 +- setup/config/config_syncapi.go | 4 +- setup/config/config_userapi.go | 4 +- setup/monolith.go | 22 +++--- setup/mscs/msc2836/msc2836.go | 2 +- setup/mscs/msc2836/storage.go | 23 +++---- syncapi/storage/postgres/syncserver.go | 6 +- syncapi/storage/sqlite3/syncserver.go | 6 +- syncapi/storage/storage.go | 7 +- syncapi/storage/storage_test.go | 2 +- syncapi/storage/storage_wasm.go | 5 +- .../storage/tables/output_room_events_test.go | 2 +- syncapi/storage/tables/topology_test.go | 2 +- syncapi/syncapi.go | 24 +++---- userapi/storage/postgres/storage.go | 7 +- userapi/storage/sqlite3/storage.go | 7 +- userapi/storage/storage.go | 7 +- userapi/storage/storage_test.go | 2 +- userapi/storage/storage_wasm.go | 4 +- userapi/userapi.go | 15 +++- userapi/userapi_test.go | 2 +- 71 files changed, 345 insertions(+), 240 deletions(-) create mode 100644 internal/sqlutil/sqlutil.go diff --git a/appservice/appservice.go b/appservice/appservice.go index b99091866..0db2c1009 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -62,7 +62,7 @@ func NewInternalAPI( js, _ := jetstream.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") } 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/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index d047f3fff..8cf663d00 100644 --- a/build/gobind-pinecone/monolith.go +++ b/build/gobind-pinecone/monolith.go @@ -268,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{} @@ -281,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) @@ -295,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, @@ -309,7 +307,7 @@ func (m *DendriteMonolith) Start() { ExtUserDirectoryProvider: userProvider, } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, diff --git a/build/gobind-yggdrasil/monolith.go b/build/gobind-yggdrasil/monolith.go index 4e95e3972..2c7d4e91b 100644 --- a/build/gobind-yggdrasil/monolith.go +++ b/build/gobind-yggdrasil/monolith.go @@ -107,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{} @@ -120,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) @@ -132,7 +131,6 @@ func (m *DendriteMonolith) Start() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: ygg.CreateClient(base), FedClient: federation, KeyRing: keyRing, @@ -147,7 +145,7 @@ func (m *DendriteMonolith) Start() { ), } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, diff --git a/cmd/create-account/main.go b/cmd/create-account/main.go index 2719f8680..7a5660522 100644 --- a/cmd/create-account/main.go +++ b/cmd/create-account/main.go @@ -25,8 +25,8 @@ import ( "strings" "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 +99,18 @@ func main() { } } - b := base.NewBaseDendrite(cfg, "Monolith") - accountDB := b.CreateAccountsDB() + accountDB, err := storage.NewUserAPIDatabase( + nil, + &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 785e7b460..33487e64b 100644 --- a/cmd/dendrite-demo-pinecone/main.go +++ b/cmd/dendrite-demo-pinecone/main.go @@ -149,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{} @@ -162,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) @@ -174,7 +173,6 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: conn.CreateClient(base, pQUIC), FedClient: federation, KeyRing: keyRing, @@ -188,7 +186,7 @@ func main() { ExtUserDirectoryProvider: userProvider, } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go index f9234319a..df9ba5121 100644 --- a/cmd/dendrite-demo-yggdrasil/main.go +++ b/cmd/dendrite-demo-yggdrasil/main.go @@ -104,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{} @@ -117,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) @@ -130,7 +129,6 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: ygg.CreateClient(base), FedClient: federation, KeyRing: keyRing, @@ -145,7 +143,7 @@ func main() { ), } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, diff --git a/cmd/dendrite-monolith-server/main.go b/cmd/dendrite-monolith-server/main.go index 5fd5c0b5d..4c7c4297f 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) @@ -104,7 +103,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,7 +134,6 @@ func main() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: base.CreateClient(), FedClient: federation, KeyRing: keyRing, @@ -146,7 +144,7 @@ func main() { KeyAPI: keyAPI, } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, diff --git a/cmd/dendrite-polylith-multi/personalities/mediaapi.go b/cmd/dendrite-polylith-multi/personalities/mediaapi.go index fa9d36a38..8c0bfa195 100644 --- a/cmd/dendrite-polylith-multi/personalities/mediaapi.go +++ b/cmd/dendrite-polylith-multi/personalities/mediaapi.go @@ -24,7 +24,10 @@ 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, base.PublicMediaAPIMux, &base.Cfg.MediaAPI, &base.Cfg.ClientAPI.RateLimiting, + 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..f9f1c5a09 100644 --- a/cmd/dendrite-polylith-multi/personalities/syncapi.go +++ b/cmd/dendrite-polylith-multi/personalities/syncapi.go @@ -27,7 +27,7 @@ func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { rsAPI := base.RoomserverHTTPClient() syncapi.AddPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, userAPI, rsAPI, base.KeyServerHTTPClient(), federation, &cfg.SyncAPI, 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/dendritejs-pinecone/main.go b/cmd/dendritejs-pinecone/main.go index 5ecf1f2fb..ead381368 100644 --- a/cmd/dendritejs-pinecone/main.go +++ b/cmd/dendritejs-pinecone/main.go @@ -180,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) @@ -189,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( @@ -201,7 +200,6 @@ func startup() { monolith := setup.Monolith{ Config: base.Cfg, - AccountDB: accountDB, Client: conn.CreateClient(base, pSessions), FedClient: federation, KeyRing: keyRing, @@ -215,7 +213,7 @@ func startup() { ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation), } monolith.AddAllPublicRoutes( - base.ProcessContext, + base, base.PublicClientAPIMux, base.PublicFederationAPIMux, base.PublicKeyAPIMux, 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-config.yaml b/dendrite-config.yaml index 1c11ef96d..1647af15d 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -54,6 +54,16 @@ global: # 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: postgres://user:pass@hostname/database?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: "" diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index 5bfe237a8..1848a242e 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -91,7 +91,7 @@ func NewInternalAPI( ) 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") } diff --git a/federationapi/storage/postgres/storage.go b/federationapi/storage/postgres/storage.go index b2aea6929..9863afb2b 100644 --- a/federationapi/storage/postgres/storage.go +++ b/federationapi/storage/postgres/storage.go @@ -23,6 +23,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" ) @@ -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.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/sqlite3/storage.go b/federationapi/storage/sqlite3/storage.go index c2e83211e..7d0cee90e 100644 --- a/federationapi/storage/sqlite3/storage.go +++ b/federationapi/storage/sqlite3/storage.go @@ -22,6 +22,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" ) @@ -34,13 +35,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/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/keyserver/keyserver.go b/keyserver/keyserver.go index c557dfbaa..007a48a55 100644 --- a/keyserver/keyserver.go +++ b/keyserver/keyserver.go @@ -41,7 +41,7 @@ func NewInternalAPI( ) api.KeyInternalAPI { js, _ := jetstream.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/postgres/storage.go b/keyserver/storage/postgres/storage.go index d4c7e2cc7..b8f70acf8 100644 --- a/keyserver/storage/postgres/storage.go +++ b/keyserver/storage/postgres/storage.go @@ -18,13 +18,14 @@ import ( "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/keyserver/storage/postgres/deltas" "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 } @@ -63,7 +64,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/sqlite3/storage.go b/keyserver/storage/sqlite3/storage.go index 84d4cdf55..aeea9eac6 100644 --- a/keyserver/storage/sqlite3/storage.go +++ b/keyserver/storage/sqlite3/storage.go @@ -18,11 +18,12 @@ import ( "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/dendrite/keyserver/storage/shared" "github.com/matrix-org/dendrite/keyserver/storage/sqlite3/deltas" + "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 } @@ -62,7 +63,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/mediaapi/mediaapi.go b/mediaapi/mediaapi.go index e5daf480d..f2fa14384 100644 --- a/mediaapi/mediaapi.go +++ b/mediaapi/mediaapi.go @@ -18,6 +18,7 @@ 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/base" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" @@ -26,13 +27,14 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component. func AddPublicRoutes( + base *base.BaseDendrite, router *mux.Router, cfg *config.MediaAPI, rateLimit *config.RateLimiting, userAPI userapi.UserInternalAPI, client *gomatrixserverlib.Client, ) { - mediaDB, err := storage.NewMediaAPIDatasource(&cfg.Database) + mediaDB, err := storage.NewMediaAPIDatasource(base, &cfg.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to media db") } diff --git a/mediaapi/routing/upload_test.go b/mediaapi/routing/upload_test.go index e04c010f8..420d0eba9 100644 --- a/mediaapi/routing/upload_test.go +++ b/mediaapi/routing/upload_test.go @@ -50,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, 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 fa88cd8e7..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 { 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/roomserver/internal/input/input_test.go b/roomserver/internal/input/input_test.go index 81c86ae38..5d34842bf 100644 --- a/roomserver/internal/input/input_test.go +++ b/roomserver/internal/input/input_test.go @@ -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/roomserver.go b/roomserver/roomserver.go index 36e3c5269..46261eb3e 100644 --- a/roomserver/roomserver.go +++ b/roomserver/roomserver.go @@ -45,7 +45,7 @@ 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") } diff --git a/roomserver/storage/postgres/storage.go b/roomserver/storage/postgres/storage.go index b5e05c982..da8d25848 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) } @@ -59,7 +60,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 } @@ -110,7 +111,7 @@ func (d *Database) create(db *sql.DB) error { return nil } -func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { +func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { eventStateKeys, err := prepareEventStateKeysTable(db) if err != nil { return err @@ -166,7 +167,7 @@ func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { d.Database = shared.Database{ DB: db, Cache: cache, - Writer: sqlutil.NewDummyWriter(), + Writer: writer, EventTypesTable: eventTypes, EventStateKeysTable: eventStateKeys, EventJSONTable: eventJSON, diff --git a/roomserver/storage/sqlite3/storage.go b/roomserver/storage/sqlite3/storage.go index 325c253b5..e6cf1a53f 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 { @@ -67,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 } @@ -118,7 +120,7 @@ func (d *Database) create(db *sql.DB) error { return nil } -func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { +func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { eventStateKeys, err := prepareEventStateKeysTable(db) if err != nil { return err @@ -174,7 +176,7 @@ func (d *Database) prepare(db *sql.DB, cache caching.RoomServerCaches) error { 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/setup/base/base.go b/setup/base/base.go index 4b771aa36..9b227b70b 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -17,6 +17,7 @@ package base import ( "context" "crypto/tls" + "database/sql" "fmt" "io" "net" @@ -32,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" @@ -40,7 +42,6 @@ import ( "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/setup/process" - userdb "github.com/matrix-org/dendrite/userapi/storage" "github.com/gorilla/mux" "github.com/kardianos/minwinsvc" @@ -81,6 +82,8 @@ type BaseDendrite struct { Cfg *config.Dendrite Caches *caching.Caches DNSCache *gomatrixserverlib.DNSCache + Database *sql.DB + DatabaseWriter sqlutil.Writer } const NoListener = "" @@ -112,7 +115,8 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base } configErrors := &config.ConfigErrors{} - cfg.Verify(configErrors, componentName == "Monolith") // TODO: better way? + isMonolith := componentName == "Monolith" // TODO: better way? + cfg.Verify(configErrors, isMonolith) if len(*configErrors) > 0 { for _, err := range *configErrors { logrus.Errorf("Configuration error: %s", err) @@ -185,6 +189,24 @@ 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") + } + if db, err = sqlutil.Open(&cfg.Global.DatabaseOptions, sqlutil.NewDummyWriter()); 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 @@ -214,6 +236,8 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base DendriteAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.DendriteAdminPathPrefix).Subrouter().UseEncodedPath(), SynapseAdminMux: mux.NewRouter().SkipClean(true).PathPrefix(httputil.SynapseAdminPathPrefix).Subrouter().UseEncodedPath(), apiHttpClient: &apiClient, + Database: db, // set if monolith with global connection pool only + DatabaseWriter: writer, // set if monolith with global connection pool only } } @@ -222,6 +246,29 @@ func (b *BaseDendrite) Close() error { return b.tracerCloser.Close() } +// 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 AppServiceQueryAPI for hitting the appservice component over HTTP. func (b *BaseDendrite) AppserviceHTTPClient() appserviceAPI.AppServiceQueryAPI { a, err := asinthttp.NewAppserviceClient(b.Cfg.AppServiceURL(), b.apiHttpClient) @@ -273,24 +320,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.NewUserAPIDatabase( - &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 { diff --git a/setup/config/config_appservice.go b/setup/config/config_appservice.go index 3f4e1c917..d93b6ebe0 100644 --- a/setup/config/config_appservice.go +++ b/setup/config/config_appservice.go @@ -52,7 +52,9 @@ func (c *AppServiceAPI) Defaults(generate bool) { func (c *AppServiceAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { 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)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "app_service_api.database.connection_string", string(c.Database.ConnectionString)) + } } // ApplicationServiceNamespace is the namespace that a specific application diff --git a/setup/config/config_federationapi.go b/setup/config/config_federationapi.go index 176334dd8..f62a23e1f 100644 --- a/setup/config/config_federationapi.go +++ b/setup/config/config_federationapi.go @@ -49,7 +49,9 @@ func (c *FederationAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { if !isMonolith { checkURL(configErrs, "federation_api.external_api.listen", string(c.ExternalAPI.Listen)) } - checkNotEmpty(configErrs, "federation_api.database.connection_string", string(c.Database.ConnectionString)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + 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..d609e2460 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"` diff --git a/setup/config/config_keyserver.go b/setup/config/config_keyserver.go index 6180ccbc8..9e2d54cdc 100644 --- a/setup/config/config_keyserver.go +++ b/setup/config/config_keyserver.go @@ -20,5 +20,7 @@ func (c *KeyServer) Defaults(generate bool) { func (c *KeyServer) Verify(configErrs *ConfigErrors, isMonolith bool) { 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)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "key_server.database.connection_string", string(c.Database.ConnectionString)) + } } diff --git a/setup/config/config_mediaapi.go b/setup/config/config_mediaapi.go index c85020d2a..273de322a 100644 --- a/setup/config/config_mediaapi.go +++ b/setup/config/config_mediaapi.go @@ -58,7 +58,9 @@ func (c *MediaAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { if !isMonolith { checkURL(configErrs, "media_api.external_api.listen", string(c.ExternalAPI.Listen)) } - checkNotEmpty(configErrs, "media_api.database.connection_string", string(c.Database.ConnectionString)) + if c.Matrix.DatabaseOptions.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)) 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..8a3227349 100644 --- a/setup/config/config_roomserver.go +++ b/setup/config/config_roomserver.go @@ -20,5 +20,7 @@ func (c *RoomServer) Defaults(generate bool) { func (c *RoomServer) Verify(configErrs *ConfigErrors, isMonolith bool) { 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)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "room_server.database.connection_string", string(c.Database.ConnectionString)) + } } diff --git a/setup/config/config_syncapi.go b/setup/config/config_syncapi.go index dc813cb7d..48fd9f506 100644 --- a/setup/config/config_syncapi.go +++ b/setup/config/config_syncapi.go @@ -27,5 +27,7 @@ func (c *SyncAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { if !isMonolith { checkURL(configErrs, "sync_api.external_api.listen", string(c.ExternalAPI.Listen)) } - checkNotEmpty(configErrs, "sync_api.database", string(c.Database.ConnectionString)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + checkNotEmpty(configErrs, "sync_api.database", string(c.Database.ConnectionString)) + } } diff --git a/setup/config/config_userapi.go b/setup/config/config_userapi.go index 570dc6030..4aa3b57bb 100644 --- a/setup/config/config_userapi.go +++ b/setup/config/config_userapi.go @@ -37,6 +37,8 @@ func (c *UserAPI) Defaults(generate bool) { func (c *UserAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { 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)) + if c.Matrix.DatabaseOptions.ConnectionString == "" { + 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/monolith.go b/setup/monolith.go index c86ec7b69..a414172ce 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -25,11 +25,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,7 +36,6 @@ 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 @@ -54,26 +52,28 @@ type Monolith struct { } // AddAllPublicRoutes attaches all public paths to the given router -func (m *Monolith) AddAllPublicRoutes(process *process.ProcessContext, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux, dendriteMux *mux.Router) { +func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux, dendriteMux *mux.Router) { userDirectoryProvider := m.ExtUserDirectoryProvider if userDirectoryProvider == nil { userDirectoryProvider = m.UserAPI } clientapi.AddPublicRoutes( - process, csMux, synapseMux, dendriteMux, &m.Config.ClientAPI, - m.FedClient, m.RoomserverAPI, - m.AppserviceAPI, transactions.New(), + base.ProcessContext, csMux, synapseMux, dendriteMux, &m.Config.ClientAPI, + m.FedClient, m.RoomserverAPI, m.AppserviceAPI, transactions.New(), m.FederationAPI, m.UserAPI, userDirectoryProvider, m.KeyAPI, m.ExtPublicRoomsProvider, &m.Config.MSCs, ) federationapi.AddPublicRoutes( - process, ssMux, keyMux, wkMux, &m.Config.FederationAPI, m.UserAPI, m.FedClient, - m.KeyRing, m.RoomserverAPI, m.FederationAPI, + base.ProcessContext, ssMux, keyMux, wkMux, &m.Config.FederationAPI, + m.UserAPI, m.FedClient, m.KeyRing, m.RoomserverAPI, m.FederationAPI, m.KeyAPI, &m.Config.MSCs, nil, ) - mediaapi.AddPublicRoutes(mediaMux, &m.Config.MediaAPI, &m.Config.ClientAPI.RateLimiting, m.UserAPI, m.Client) + mediaapi.AddPublicRoutes( + base, mediaMux, &m.Config.MediaAPI, &m.Config.ClientAPI.RateLimiting, + m.UserAPI, m.Client, + ) syncapi.AddPublicRoutes( - process, csMux, m.UserAPI, m.RoomserverAPI, + base, csMux, m.UserAPI, m.RoomserverAPI, m.KeyAPI, m.FedClient, &m.Config.SyncAPI, ) } 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/storage/postgres/syncserver.go b/syncapi/storage/postgres/syncserver.go index b0382512a..9cfe7c070 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/postgres/deltas" "github.com/matrix-org/dendrite/syncapi/storage/shared" @@ -35,13 +36,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/sqlite3/syncserver.go b/syncapi/storage/sqlite3/syncserver.go index dfc289482..e08a0ba82 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" "github.com/matrix-org/dendrite/syncapi/storage/sqlite3/deltas" @@ -35,13 +36,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(dbProperties); 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..1150c2f3d 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 { 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/output_room_events_test.go b/syncapi/storage/tables/output_room_events_test.go index a143e5ecd..8bbf879d4 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) } diff --git a/syncapi/storage/tables/topology_test.go b/syncapi/storage/tables/topology_test.go index b6ece0b0d..2334aae2e 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) } diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index b2d333f74..a2b8859cd 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -23,9 +23,9 @@ import ( keyapi "github.com/matrix-org/dendrite/keyserver/api" "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/matrix-org/gomatrixserverlib" @@ -41,7 +41,7 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the SyncAPI // component. func AddPublicRoutes( - process *process.ProcessContext, + base *base.BaseDendrite, router *mux.Router, userAPI userapi.UserInternalAPI, rsAPI api.RoomserverInternalAPI, @@ -49,9 +49,9 @@ func AddPublicRoutes( federation *gomatrixserverlib.FederationClient, cfg *config.SyncAPI, ) { - js, natsClient := jetstream.Prepare(process, &cfg.Matrix.JetStream) + js, natsClient := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) - syncDB, err := storage.NewSyncServerDatasource(&cfg.Database) + syncDB, err := storage.NewSyncServerDatasource(base, &cfg.Database) if err != nil { logrus.WithError(err).Panicf("failed to connect to sync db") } @@ -86,7 +86,7 @@ func AddPublicRoutes( } keyChangeConsumer := consumers.NewOutputKeyChangeEventConsumer( - process, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), + base.ProcessContext, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), js, keyAPI, rsAPI, syncDB, notifier, streams.DeviceListStreamProvider, ) @@ -95,7 +95,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 +103,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,28 +111,28 @@ 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 { @@ -140,7 +140,7 @@ func AddPublicRoutes( } presenceConsumer := consumers.NewPresenceConsumer( - process, cfg, js, natsClient, syncDB, + base.ProcessContext, cfg, js, natsClient, syncDB, notifier, streams.PresenceStreamProvider, userAPI, ) diff --git a/userapi/storage/postgres/storage.go b/userapi/storage/postgres/storage.go index b2a517605..74100a728 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/postgres/deltas" "github.com/matrix-org/dendrite/userapi/storage/shared" @@ -30,8 +31,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 } @@ -107,7 +108,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver Notifications: notificationsTable, ServerName: serverName, DB: db, - Writer: sqlutil.NewDummyWriter(), + Writer: writer, LoginTokenLifetime: loginTokenLifetime, BcryptCost: bcryptCost, OpenIDTokenLifetimeMS: openIDTokenLifetimeMS, diff --git a/userapi/storage/sqlite3/storage.go b/userapi/storage/sqlite3/storage.go index 03c013f00..6858d3d15 100644 --- a/userapi/storage/sqlite3/storage.go +++ b/userapi/storage/sqlite3/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" @@ -31,8 +32,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.NewExclusiveWriter()) if err != nil { return nil, err } @@ -108,7 +109,7 @@ func NewDatabase(dbProperties *config.DatabaseOptions, serverName gomatrixserver Notifications: notificationsTable, 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 faf1ce75c..42221e752 100644 --- a/userapi/storage/storage.go +++ b/userapi/storage/storage.go @@ -23,6 +23,7 @@ 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" @@ -30,12 +31,12 @@ import ( // NewUserAPIDatabase opens a new Postgres or Sqlite database (based on dataSourceName scheme) // and sets postgres connection parameters -func NewUserAPIDatabase(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 index ef25b8000..79d5a8dae 100644 --- a/userapi/storage/storage_test.go +++ b/userapi/storage/storage_test.go @@ -29,7 +29,7 @@ var ( func mustCreateDatabase(t *testing.T, dbType test.DBType) (storage.Database, func()) { connStr, close := test.PrepareDBConnectionString(t, dbType) - db, err := storage.NewUserAPIDatabase(&config.DatabaseOptions{ + db, err := storage.NewUserAPIDatabase(nil, &config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), }, "localhost", bcrypt.MinCost, openIDLifetimeMS, loginTokenLifetime, "_server") if err != nil { diff --git a/userapi/storage/storage_wasm.go b/userapi/storage/storage_wasm.go index a8e6f031c..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 NewUserAPIDatabase( + base *base.BaseDendrite, dbProperties *config.DatabaseOptions, serverName gomatrixserverlib.ServerName, bcryptCost int, @@ -33,7 +35,7 @@ func NewUserAPIDatabase( ) (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/userapi.go b/userapi/userapi.go index e91ce3a7a..9174119e1 100644 --- a/userapi/userapi.go +++ b/userapi/userapi.go @@ -42,12 +42,25 @@ 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, + base *base.BaseDendrite, cfg *config.UserAPI, appServices []config.ApplicationService, keyAPI keyapi.KeyInternalAPI, rsAPI rsapi.RoomserverInternalAPI, pgClient pushgateway.Client, ) api.UserInternalAPI { js, _ := jetstream.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, // TODO: user API should handle syncs for account data. Right now, diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go index 076b4f3c6..64e23909a 100644 --- a/userapi/userapi_test.go +++ b/userapi/userapi_test.go @@ -52,7 +52,7 @@ func MustMakeInternalAPI(t *testing.T, opts apiTestOpts) (api.UserInternalAPI, s MaxOpenConnections: 1, MaxIdleConnections: 1, } - accountDB, err := storage.NewUserAPIDatabase(dbopts, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") + accountDB, err := storage.NewUserAPIDatabase(nil, dbopts, serverName, bcrypt.MinCost, config.DefaultOpenIDTokenLifetimeMS, opts.loginTokenLifetime, "") if err != nil { t.Fatalf("failed to create account DB: %s", err) } From dd061a172e97005a4a7a4c37db6caf3f77c10d51 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 3 May 2022 17:17:02 +0100 Subject: [PATCH 046/103] Tidy up `AddPublicRoutes` (#2412) * Simplify federation API `AddPublicRoutes` * Simplify client API `AddPublicRoutes` * Simplify media API `AddPublicRoutes` * Simplify sync API `AddPublicRoutes` * Simplify `AddAllPublicRoutes` --- build/gobind-pinecone/monolith.go | 11 +---------- build/gobind-yggdrasil/monolith.go | 11 +---------- clientapi/clientapi.go | 19 ++++++++----------- cmd/dendrite-demo-pinecone/main.go | 11 +---------- cmd/dendrite-demo-yggdrasil/main.go | 11 +---------- cmd/dendrite-monolith-server/main.go | 11 +---------- .../personalities/clientapi.go | 6 ++---- .../personalities/federationapi.go | 7 +++---- .../personalities/mediaapi.go | 3 +-- .../personalities/syncapi.go | 4 ++-- cmd/dendritejs-pinecone/main.go | 11 +---------- federationapi/federationapi.go | 19 +++++++++---------- federationapi/federationapi_test.go | 2 +- mediaapi/mediaapi.go | 10 ++++------ setup/monolith.go | 19 +++++++------------ syncapi/syncapi.go | 11 ++++++----- 16 files changed, 49 insertions(+), 117 deletions(-) diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index 8cf663d00..310ac7dda 100644 --- a/build/gobind-pinecone/monolith.go +++ b/build/gobind-pinecone/monolith.go @@ -306,16 +306,7 @@ func (m *DendriteMonolith) Start() { ExtPublicRoomsProvider: roomProvider, ExtUserDirectoryProvider: userProvider, } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + 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 2c7d4e91b..991bc462f 100644 --- a/build/gobind-yggdrasil/monolith.go +++ b/build/gobind-yggdrasil/monolith.go @@ -144,16 +144,7 @@ func (m *DendriteMonolith) Start() { ygg, fsAPI, federation, ), } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + monolith.AddAllPublicRoutes(base) httpRouter := mux.NewRouter() httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux) diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index ad277056c..0d16e4c14 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,20 +23,15 @@ 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, - dendriteAdminRouter *mux.Router, - cfg *config.ClientAPI, + base *base.BaseDendrite, federation *gomatrixserverlib.FederationClient, rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, @@ -47,9 +41,10 @@ func AddPublicRoutes( userDirectoryProvider userapi.UserDirectoryProvider, keyAPI keyserverAPI.KeyInternalAPI, extRoomsProvider api.ExtraPublicRoomsProvider, - mscCfg *config.MSCs, ) { - js, natsClient := jetstream.Prepare(process, &cfg.Matrix.JetStream) + cfg := &base.Cfg.ClientAPI + mscCfg := &base.Cfg.MSCs + js, natsClient := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) syncProducer := &producers.SyncAPIProducer{ JetStream: js, @@ -63,7 +58,9 @@ func AddPublicRoutes( } routing.Setup( - router, synapseAdminRouter, dendriteAdminRouter, + base.PublicClientAPIMux, + base.SynapseAdminMux, + base.DendriteAdminMux, cfg, rsAPI, asAPI, userAPI, userDirectoryProvider, federation, syncProducer, transactionsCache, fsAPI, keyAPI, diff --git a/cmd/dendrite-demo-pinecone/main.go b/cmd/dendrite-demo-pinecone/main.go index 33487e64b..703436051 100644 --- a/cmd/dendrite-demo-pinecone/main.go +++ b/cmd/dendrite-demo-pinecone/main.go @@ -185,16 +185,7 @@ func main() { ExtPublicRoomsProvider: roomProvider, ExtUserDirectoryProvider: userProvider, } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + monolith.AddAllPublicRoutes(base) wsUpgrader := websocket.Upgrader{ CheckOrigin: func(_ *http.Request) bool { diff --git a/cmd/dendrite-demo-yggdrasil/main.go b/cmd/dendrite-demo-yggdrasil/main.go index df9ba5121..619720d6c 100644 --- a/cmd/dendrite-demo-yggdrasil/main.go +++ b/cmd/dendrite-demo-yggdrasil/main.go @@ -142,16 +142,7 @@ func main() { ygg, fsAPI, federation, ), } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + 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 4c7c4297f..2fa4675a4 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -143,16 +143,7 @@ func main() { UserAPI: userAPI, KeyAPI: keyAPI, } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + 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/personalities/clientapi.go b/cmd/dendrite-polylith-multi/personalities/clientapi.go index 7ed2075aa..a5d69d07c 100644 --- a/cmd/dendrite-polylith-multi/personalities/clientapi.go +++ b/cmd/dendrite-polylith-multi/personalities/clientapi.go @@ -31,11 +31,9 @@ func ClientAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { keyAPI := base.KeyServerHTTPClient() clientapi.AddPublicRoutes( - base.ProcessContext, base.PublicClientAPIMux, - base.SynapseAdminMux, base.DendriteAdminMux, - &base.Cfg.ClientAPI, federation, rsAPI, asQuery, + base, federation, rsAPI, asQuery, transactions.New(), fsAPI, userAPI, userAPI, - keyAPI, nil, &cfg.MSCs, + 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 8c0bfa195..69d5fd5a8 100644 --- a/cmd/dendrite-polylith-multi/personalities/mediaapi.go +++ b/cmd/dendrite-polylith-multi/personalities/mediaapi.go @@ -25,8 +25,7 @@ func MediaAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { client := base.CreateClient() mediaapi.AddPublicRoutes( - base, base.PublicMediaAPIMux, &base.Cfg.MediaAPI, &base.Cfg.ClientAPI.RateLimiting, - userAPI, client, + base, userAPI, client, ) base.SetupAndServeHTTP( diff --git a/cmd/dendrite-polylith-multi/personalities/syncapi.go b/cmd/dendrite-polylith-multi/personalities/syncapi.go index f9f1c5a09..2245b9b54 100644 --- a/cmd/dendrite-polylith-multi/personalities/syncapi.go +++ b/cmd/dendrite-polylith-multi/personalities/syncapi.go @@ -28,9 +28,9 @@ func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { syncapi.AddPublicRoutes( base, - base.PublicClientAPIMux, userAPI, rsAPI, + userAPI, rsAPI, base.KeyServerHTTPClient(), - federation, &cfg.SyncAPI, + federation, ) base.SetupAndServeHTTP( diff --git a/cmd/dendritejs-pinecone/main.go b/cmd/dendritejs-pinecone/main.go index ead381368..e070173aa 100644 --- a/cmd/dendritejs-pinecone/main.go +++ b/cmd/dendritejs-pinecone/main.go @@ -212,16 +212,7 @@ func startup() { //ServerKeyAPI: serverKeyAPI, ExtPublicRoomsProvider: rooms.NewPineconeRoomProvider(pRouter, pSessions, fedSenderAPI, federation), } - monolith.AddAllPublicRoutes( - base, - base.PublicClientAPIMux, - base.PublicFederationAPIMux, - base.PublicKeyAPIMux, - base.PublicWellKnownAPIMux, - base.PublicMediaAPIMux, - base.SynapseAdminMux, - base.DendriteAdminMux, - ) + monolith.AddAllPublicRoutes(base) httpRouter := mux.NewRouter().SkipClean(true).UseEncodedPath() httpRouter.PathPrefix(httputil.InternalPathPrefix).Handler(base.InternalAPIMux) diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index 1848a242e..c627aab5c 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, servers federationAPI.ServersInRoomProvider, ) { - - js, _ := jetstream.Prepare(process, &cfg.Matrix.JetStream) + cfg := &base.Cfg.FederationAPI + mscCfg := &base.Cfg.MSCs + js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) producer := &producers.SyncAPIProducer{ JetStream: js, TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent), @@ -72,8 +68,11 @@ func AddPublicRoutes( } routing.Setup( - fedRouter, keyRouter, wellKnownRouter, cfg, rsAPI, - federationAPI, keyRing, + base.PublicFederationAPIMux, + base.PublicKeyAPIMux, + base.PublicWellKnownAPIMux, + cfg, + rsAPI, federationAPI, keyRing, federation, userAPI, keyAPI, mscCfg, servers, producer, ) diff --git a/federationapi/federationapi_test.go b/federationapi/federationapi_test.go index 833359c11..687241646 100644 --- a/federationapi/federationapi_test.go +++ b/federationapi/federationapi_test.go @@ -30,7 +30,7 @@ func TestRoomsV3URLEscapeDoNot404(t *testing.T) { 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, fsAPI, nil, nil) baseURL, cancel := test.ListenAndServe(t, base.PublicFederationAPIMux, true) defer cancel() serverName := gomatrixserverlib.ServerName(strings.TrimPrefix(baseURL, "https://")) diff --git a/mediaapi/mediaapi.go b/mediaapi/mediaapi.go index f2fa14384..5976957ca 100644 --- a/mediaapi/mediaapi.go +++ b/mediaapi/mediaapi.go @@ -15,11 +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/base" - "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/sirupsen/logrus" @@ -28,18 +26,18 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component. func AddPublicRoutes( base *base.BaseDendrite, - router *mux.Router, - cfg *config.MediaAPI, - rateLimit *config.RateLimiting, userAPI userapi.UserInternalAPI, client *gomatrixserverlib.Client, ) { + 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/setup/monolith.go b/setup/monolith.go index a414172ce..23bd2fb52 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" @@ -52,28 +51,24 @@ type Monolith struct { } // AddAllPublicRoutes attaches all public paths to the given router -func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite, csMux, ssMux, keyMux, wkMux, mediaMux, synapseMux, dendriteMux *mux.Router) { +func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) { userDirectoryProvider := m.ExtUserDirectoryProvider if userDirectoryProvider == nil { userDirectoryProvider = m.UserAPI } clientapi.AddPublicRoutes( - base.ProcessContext, csMux, synapseMux, dendriteMux, &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( - base.ProcessContext, 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, mediaMux, &m.Config.MediaAPI, &m.Config.ClientAPI.RateLimiting, - m.UserAPI, m.Client, + base, m.UserAPI, m.Client, ) syncapi.AddPublicRoutes( - base, csMux, m.UserAPI, m.RoomserverAPI, - m.KeyAPI, m.FedClient, &m.Config.SyncAPI, + base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, m.FedClient, ) } diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index a2b8859cd..d8becb6ed 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -17,14 +17,12 @@ 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/base" - "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/jetstream" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" @@ -42,13 +40,13 @@ import ( // component. func AddPublicRoutes( base *base.BaseDendrite, - router *mux.Router, userAPI userapi.UserInternalAPI, rsAPI api.RoomserverInternalAPI, keyAPI keyapi.KeyInternalAPI, federation *gomatrixserverlib.FederationClient, - cfg *config.SyncAPI, ) { + cfg := &base.Cfg.SyncAPI + js, natsClient := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) syncDB, err := storage.NewSyncServerDatasource(base, &cfg.Database) @@ -148,5 +146,8 @@ func AddPublicRoutes( logrus.WithError(err).Panicf("failed to start presence consumer") } - routing.Setup(router, requestPool, syncDB, userAPI, federation, rsAPI, cfg, lazyLoadCache) + routing.Setup( + base.PublicClientAPIMux, requestPool, syncDB, userAPI, + federation, rsAPI, cfg, lazyLoadCache, + ) } From e01d1e1f5b39677e8817bda7cc946b3c8fcf9a4a Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 3 May 2022 17:38:54 +0100 Subject: [PATCH 047/103] Skip tests that require a database if we can't connect to one (#2413) * Skip tests that require a database if we can't connect to one * Add `DENDRITE_SKIP_DB_TESTS` environment variable to bring @kegsay joy * Call it `DENDRITE_TEST_SKIP_NODB` intead --- test/db.go | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/test/db.go b/test/db.go index 6412feaa6..fecae5d48 100644 --- a/test/db.go +++ b/test/db.go @@ -33,10 +33,19 @@ var DBTypeSQLite DBType = 1 var DBTypePostgres DBType = 2 var Quiet = false +var Required = os.Getenv("DENDRITE_TEST_SKIP_NODB") == "" -func createLocalDB(dbName string) { +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 !Quiet { - fmt.Println("Note: tests require a postgres install accessible to the current user") + t.Log("Note: tests require a postgres install accessible to the current user") } createDB := exec.Command("createdb", dbName) if !Quiet { @@ -44,15 +53,15 @@ func createLocalDB(dbName string) { createDB.Stderr = os.Stderr } err := createDB.Run() - if err != nil && !Quiet { - fmt.Println("createLocalDB returned error:", err) + if err != nil { + fatalError(t, "createLocalDB returned error: %s", err) } } 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) } _, err = db.Exec(fmt.Sprintf(`CREATE DATABASE %s;`, dbName)) if err != nil { @@ -133,7 +142,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) } From b0a9e85c4a02f39880682d9d682f9cc7af13a93c Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 3 May 2022 17:40:56 +0100 Subject: [PATCH 048/103] Fix bug in database global setup --- setup/base/base.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup/base/base.go b/setup/base/base.go index 9b227b70b..3641ad780 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -201,7 +201,8 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base if cfg.Global.DatabaseOptions.ConnectionString.IsSQLite() { logrus.Panic("Using a global database connection pool is not supported with SQLite databases") } - if db, err = sqlutil.Open(&cfg.Global.DatabaseOptions, sqlutil.NewDummyWriter()); err != nil { + 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") From 3c940c428d529476b6fa2cbf1ba28d53ec011584 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Wed, 4 May 2022 19:04:28 +0200 Subject: [PATCH 049/103] Add opt-in anonymous stats reporting (#2249) * Initial phone home stats queries * Add userAgent to UpdateDeviceLastSeen Add new Table for tracking daily user vists * Add user_daily_visits table * Fix queries * userapi stats tables & queries * userapi interface and internal api * sycnapi stats queries * testing phone home stats * Add complete config to syncapi * add missing files * Fix queries * Send empty request * Add version & monolith stats * Add configuration for phone home stats * Move WASM to its own file, add config and comments * Add tracing methods * Add total rooms * Add more fields, actually send data somewhere * Move stats to the userapi * Move phone home stats to util package * Cleanup * Linter & parts of GH comments * More GH comments changes - Move comments to SQL statements - Shrink interface, add struct for stats - No fatal errors, use defaults * Be more explicit when querying * Fix wrong calculation & wrong query params Add tests * Add Windows stats * ADd build constraint * Use new testing structure Fix issues with getting values when using SQLite Fix wrong AddDate value Export UpdateUserDailyVisits * Fix query params * Fix test * Add comment about countR30UsersSQL and countR30UsersV2SQL; fix test * Update config * Also update example config file * Use OS level proxy, update logging Co-authored-by: kegsay --- cmd/dendrite-polylith-multi/main.go | 2 +- dendrite-config.yaml | 9 + setup/config/config.go | 3 + setup/config/config_global.go | 25 ++ syncapi/sync/requestpool.go | 1 + userapi/api/api.go | 1 + userapi/internal/api.go | 2 +- userapi/storage/interface.go | 8 +- userapi/storage/postgres/devices_table.go | 6 +- userapi/storage/postgres/stats_table.go | 437 ++++++++++++++++++++ userapi/storage/postgres/storage.go | 5 + userapi/storage/shared/storage.go | 13 +- userapi/storage/sqlite3/devices_table.go | 6 +- userapi/storage/sqlite3/stats_table.go | 452 +++++++++++++++++++++ userapi/storage/sqlite3/storage.go | 5 + userapi/storage/storage_test.go | 2 +- userapi/storage/tables/interface.go | 9 +- userapi/storage/tables/stats_table_test.go | 319 +++++++++++++++ userapi/types/statistics.go | 30 ++ userapi/userapi.go | 5 + userapi/util/phonehomestats.go | 160 ++++++++ userapi/util/stats.go | 47 +++ userapi/util/stats_wasm.go | 20 + userapi/util/stats_windows.go | 29 ++ 24 files changed, 1582 insertions(+), 14 deletions(-) create mode 100644 userapi/storage/postgres/stats_table.go create mode 100644 userapi/storage/sqlite3/stats_table.go create mode 100644 userapi/storage/tables/stats_table_test.go create mode 100644 userapi/types/statistics.go create mode 100644 userapi/util/phonehomestats.go create mode 100644 userapi/util/stats.go create mode 100644 userapi/util/stats_wasm.go create mode 100644 userapi/util/stats_windows.go diff --git a/cmd/dendrite-polylith-multi/main.go b/cmd/dendrite-polylith-multi/main.go index 6226cc328..4fccaa922 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 { diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 1647af15d..7709e0c87 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -85,6 +85,15 @@ global: # Whether outbound presence events are allowed, e.g. sending presence events to other servers enable_outbound: false + # Configures opt-in anonymous stats reporting. + report_stats: + # Whether this instance sends anonymous usage stats + enabled: false + + # The endpoint to report the anonymized homeserver usage statistics to. + # Defaults to https://matrix.org/report-usage-stats/push + endpoint: https://matrix.org/report-usage-stats/push + # Server notices allows server admins to send messages to all users. server_notices: enabled: false 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_global.go b/setup/config/config_global.go index d609e2460..9d4c1485e 100644 --- a/setup/config/config_global.go +++ b/setup/config/config_global.go @@ -70,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) { @@ -86,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) { @@ -97,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 { @@ -163,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/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 76d550a65..f8e502d2c 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -182,6 +182,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 diff --git a/userapi/api/api.go b/userapi/api/api.go index 6aa6a6842..6ab68fa08 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -320,6 +320,7 @@ type PerformLastSeenUpdateRequest struct { UserID string DeviceID string RemoteAddr string + UserAgent string } // PerformLastSeenUpdateResponse is the response for PerformLastSeenUpdate. diff --git a/userapi/internal/api.go b/userapi/internal/api.go index be58e2d8d..394bfa224 100644 --- a/userapi/internal/api.go +++ b/userapi/internal/api.go @@ -210,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 a4562cf19..f7cd1810a 100644 --- a/userapi/storage/interface.go +++ b/userapi/storage/interface.go @@ -22,6 +22,7 @@ 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 { @@ -67,7 +68,7 @@ type Device 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 + 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) @@ -135,9 +136,14 @@ type Database interface { 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/devices_table.go b/userapi/storage/postgres/devices_table.go index 6c777982f..ccb776672 100644 --- a/userapi/storage/postgres/devices_table.go +++ b/userapi/storage/postgres/devices_table.go @@ -96,7 +96,7 @@ const selectDevicesByIDSQL = "" + "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 @@ -304,9 +304,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..f71900015 --- /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 user_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 localpart_device_timestamp_idx ON user_daily_visits(localpart, device_id, timestamp); +CREATE INDEX IF NOT EXISTS timestamp_idx ON user_daily_visits(timestamp); +CREATE INDEX IF NOT EXISTS localpart_timestamp_idx ON user_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 user_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 user_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 user_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 74100a728..b9afb5a56 100644 --- a/userapi/storage/postgres/storage.go +++ b/userapi/storage/postgres/storage.go @@ -94,6 +94,10 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, 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, @@ -106,6 +110,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, ThreePIDs: threePIDTable, Pushers: pusherTable, Notifications: notificationsTable, + Stats: statsTable, ServerName: serverName, DB: db, Writer: writer, diff --git a/userapi/storage/shared/storage.go b/userapi/storage/shared/storage.go index f7212e030..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 @@ -611,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) }) } @@ -756,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/devices_table.go b/userapi/storage/sqlite3/devices_table.go index b86ed1cc2..93291e6ad 100644 --- a/userapi/storage/sqlite3/devices_table.go +++ b/userapi/storage/sqlite3/devices_table.go @@ -81,7 +81,7 @@ const selectDevicesByIDSQL = "" + "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 @@ -306,9 +306,9 @@ func (s *devicesStatements) SelectDevicesByID(ctx context.Context, deviceIDs []s 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..af4c7ff98 --- /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 user_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 localpart_device_timestamp_idx ON user_daily_visits(localpart, device_id, timestamp); +CREATE INDEX IF NOT EXISTS timestamp_idx ON user_daily_visits(timestamp); +CREATE INDEX IF NOT EXISTS localpart_timestamp_idx ON user_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 user_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 user_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 user_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 6858d3d15..a822f687d 100644 --- a/userapi/storage/sqlite3/storage.go +++ b/userapi/storage/sqlite3/storage.go @@ -95,6 +95,10 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, 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, @@ -107,6 +111,7 @@ func NewDatabase(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, ThreePIDs: threePIDTable, Pushers: pusherTable, Notifications: notificationsTable, + Stats: statsTable, ServerName: serverName, DB: db, Writer: writer, diff --git a/userapi/storage/storage_test.go b/userapi/storage/storage_test.go index 79d5a8dae..5683fe067 100644 --- a/userapi/storage/storage_test.go +++ b/userapi/storage/storage_test.go @@ -174,7 +174,7 @@ func Test_Devices(t *testing.T) { 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") + 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 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 9174119e1..5b11665db 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" ) @@ -104,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/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 +} From d86dcbef66dad344bc38c58762a9634ff126d5c7 Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 5 May 2022 09:56:03 +0100 Subject: [PATCH 050/103] syncapi: define specific interfaces for internal HTTP communications (#2416) * syncapi: use finer-grained interfaces when making the syncapi * Use specific interfaces for syncapi-roomserver interactions * Define query access token api for shared http auth code --- clientapi/auth/auth.go | 2 +- .../personalities/syncapi.go | 2 - internal/httputil/httpapi.go | 2 +- keyserver/api/api.go | 9 ++- roomserver/api/api.go | 81 ++++++++++--------- setup/monolith.go | 2 +- syncapi/consumers/keychange.go | 7 +- syncapi/consumers/presence.go | 4 +- syncapi/consumers/roomserver.go | 4 +- syncapi/internal/keychange.go | 8 +- syncapi/routing/context.go | 2 +- syncapi/routing/messages.go | 9 +-- syncapi/routing/routing.go | 6 +- syncapi/streams/stream_accountdata.go | 2 +- syncapi/streams/stream_devicelist.go | 4 +- syncapi/streams/stream_pdu.go | 2 +- syncapi/streams/streams.go | 4 +- syncapi/sync/requestpool.go | 10 +-- syncapi/syncapi.go | 12 ++- userapi/api/api.go | 14 +++- 20 files changed, 95 insertions(+), 91 deletions(-) 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/cmd/dendrite-polylith-multi/personalities/syncapi.go b/cmd/dendrite-polylith-multi/personalities/syncapi.go index 2245b9b54..41637fe1d 100644 --- a/cmd/dendrite-polylith-multi/personalities/syncapi.go +++ b/cmd/dendrite-polylith-multi/personalities/syncapi.go @@ -22,7 +22,6 @@ import ( func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { userAPI := base.UserAPIClient() - federation := base.CreateFederationClient() rsAPI := base.RoomserverHTTPClient() @@ -30,7 +29,6 @@ func SyncAPI(base *basepkg.BaseDendrite, cfg *config.Dendrite) { base, userAPI, rsAPI, base.KeyServerHTTPClient(), - federation, ) base.SetupAndServeHTTP( diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go index 5fcacd2ad..3a818cc5e 100644 --- a/internal/httputil/httpapi.go +++ b/internal/httputil/httpapi.go @@ -49,7 +49,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 { diff --git a/keyserver/api/api.go b/keyserver/api/api.go index 429617b10..ce651ba4e 100644 --- a/keyserver/api/api.go +++ b/keyserver/api/api.go @@ -27,6 +27,7 @@ import ( ) type KeyInternalAPI interface { + SyncKeyAPI // SetUserAPI assigns a user API to query when extracting device names. SetUserAPI(i userapi.UserInternalAPI) // InputDeviceListUpdate from a federated server EDU @@ -38,12 +39,16 @@ type KeyInternalAPI interface { PerformUploadDeviceKeys(ctx context.Context, req *PerformUploadDeviceKeysRequest, res *PerformUploadDeviceKeysResponse) PerformUploadDeviceSignatures(ctx context.Context, req *PerformUploadDeviceSignaturesRequest, res *PerformUploadDeviceSignaturesResponse) QueryKeys(ctx context.Context, req *QueryKeysRequest, res *QueryKeysResponse) - QueryKeyChanges(ctx context.Context, req *QueryKeyChangesRequest, res *QueryKeyChangesResponse) - QueryOneTimeKeys(ctx context.Context, req *QueryOneTimeKeysRequest, res *QueryOneTimeKeysResponse) QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) } +// 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) +} + // KeyError is returned if there was a problem performing/querying the server type KeyError struct { Err string `json:"error"` diff --git a/roomserver/api/api.go b/roomserver/api/api.go index f0ca8a615..2e4ec3ffd 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -12,6 +12,8 @@ import ( // RoomserverInputAPI is used to write events to the room server. type RoomserverInternalAPI interface { + SyncRoomserverAPI + // 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) @@ -78,34 +80,6 @@ type RoomserverInternalAPI interface { res *QueryPublishedRoomsResponse, ) error - // Query the latest events and state for a room from the room server. - QueryLatestEventsAndState( - ctx context.Context, - request *QueryLatestEventsAndStateRequest, - response *QueryLatestEventsAndStateResponse, - ) error - - // Query the state after a list of events in a room from the room server. - QueryStateAfterEvents( - ctx context.Context, - request *QueryStateAfterEventsRequest, - response *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, @@ -157,22 +131,11 @@ type RoomserverInternalAPI interface { 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, - ) error - // PerformForget forgets a rooms history for a specific user PerformForget(ctx context.Context, req *PerformForgetRequest, resp *PerformForgetResponse) error @@ -228,3 +191,43 @@ type RoomserverInternalAPI interface { response *RemoveRoomAliasResponse, ) error } + +// API functions required by the syncapi +type SyncRoomserverAPI interface { + // Query the latest events and state for a room from the room server. + QueryLatestEventsAndState( + ctx context.Context, + request *QueryLatestEventsAndStateRequest, + response *QueryLatestEventsAndStateResponse, + ) 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 + // 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 the state after a list of events in a room from the room server. + QueryStateAfterEvents( + ctx context.Context, + request *QueryStateAfterEventsRequest, + response *QueryStateAfterEventsResponse, + ) error + + // Query a given amount (or less) of events prior to a given set of events. + PerformBackfill( + ctx context.Context, + request *PerformBackfillRequest, + response *PerformBackfillResponse, + ) error +} diff --git a/setup/monolith.go b/setup/monolith.go index 23bd2fb52..e033c14d7 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -69,6 +69,6 @@ func (m *Monolith) AddAllPublicRoutes(base *base.BaseDendrite) { base, m.UserAPI, m.Client, ) syncapi.AddPublicRoutes( - base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, m.FedClient, + base, m.UserAPI, m.RoomserverAPI, m.KeyAPI, ) } 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 6bcca48f4..388c08ff4 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(), diff --git a/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go index 5bdc0fad7..7712c8403 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{ 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/routing/context.go b/syncapi/routing/context.go index 17215b669..f5f4b2dd0 100644 --- a/syncapi/routing/context.go +++ b/syncapi/routing/context.go @@ -42,7 +42,7 @@ 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, diff --git a/syncapi/routing/messages.go b/syncapi/routing/messages.go index f34901bf2..f19dfaed3 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,8 +60,7 @@ 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, @@ -180,7 +178,6 @@ func OnIncomingMessagesRequest( ctx: req.Context(), db: db, rsAPI: rsAPI, - federation: federation, cfg: cfg, roomID: roomID, from: &from, @@ -247,7 +244,7 @@ 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) (bool, error) { req := api.QueryMembershipForUserRequest{ RoomID: roomID, UserID: userID, diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go index 4102cf073..245ee5b66 100644 --- a/syncapi/routing/routing.go +++ b/syncapi/routing/routing.go @@ -36,8 +36,8 @@ 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, ) { @@ -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/streams/stream_accountdata.go b/syncapi/streams/stream_accountdata.go index 2cddbcf04..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() { 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 0d033095d..f774a1af8 100644 --- a/syncapi/streams/stream_pdu.go +++ b/syncapi/streams/stream_pdu.go @@ -33,7 +33,7 @@ type PDUStreamProvider struct { workers atomic.Int32 // userID+deviceID -> lazy loading cache lazyLoadCache *caching.LazyLoadCache - rsAPI roomserverAPI.RoomserverInternalAPI + rsAPI roomserverAPI.SyncRoomserverAPI } func (p *PDUStreamProvider) worker() { diff --git a/syncapi/streams/streams.go b/syncapi/streams/streams.go index a18a0cc41..af2a0387e 100644 --- a/syncapi/streams/streams.go +++ b/syncapi/streams/streams.go @@ -25,8 +25,8 @@ type Streams struct { } func NewSyncStreamProviders( - d storage.Database, userAPI userapi.UserInternalAPI, - rsAPI rsapi.RoomserverInternalAPI, keyAPI keyapi.KeyInternalAPI, + d storage.Database, userAPI userapi.SyncUserAPI, + rsAPI rsapi.SyncRoomserverAPI, keyAPI keyapi.SyncKeyAPI, eduCache *caching.EDUCache, lazyLoadCache *caching.LazyLoadCache, notifier *notifier.Notifier, ) *Streams { streams := &Streams{ diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index f8e502d2c..99d1e40c3 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -45,9 +45,9 @@ 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 @@ -62,8 +62,8 @@ type PresencePublisher interface { // 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, ) *RequestPool { diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index d8becb6ed..686e2044f 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -25,7 +25,6 @@ import ( "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/jetstream" 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" @@ -40,10 +39,9 @@ import ( // component. func AddPublicRoutes( base *base.BaseDendrite, - userAPI userapi.UserInternalAPI, - rsAPI api.RoomserverInternalAPI, - keyAPI keyapi.KeyInternalAPI, - federation *gomatrixserverlib.FederationClient, + userAPI userapi.SyncUserAPI, + rsAPI api.SyncRoomserverAPI, + keyAPI keyapi.SyncKeyAPI, ) { cfg := &base.Cfg.SyncAPI @@ -85,7 +83,7 @@ func AddPublicRoutes( keyChangeConsumer := consumers.NewOutputKeyChangeEventConsumer( base.ProcessContext, cfg, cfg.Matrix.JetStream.Prefixed(jetstream.OutputKeyChangeEvent), - js, keyAPI, rsAPI, syncDB, notifier, + js, rsAPI, syncDB, notifier, streams.DeviceListStreamProvider, ) if err = keyChangeConsumer.Start(); err != nil { @@ -148,6 +146,6 @@ func AddPublicRoutes( routing.Setup( base.PublicClientAPIMux, requestPool, syncDB, userAPI, - federation, rsAPI, cfg, lazyLoadCache, + rsAPI, cfg, lazyLoadCache, ) } diff --git a/userapi/api/api.go b/userapi/api/api.go index 6ab68fa08..6f00fe44f 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -31,7 +31,8 @@ type UserInternalAPI interface { UserRegisterAPI UserAccountAPI UserThreePIDAPI - UserDeviceAPI + QueryAcccessTokenAPI + SyncUserAPI InputAccountData(ctx context.Context, req *InputAccountDataRequest, res *InputAccountDataResponse) error @@ -42,15 +43,20 @@ type UserInternalAPI interface { 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 } -type UserDeviceAPI interface { +type QueryAcccessTokenAPI interface { + QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error +} + +type SyncUserAPI interface { + QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error + QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) error PerformLastSeenUpdate(ctx context.Context, req *PerformLastSeenUpdateRequest, res *PerformLastSeenUpdateResponse) error PerformDeviceUpdate(ctx context.Context, req *PerformDeviceUpdateRequest, res *PerformDeviceUpdateResponse) error From 5c37f165ae5301ee1f41c1491bed7fcc7c439c40 Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 5 May 2022 10:53:52 +0100 Subject: [PATCH 051/103] Errors from createdb are non-fatal (#2420) As they are expected if the database already exists. --- test/db.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/db.go b/test/db.go index fecae5d48..a1754cd08 100644 --- a/test/db.go +++ b/test/db.go @@ -53,8 +53,8 @@ func createLocalDB(t *testing.T, dbName string) { createDB.Stderr = os.Stderr } err := createDB.Run() - if err != nil { - fatalError(t, "createLocalDB returned error: %s", err) + if err != nil && !Quiet { + fmt.Println("createLocalDB returned error:", err) } } From 658e82f8bcfc78d8489f9687e88fb712af0ea75f Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Thu, 5 May 2022 12:00:18 +0200 Subject: [PATCH 052/103] Don't use in-memory db for userapi tests (#2417) * Don't use in-memory db * Use WithAllDatabases where possible --- userapi/userapi_test.go | 205 +++++++++++++++++++++------------------- 1 file changed, 109 insertions(+), 96 deletions(-) diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go index 64e23909a..e614765a2 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,17 @@ import ( "time" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/internal/httputil" + internalTest "github.com/matrix-org/dendrite/internal/test" + "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 +45,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.NewUserAPIDatabase(nil, 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 +67,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,8 +134,8 @@ func TestQueryProfile(t *testing.T) { t.Run("HTTP API", func(t *testing.T) { router := mux.NewRouter().PathPrefix(httputil.InternalPathPrefix).Subrouter() - AddInternalRoutes(router, userAPI) - apiURL, cancel := test.ListenAndServe(t, router, false) + userapi.AddInternalRoutes(router, userAPI) + apiURL, cancel := internalTest.ListenAndServe(t, router, false) defer cancel() httpAPI, err := inthttp.NewUserAPIClient(apiURL, &http.Client{}) if err != nil { @@ -149,110 +152,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) + } + }) }) } From 1bfe87aa5614d68cbb0cad127b375048cdc70ca9 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Thu, 5 May 2022 12:01:28 +0200 Subject: [PATCH 053/103] Fix user already joined when using server notices (#2364) --- clientapi/routing/server_notices.go | 49 +++++++++++++---------------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/clientapi/routing/server_notices.go b/clientapi/routing/server_notices.go index eec3d7e38..47b0da7bb 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" @@ -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 + } } } From d9e71b93b68efb57582d02448883b8a1259205e8 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 5 May 2022 11:33:16 +0100 Subject: [PATCH 054/103] Use `gomatrixserverlib.Client` instead of `http.Client` (#2421) * Update to matrix-org/gomatrixserverlib#303 * Use `gomatrixserverlib.Client` for phone-home stats * Use `gomatrixserverlib.Client` for push notifications * Use `gomatrixserverlib.Client` for appservices * Use `gomatrixserverlib.Client` for three-PID invites --- appservice/appservice.go | 18 +++++++----------- appservice/query/query.go | 8 ++++---- appservice/workers/transaction_scheduler.go | 8 ++++---- clientapi/threepid/invites.go | 4 ++-- go.mod | 4 ++-- go.sum | 4 ++-- internal/pushgateway/client.go | 21 +++++++++------------ userapi/util/phonehomestats.go | 12 +++++------- 8 files changed, 35 insertions(+), 44 deletions(-) diff --git a/appservice/appservice.go b/appservice/appservice.go index 0db2c1009..ac4ed967e 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -16,8 +16,6 @@ package appservice import ( "context" - "crypto/tls" - "net/http" "sync" "time" @@ -36,6 +34,7 @@ import ( "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/dendrite/setup/jetstream" userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" ) // AddInternalRoutes registers HTTP handlers for internal API calls @@ -50,15 +49,12 @@ func NewInternalAPI( userAPI userapi.UserInternalAPI, rsAPI roomserverAPI.RoomserverInternalAPI, ) appserviceAPI.AppServiceQueryAPI { - client := &http.Client{ - Timeout: time.Second * 30, - Transport: &http.Transport{ - DisableKeepAlives: true, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation, - }, - }, - } + client := gomatrixserverlib.NewClient( + gomatrixserverlib.WithTimeout(time.Second*30), + gomatrixserverlib.WithKeepAlives(false), + gomatrixserverlib.WithSkipVerify(base.Cfg.AppServiceAPI.DisableTLSValidation), + ) + js, _ := jetstream.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) // Create a connection to the appservice postgres DB diff --git a/appservice/query/query.go b/appservice/query/query.go index dacd3caa8..b7b0b335a 100644 --- a/appservice/query/query.go +++ b/appservice/query/query.go @@ -23,6 +23,7 @@ import ( "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/gomatrixserverlib" opentracing "github.com/opentracing/opentracing-go" log "github.com/sirupsen/logrus" ) @@ -32,7 +33,7 @@ const userIDExistsPath = "/users/" // AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI type AppServiceQueryAPI struct { - HTTPClient *http.Client + HTTPClient *gomatrixserverlib.Client Cfg *config.Dendrite } @@ -64,9 +65,8 @@ func (a *AppServiceQueryAPI) RoomAliasExists( if err != nil { return err } - req = req.WithContext(ctx) - resp, err := a.HTTPClient.Do(req) + resp, err := a.HTTPClient.DoHTTPRequest(ctx, req) if resp != nil { defer func() { err = resp.Body.Close() @@ -130,7 +130,7 @@ func (a *AppServiceQueryAPI) UserIDExists( if err != nil { return err } - resp, err := a.HTTPClient.Do(req.WithContext(ctx)) + resp, err := a.HTTPClient.DoHTTPRequest(ctx, req) if resp != nil { defer func() { err = resp.Body.Close() diff --git a/appservice/workers/transaction_scheduler.go b/appservice/workers/transaction_scheduler.go index 4dab00bd7..47d447c2c 100644 --- a/appservice/workers/transaction_scheduler.go +++ b/appservice/workers/transaction_scheduler.go @@ -42,7 +42,7 @@ var ( // size), then send that off to the AS's /transactions/{txnID} endpoint. It also // handles exponentially backing off in case the AS isn't currently available. func SetupTransactionWorkers( - client *http.Client, + client *gomatrixserverlib.Client, appserviceDB storage.Database, workerStates []types.ApplicationServiceWorkerState, ) error { @@ -58,7 +58,7 @@ func SetupTransactionWorkers( // worker is a goroutine that sends any queued events to the application service // it is given. -func worker(client *http.Client, db storage.Database, ws types.ApplicationServiceWorkerState) { +func worker(client *gomatrixserverlib.Client, db storage.Database, ws types.ApplicationServiceWorkerState) { log.WithFields(log.Fields{ "appservice": ws.AppService.ID, }).Info("Starting application service") @@ -200,7 +200,7 @@ func createTransaction( // send sends events to an application service. Returns an error if an OK was not // received back from the application service or the request timed out. func send( - client *http.Client, + client *gomatrixserverlib.Client, appservice config.ApplicationService, txnID int, transaction []byte, @@ -213,7 +213,7 @@ func send( return err } req.Header.Set("Content-Type", "application/json") - resp, err := client.Do(req) + resp, err := client.DoHTTPRequest(context.TODO(), req) if err != nil { return err } diff --git a/clientapi/threepid/invites.go b/clientapi/threepid/invites.go index 6b750199b..eee6992f8 100644 --- a/clientapi/threepid/invites.go +++ b/clientapi/threepid/invites.go @@ -231,7 +231,7 @@ func queryIDServerStoreInvite( profile = &authtypes.Profile{} } - client := http.Client{} + client := gomatrixserverlib.NewClient() data := url.Values{} data.Add("medium", body.Medium) @@ -253,7 +253,7 @@ func queryIDServerStoreInvite( } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := client.Do(req.WithContext(ctx)) + resp, err := client.DoHTTPRequest(ctx, req) if err != nil { return nil, err } diff --git a/go.mod b/go.mod index a7caadfb5..56f5dcbbb 100644 --- a/go.mod +++ b/go.mod @@ -25,12 +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 // 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-20220505092512-c4ceb4751ac2 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 diff --git a/go.sum b/go.sum index f8daca79e..fccda40c4 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220505092512-c4ceb4751ac2 h1:5/Y4BpiMk1D/l/HkJz8Ng8bLBz1BHwV6V4e+yMNySzk= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220505092512-c4ceb4751ac2/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= diff --git a/internal/pushgateway/client.go b/internal/pushgateway/client.go index 49907cee8..231327a1e 100644 --- a/internal/pushgateway/client.go +++ b/internal/pushgateway/client.go @@ -3,31 +3,28 @@ package pushgateway import ( "bytes" "context" - "crypto/tls" "encoding/json" "fmt" "net/http" "time" + "github.com/matrix-org/gomatrixserverlib" "github.com/opentracing/opentracing-go" ) type httpClient struct { - hc *http.Client + hc *gomatrixserverlib.Client } // NewHTTPClient creates a new Push Gateway client. func NewHTTPClient(disableTLSValidation bool) Client { - hc := &http.Client{ - Timeout: 30 * time.Second, - Transport: &http.Transport{ - DisableKeepAlives: true, - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: disableTLSValidation, - }, - }, + return &httpClient{ + hc: gomatrixserverlib.NewClient( + gomatrixserverlib.WithTimeout(time.Second*30), + gomatrixserverlib.WithKeepAlives(false), + gomatrixserverlib.WithSkipVerify(disableTLSValidation), + ), } - return &httpClient{hc: hc} } func (h *httpClient) Notify(ctx context.Context, url string, req *NotifyRequest, resp *NotifyResponse) error { @@ -44,7 +41,7 @@ func (h *httpClient) Notify(ctx context.Context, url string, req *NotifyRequest, } hreq.Header.Set("Content-Type", "application/json") - hresp, err := h.hc.Do(hreq) + hresp, err := h.hc.DoHTTPRequest(ctx, hreq) if err != nil { return err } diff --git a/userapi/util/phonehomestats.go b/userapi/util/phonehomestats.go index ad93a50e3..e24daba6b 100644 --- a/userapi/util/phonehomestats.go +++ b/userapi/util/phonehomestats.go @@ -39,7 +39,7 @@ type phoneHomeStats struct { cfg *config.Dendrite db storage.Statistics isMonolith bool - client *http.Client + client *gomatrixserverlib.Client } type timestampToRUUsage struct { @@ -55,10 +55,9 @@ func StartPhoneHomeCollector(startTime time.Time, cfg *config.Dendrite, statsDB cfg: cfg, db: statsDB, isMonolith: cfg.IsMonolith, - client: &http.Client{ - Timeout: time.Second * 30, - Transport: http.DefaultTransport, - }, + client: gomatrixserverlib.NewClient( + gomatrixserverlib.WithTimeout(time.Second * 30), + ), } // start initial run after 5min @@ -152,8 +151,7 @@ func (p *phoneHomeStats) collect() { } request.Header.Set("User-Agent", "Dendrite/"+internal.VersionString()) - _, err = p.client.Do(request) - if err != nil { + if _, err = p.client.DoHTTPRequest(ctx, request); err != nil { logrus.WithError(err).Error("unable to send anonymous stats") return } From 506de4bb3d69fef27e60d09d06712a72e588a198 Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 5 May 2022 13:17:38 +0100 Subject: [PATCH 055/103] Define component interfaces based on consumers (1/2) (#2423) * Specify interfaces used by appservice, do half of clientapi * convert more deps of clientapi to finer-grained interfaces * Convert mediaapi and rest of clientapi * Somehow this got missed --- appservice/api/query.go | 2 +- appservice/appservice.go | 6 +- appservice/consumers/roomserver.go | 4 +- clientapi/auth/login.go | 2 +- clientapi/auth/login_test.go | 5 +- clientapi/auth/user_interactive.go | 2 +- clientapi/auth/user_interactive_test.go | 4 +- clientapi/clientapi.go | 8 +- clientapi/producers/syncapi.go | 2 +- clientapi/routing/account_data.go | 6 +- clientapi/routing/admin.go | 2 +- clientapi/routing/admin_whois.go | 2 +- clientapi/routing/aliases.go | 2 +- clientapi/routing/capabilities.go | 2 +- clientapi/routing/createroom.go | 30 ++-- clientapi/routing/deactivate.go | 2 +- clientapi/routing/device.go | 10 +- clientapi/routing/directory.go | 12 +- clientapi/routing/directory_public.go | 6 +- clientapi/routing/getevent.go | 5 +- clientapi/routing/joinroom.go | 4 +- clientapi/routing/key_backup.go | 12 +- clientapi/routing/key_crosssigning.go | 6 +- clientapi/routing/keys.go | 6 +- clientapi/routing/leaveroom.go | 2 +- clientapi/routing/login.go | 4 +- clientapi/routing/logout.go | 4 +- clientapi/routing/membership.go | 74 ++++----- clientapi/routing/memberships.go | 4 +- clientapi/routing/notification.go | 2 +- clientapi/routing/openid.go | 2 +- clientapi/routing/password.go | 2 +- clientapi/routing/peekroom.go | 4 +- clientapi/routing/profile.go | 18 +-- clientapi/routing/pusher.go | 4 +- clientapi/routing/pushrules.go | 20 +-- clientapi/routing/redaction.go | 2 +- clientapi/routing/register.go | 16 +- clientapi/routing/room_tagging.go | 10 +- clientapi/routing/routing.go | 12 +- clientapi/routing/sendevent.go | 4 +- clientapi/routing/sendtyping.go | 2 +- clientapi/routing/server_notices.go | 6 +- clientapi/routing/state.go | 4 +- clientapi/routing/threepid.go | 8 +- clientapi/routing/upgrade_room.go | 4 +- clientapi/routing/userdirectory.go | 14 +- clientapi/threepid/invites.go | 14 +- federationapi/api/api.go | 5 + federationapi/routing/invite.go | 49 +++--- internal/eventutil/events.go | 4 +- keyserver/api/api.go | 18 ++- mediaapi/mediaapi.go | 2 +- mediaapi/routing/routing.go | 2 +- roomserver/api/api.go | 190 +++++++++++++++--------- roomserver/api/wrapper.go | 42 +----- userapi/api/api.go | 96 ++++++------ 57 files changed, 414 insertions(+), 372 deletions(-) diff --git a/appservice/api/query.go b/appservice/api/query.go index cf25a9616..6db8be85b 100644 --- a/appservice/api/query.go +++ b/appservice/api/query.go @@ -84,7 +84,7 @@ func RetrieveUserProfile( ctx context.Context, userID string, asAPI AppServiceQueryAPI, - profileAPI userapi.UserProfileAPI, + 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 ac4ed967e..e026a7875 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -46,8 +46,8 @@ func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceQuer // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( base *base.BaseDendrite, - userAPI userapi.UserInternalAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + userAPI userapi.AppserviceUserAPI, + rsAPI roomserverAPI.AppserviceRoomserverAPI, ) appserviceAPI.AppServiceQueryAPI { client := gomatrixserverlib.NewClient( gomatrixserverlib.WithTimeout(time.Second*30), @@ -113,7 +113,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/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 0d16e4c14..957d082a6 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -33,13 +33,13 @@ import ( func AddPublicRoutes( base *base.BaseDendrite, federation *gomatrixserverlib.FederationClient, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, transactionsCache *transactions.Cache, - fsAPI federationAPI.FederationInternalAPI, - userAPI userapi.UserInternalAPI, + fsAPI federationAPI.ClientFederationAPI, + userAPI userapi.ClientUserAPI, userDirectoryProvider userapi.UserDirectoryProvider, - keyAPI keyserverAPI.KeyInternalAPI, + keyAPI keyserverAPI.ClientKeyAPI, extRoomsProvider api.ExtraPublicRoomsProvider, ) { cfg := &base.Cfg.ClientAPI 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 index 31e431c78..125b3847d 100644 --- a/clientapi/routing/admin.go +++ b/clientapi/routing/admin.go @@ -11,7 +11,7 @@ import ( "github.com/matrix-org/util" ) -func AdminEvacuateRoom(req *http.Request, device *userapi.Device, rsAPI roomserverAPI.RoomserverInternalAPI) util.JSONResponse { +func AdminEvacuateRoom(req *http.Request, device *userapi.Device, rsAPI roomserverAPI.ClientRoomserverAPI) util.JSONResponse { if device.AccountType != userapi.AccountTypeAdmin { return util.JSONResponse{ Code: http.StatusForbidden, 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..a21abb0eb 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -137,7 +137,7 @@ type fledglingEvent struct { func CreateRoom( req *http.Request, device *api.Device, cfg *config.ClientAPI, - profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, ) util.JSONResponse { var r createRoomRequest @@ -164,7 +164,7 @@ func createRoom( ctx context.Context, r createRoomRequest, device *api.Device, cfg *config.ClientAPI, - profileAPI api.UserProfileAPI, rsAPI roomserverAPI.RoomserverInternalAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, evTime time.Time, ) util.JSONResponse { @@ -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..7d91c7b03 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.AppServiceQueryAPI, ) 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.AppServiceQueryAPI) 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.AppServiceQueryAPI, ) 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.AppServiceQueryAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -199,9 +200,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.AppServiceQueryAPI, ) util.JSONResponse { body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -233,11 +234,11 @@ 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, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, evTime time.Time, ) (util.JSONResponse, error) { event, err := buildMembershipEvent( @@ -259,37 +260,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.AppServiceQueryAPI, ) (*gomatrixserverlib.HeaderedEvent, error) { profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI) if err != nil { @@ -326,7 +326,7 @@ func loadProfile( ctx context.Context, userID string, cfg *config.ClientAPI, - profileAPI userapi.UserProfileAPI, + profileAPI userapi.ClientUserAPI, asAPI appserviceAPI.AppServiceQueryAPI, ) (*authtypes.Profile, error) { _, serverName, err := gomatrixserverlib.SplitID('@', userID) @@ -344,7 +344,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 +379,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 +418,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 +457,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) 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..97f86afe2 100644 --- a/clientapi/routing/profile.go +++ b/clientapi/routing/profile.go @@ -35,7 +35,7 @@ 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, federation *gomatrixserverlib.FederationClient, @@ -64,7 +64,7 @@ func GetProfile( // GetAvatarURL implements GET /profile/{userID}/avatar_url func GetAvatarURL( - req *http.Request, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, userID string, asAPI appserviceAPI.AppServiceQueryAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { @@ -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,7 +193,7 @@ func SetAvatarURL( // GetDisplayName implements GET /profile/{userID}/displayname func GetDisplayName( - req *http.Request, profileAPI userapi.UserProfileAPI, cfg *config.ClientAPI, + req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, userID string, asAPI appserviceAPI.AppServiceQueryAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { @@ -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,7 +325,7 @@ 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, federation *gomatrixserverlib.FederationClient, @@ -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 e8d14ce34..27f0ba5d0 100644 --- a/clientapi/routing/redaction.go +++ b/clientapi/routing/redaction.go @@ -40,7 +40,7 @@ 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 { 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 ba1c76b81..6da467073 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -50,15 +50,15 @@ import ( func Setup( publicAPIMux, synapseAdminRouter, dendriteAdminRouter *mux.Router, cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, - userAPI userapi.UserInternalAPI, + userAPI userapi.ClientUserAPI, userDirectoryProvider userapi.UserDirectoryProvider, 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, ) { @@ -325,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) @@ -897,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 47b0da7bb..9c34f2e1c 100644 --- a/clientapi/routing/server_notices.go +++ b/clientapi/routing/server_notices.go @@ -56,8 +56,8 @@ func SendServerNotice( req *http.Request, cfgNotices *config.ServerNotices, cfgClient *config.ClientAPI, - userAPI userapi.UserInternalAPI, - rsAPI api.RoomserverInternalAPI, + userAPI userapi.ClientUserAPI, + rsAPI api.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, device *userapi.Device, senderDevice *userapi.Device, @@ -276,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..c6e9e91d0 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 @@ -162,7 +162,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..505bf8f53 100644 --- a/clientapi/routing/upgrade_room.go +++ b/clientapi/routing/upgrade_room.go @@ -40,8 +40,8 @@ type upgradeRoomResponse struct { func UpgradeRoom( req *http.Request, device *userapi.Device, cfg *config.ClientAPI, - roomID string, profileAPI userapi.UserProfileAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + roomID string, profileAPI userapi.ClientUserAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, ) util.JSONResponse { var r upgradeRoomRequest diff --git a/clientapi/routing/userdirectory.go b/clientapi/routing/userdirectory.go index ab73cf430..4d41da734 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, + userAPI userapi.ClientUserAPI, + rsAPI api.ClientRoomserverAPI, provider userapi.UserDirectoryProvider, 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 eee6992f8..6e7426a7f 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/federationapi/api/api.go b/federationapi/api/api.go index 4d6b0211c..ce6a0f2ed 100644 --- a/federationapi/api/api.go +++ b/federationapi/api/api.go @@ -42,6 +42,7 @@ func (e *FederationClientError) Error() string { type FederationInternalAPI interface { FederationClient gomatrixserverlib.KeyDatabase + ClientFederationAPI KeyRing() *gomatrixserverlib.KeyRing @@ -100,6 +101,10 @@ type FederationInternalAPI interface { ) error } +type ClientFederationAPI interface { + QueryJoinedHostServerNamesInRoom(ctx context.Context, request *QueryJoinedHostServerNamesInRoomRequest, response *QueryJoinedHostServerNamesInRoomResponse) error +} + type QueryServerKeysRequest struct { ServerName gomatrixserverlib.ServerName KeyIDToCriteria map[gomatrixserverlib.KeyID]gomatrixserverlib.PublicKeyNotaryQueryCriteria diff --git a/federationapi/routing/invite.go b/federationapi/routing/invite.go index 58bf99f4a..25faff0cb 100644 --- a/federationapi/routing/invite.go +++ b/federationapi/routing/invite.go @@ -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/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/keyserver/api/api.go b/keyserver/api/api.go index ce651ba4e..5564eb271 100644 --- a/keyserver/api/api.go +++ b/keyserver/api/api.go @@ -28,21 +28,27 @@ import ( type KeyInternalAPI interface { SyncKeyAPI + ClientKeyAPI // 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) - 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) QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) } +// 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) + PerformUploadDeviceKeys(ctx context.Context, req *PerformUploadDeviceKeysRequest, res *PerformUploadDeviceKeysResponse) + PerformUploadDeviceSignatures(ctx context.Context, req *PerformUploadDeviceSignaturesRequest, res *PerformUploadDeviceSignaturesResponse) + // PerformClaimKeys claims one-time keys for use in pre-key messages + PerformClaimKeys(ctx context.Context, req *PerformClaimKeysRequest, res *PerformClaimKeysResponse) +} + // API functions required by the syncapi type SyncKeyAPI interface { QueryKeyChanges(ctx context.Context, req *QueryKeyChangesRequest, res *QueryKeyChangesResponse) diff --git a/mediaapi/mediaapi.go b/mediaapi/mediaapi.go index 5976957ca..4792c996d 100644 --- a/mediaapi/mediaapi.go +++ b/mediaapi/mediaapi.go @@ -26,7 +26,7 @@ import ( // AddPublicRoutes sets up and registers HTTP handlers for the MediaAPI component. func AddPublicRoutes( base *base.BaseDendrite, - userAPI userapi.UserInternalAPI, + userAPI userapi.MediaUserAPI, client *gomatrixserverlib.Client, ) { cfg := &base.Cfg.MediaAPI diff --git a/mediaapi/routing/routing.go b/mediaapi/routing/routing.go index 97dfd3341..76f07415b 100644 --- a/mediaapi/routing/routing.go +++ b/mediaapi/routing/routing.go @@ -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) diff --git a/roomserver/api/api.go b/roomserver/api/api.go index 2e4ec3ffd..33c3d157b 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -12,7 +12,13 @@ import ( // RoomserverInputAPI is used to write events to the room server. type RoomserverInternalAPI interface { + InputRoomEventsAPI + QueryLatestEventsAndStateAPI + QueryEventsAPI + SyncRoomserverAPI + AppserviceRoomserverAPI + ClientRoomserverAPI // needed to avoid chicken and egg scenario when setting up the // interdependencies between the roomserver and other input APIs @@ -20,12 +26,6 @@ type RoomserverInternalAPI interface { SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) SetUserAPI(userAPI userapi.UserInternalAPI) - InputRoomEvents( - ctx context.Context, - request *InputRoomEventsRequest, - response *InputRoomEventsResponse, - ) - PerformInvite( ctx context.Context, req *PerformInviteRequest, @@ -68,44 +68,31 @@ type RoomserverInternalAPI interface { res *PerformInboundPeekResponse, ) error - PerformAdminEvacuateRoom( - ctx context.Context, - req *PerformAdminEvacuateRoomRequest, - res *PerformAdminEvacuateRoomResponse, - ) - QueryPublishedRooms( ctx context.Context, req *QueryPublishedRoomsRequest, res *QueryPublishedRoomsResponse, ) 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, + req *QueryServerJoinedToRoomRequest, + res *QueryServerJoinedToRoomResponse, ) error // Query whether a server is allowed to see an event QueryServerAllowedToSeeEvent( ctx context.Context, - request *QueryServerAllowedToSeeEventRequest, - response *QueryServerAllowedToSeeEventResponse, + req *QueryServerAllowedToSeeEventRequest, + res *QueryServerAllowedToSeeEventResponse, ) error // Query missing events for a room from roomserver QueryMissingEvents( ctx context.Context, - request *QueryMissingEventsRequest, - response *QueryMissingEventsResponse, + req *QueryMissingEventsRequest, + res *QueryMissingEventsResponse, ) error // Query to get state and auth chain for a (potentially hypothetical) event. @@ -113,8 +100,8 @@ type RoomserverInternalAPI interface { // the state and auth chain to return. QueryStateAndAuthChain( ctx context.Context, - request *QueryStateAndAuthChainRequest, - response *QueryStateAndAuthChainResponse, + req *QueryStateAndAuthChainRequest, + res *QueryStateAndAuthChainResponse, ) error // QueryAuthChain returns the entire auth chain for the event IDs given. @@ -122,112 +109,179 @@ type RoomserverInternalAPI interface { // Omits without error for any missing auth events. There will be no duplicates. QueryAuthChain( ctx context.Context, - request *QueryAuthChainRequest, - response *QueryAuthChainResponse, + req *QueryAuthChainRequest, + res *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 - // 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 - // 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( ctx context.Context, - request *QueryRoomVersionCapabilitiesRequest, - response *QueryRoomVersionCapabilitiesResponse, + req *QueryRoomVersionCapabilitiesRequest, + res *QueryRoomVersionCapabilitiesResponse, ) error // Asks for the room version for a given room. QueryRoomVersionForRoom( ctx context.Context, - request *QueryRoomVersionForRoomRequest, - response *QueryRoomVersionForRoomResponse, + req *QueryRoomVersionForRoomRequest, + res *QueryRoomVersionForRoomResponse, ) error // Set a room alias SetRoomAlias( ctx context.Context, req *SetRoomAliasRequest, - response *SetRoomAliasResponse, + res *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, + res *GetRoomIDForAliasResponse, ) error // Get the user ID of the creator of an alias GetCreatorIDForAlias( ctx context.Context, req *GetCreatorIDForAliasRequest, - response *GetCreatorIDForAliasResponse, + res *GetCreatorIDForAliasResponse, ) error // Remove a room alias RemoveRoomAlias( ctx context.Context, req *RemoveRoomAliasRequest, - response *RemoveRoomAliasResponse, + res *RemoveRoomAliasResponse, ) error } +type InputRoomEventsAPI interface { + InputRoomEvents( + ctx context.Context, + req *InputRoomEventsRequest, + res *InputRoomEventsResponse, + ) +} + +// 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 *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 +} + // API functions required by the syncapi type SyncRoomserverAPI interface { - // Query the latest events and state for a room from the room server. - QueryLatestEventsAndState( - ctx context.Context, - request *QueryLatestEventsAndStateRequest, - response *QueryLatestEventsAndStateResponse, - ) error - // QueryBulkStateContent does a bulk query for state event content in the given rooms. - QueryBulkStateContent(ctx context.Context, req *QueryBulkStateContentRequest, res *QueryBulkStateContentResponse) error + 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, - request *QueryEventsByIDRequest, - response *QueryEventsByIDResponse, + req *QueryEventsByIDRequest, + res *QueryEventsByIDResponse, ) error // Query the membership event for an user for a room. QueryMembershipForUser( ctx context.Context, - request *QueryMembershipForUserRequest, - response *QueryMembershipForUserResponse, + 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 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 } + +type AppserviceRoomserverAPI interface { + // Query a list of events by event ID. + QueryEventsByID( + ctx context.Context, + req *QueryEventsByIDRequest, + res *QueryEventsByIDResponse, + ) error + // Query a list of membership events for a room + QueryMembershipsForRoom( + ctx context.Context, + req *QueryMembershipsForRoomRequest, + res *QueryMembershipsForRoomResponse, + ) error + // Get all known aliases for a room ID + GetAliasesForRoomID( + ctx context.Context, + req *GetAliasesForRoomIDRequest, + 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 +} diff --git a/roomserver/api/wrapper.go b/roomserver/api/wrapper.go index 5491d36b3..9f7a09ddd 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, @@ -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/userapi/api/api.go b/userapi/api/api.go index 6f00fe44f..928b91e6d 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -26,44 +26,77 @@ import ( // UserInternalAPI is the internal API for information about users and devices. type UserInternalAPI interface { - LoginTokenInternalAPI UserProfileAPI - UserRegisterAPI - UserAccountAPI - UserThreePIDAPI QueryAcccessTokenAPI + + AppserviceUserAPI SyncUserAPI - - 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) + ClientUserAPI + MediaUserAPI 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 } 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 AppserviceUserAPI interface { + PerformAccountCreation(ctx context.Context, req *PerformAccountCreationRequest, res *PerformAccountCreationResponse) error + PerformDeviceCreation(ctx context.Context, req *PerformDeviceCreationRequest, res *PerformDeviceCreationResponse) error +} + +type MediaUserAPI interface { + QueryAcccessTokenAPI +} + type SyncUserAPI interface { + QueryAcccessTokenAPI QueryAccountData(ctx context.Context, req *QueryAccountDataRequest, res *QueryAccountDataResponse) error - QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error - PerformDeviceDeletion(ctx context.Context, req *PerformDeviceDeletionRequest, res *PerformDeviceDeletionResponse) 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 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 + 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 + 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) + + 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 +} + type UserDirectoryProvider interface { QuerySearchProfiles(ctx context.Context, req *QuerySearchProfilesRequest, res *QuerySearchProfilesResponse) error } @@ -72,31 +105,6 @@ type UserDirectoryProvider interface { 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 { - QueryNumericLocalpart(ctx context.Context, res *QueryNumericLocalpartResponse) 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 { - PerformPasswordUpdate(ctx context.Context, req *PerformPasswordUpdateRequest, res *PerformPasswordUpdateResponse) error - PerformAccountDeactivation(ctx context.Context, req *PerformAccountDeactivationRequest, res *PerformAccountDeactivationResponse) error - QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error -} - -// 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 - PerformForgetThreePID(ctx context.Context, req *PerformForgetThreePIDRequest, res *struct{}) error - PerformSaveThreePIDAssociation(ctx context.Context, req *PerformSaveThreePIDAssociationRequest, res *struct{}) error } type PerformKeyBackupRequest struct { From 530fd488a91dd1644799920328a12810b70b1b49 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 5 May 2022 13:29:39 +0100 Subject: [PATCH 056/103] Don't log consumer errors on shutdown --- roomserver/internal/input/input.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/roomserver/internal/input/input.go b/roomserver/internal/input/input.go index 1fea6ef06..10c210220 100644 --- a/roomserver/internal/input/input.go +++ b/roomserver/internal/input/input.go @@ -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 From 42f35a57ac82e78e7035547504806733089f21a0 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 5 May 2022 13:42:12 +0100 Subject: [PATCH 057/103] Update table names for user API stats table --- userapi/storage/postgres/stats_table.go | 14 +++++++------- userapi/storage/sqlite3/stats_table.go | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/userapi/storage/postgres/stats_table.go b/userapi/storage/postgres/stats_table.go index f71900015..c0b317503 100644 --- a/userapi/storage/postgres/stats_table.go +++ b/userapi/storage/postgres/stats_table.go @@ -30,7 +30,7 @@ import ( ) const userDailyVisitsSchema = ` -CREATE TABLE IF NOT EXISTS user_daily_visits ( +CREATE TABLE IF NOT EXISTS userapi_daily_visits ( localpart TEXT NOT NULL, device_id TEXT NOT NULL, timestamp BIGINT NOT NULL, @@ -38,9 +38,9 @@ CREATE TABLE IF NOT EXISTS user_daily_visits ( ); -- Device IDs and timestamp must be unique for a given user per day -CREATE UNIQUE INDEX IF NOT EXISTS localpart_device_timestamp_idx ON user_daily_visits(localpart, device_id, timestamp); -CREATE INDEX IF NOT EXISTS timestamp_idx ON user_daily_visits(timestamp); -CREATE INDEX IF NOT EXISTS localpart_timestamp_idx ON user_daily_visits(localpart, timestamp); +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 = "" + @@ -112,7 +112,7 @@ FROM WHEN LOWER(user_agent) LIKE '%%mozilla%%' OR LOWER(user_agent) LIKE '%%gecko%%' THEN 'web' ELSE 'unknown' END as client_type - FROM user_daily_visits + FROM userapi_daily_visits WHERE timestamp > $1 AND timestamp < $2 GROUP BY localpart, client_type HAVING max(timestamp) - min(timestamp) > $3 @@ -141,11 +141,11 @@ SELECT user_type, COUNT(*) AS count FROM ( // account_type 1 = users; 3 = admins const updateUserDailyVisitsSQL = ` -INSERT INTO user_daily_visits(localpart, device_id, timestamp, user_agent) +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 user_daily_visits + 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 diff --git a/userapi/storage/sqlite3/stats_table.go b/userapi/storage/sqlite3/stats_table.go index af4c7ff98..e00ed417b 100644 --- a/userapi/storage/sqlite3/stats_table.go +++ b/userapi/storage/sqlite3/stats_table.go @@ -30,7 +30,7 @@ import ( ) const userDailyVisitsSchema = ` -CREATE TABLE IF NOT EXISTS user_daily_visits ( +CREATE TABLE IF NOT EXISTS userapi_daily_visits ( localpart TEXT NOT NULL, device_id TEXT NOT NULL, timestamp BIGINT NOT NULL, @@ -38,9 +38,9 @@ CREATE TABLE IF NOT EXISTS user_daily_visits ( ); -- Device IDs and timestamp must be unique for a given user per day -CREATE UNIQUE INDEX IF NOT EXISTS localpart_device_timestamp_idx ON user_daily_visits(localpart, device_id, timestamp); -CREATE INDEX IF NOT EXISTS timestamp_idx ON user_daily_visits(timestamp); -CREATE INDEX IF NOT EXISTS localpart_timestamp_idx ON user_daily_visits(localpart, timestamp); +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 = "" + @@ -116,7 +116,7 @@ FROM WHEN LOWER(user_agent) LIKE '%%mozilla%%' OR LOWER(user_agent) LIKE '%%gecko%%' THEN 'web' ELSE 'unknown' END as client_type - FROM user_daily_visits + FROM userapi_daily_visits WHERE timestamp > $1 AND timestamp < $2 GROUP BY localpart, client_type HAVING max(timestamp) - min(timestamp) > $3 @@ -145,11 +145,11 @@ SELECT user_type, COUNT(*) AS count FROM ( // account_type 1 = users; 3 = admins const updateUserDailyVisitsSQL = ` -INSERT INTO user_daily_visits(localpart, device_id, timestamp, user_agent) +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 user_daily_visits + 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 From e4da04e75b4cba1c9afb63b9973444e1da12021b Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 5 May 2022 14:06:05 +0100 Subject: [PATCH 058/103] Update to matrix-org/gomatrixserverlib#303 --- go.mod | 2 +- go.sum | 4 ++-- setup/base/base.go | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 56f5dcbbb..b6070b94f 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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-20220505092512-c4ceb4751ac2 + github.com/matrix-org/gomatrixserverlib v0.0.0-20220505130352-f72a63510060 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 diff --git a/go.sum b/go.sum index fccda40c4..cbea7319e 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220505092512-c4ceb4751ac2 h1:5/Y4BpiMk1D/l/HkJz8Ng8bLBz1BHwV6V4e+yMNySzk= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220505092512-c4ceb4751ac2/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220505130352-f72a63510060 h1:tYi4mCOWgVLt8mpkG1LFRKcMfSTwp5NQ5wBKdtaxO9s= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220505130352-f72a63510060/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= diff --git a/setup/base/base.go b/setup/base/base.go index 3641ad780..9326be1c0 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -331,6 +331,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)) From 9957752a9d60d4519cc0b7e8b9b40a781240c27d Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 5 May 2022 19:30:38 +0100 Subject: [PATCH 059/103] Define component interfaces based on consumers (2/2) (#2425) * convert remaining interfaces * Tidy up the userapi interfaces --- clientapi/clientapi.go | 2 +- clientapi/routing/routing.go | 2 +- clientapi/routing/userdirectory.go | 2 +- cmd/dendrite-demo-pinecone/users/users.go | 4 +- federationapi/api/api.go | 62 ++++++-------- federationapi/federationapi.go | 2 +- federationapi/routing/backfill.go | 2 +- federationapi/routing/eventauth.go | 2 +- federationapi/routing/events.go | 6 +- federationapi/routing/invite.go | 6 +- federationapi/routing/join.go | 4 +- federationapi/routing/leave.go | 4 +- federationapi/routing/missingevents.go | 2 +- federationapi/routing/openid.go | 2 +- federationapi/routing/peek.go | 2 +- federationapi/routing/profile.go | 2 +- federationapi/routing/publicrooms.go | 6 +- federationapi/routing/query.go | 2 +- federationapi/routing/routing.go | 6 +- federationapi/routing/send.go | 4 +- federationapi/routing/send_test.go | 2 +- federationapi/routing/state.go | 6 +- federationapi/routing/threepid.go | 12 +-- keyserver/api/api.go | 9 ++- roomserver/api/api.go | 80 ++++++++----------- roomserver/api/api_trace.go | 2 +- roomserver/api/wrapper.go | 2 +- roomserver/internal/api.go | 4 +- roomserver/internal/input/input.go | 2 +- roomserver/internal/input/input_missing.go | 2 +- .../internal/perform/perform_backfill.go | 6 +- roomserver/internal/perform/perform_invite.go | 2 +- roomserver/internal/perform/perform_join.go | 2 +- roomserver/internal/perform/perform_leave.go | 2 +- roomserver/internal/perform/perform_peek.go | 2 +- roomserver/internal/perform/perform_unpeek.go | 2 +- roomserver/inthttp/client.go | 2 +- setup/monolith.go | 2 +- userapi/api/api.go | 38 +++++---- userapi/consumers/syncapi_streamevent.go | 6 +- userapi/internal/api.go | 2 +- userapi/userapi.go | 4 +- 42 files changed, 153 insertions(+), 162 deletions(-) diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index 957d082a6..ad4609080 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -38,7 +38,7 @@ func AddPublicRoutes( transactionsCache *transactions.Cache, fsAPI federationAPI.ClientFederationAPI, userAPI userapi.ClientUserAPI, - userDirectoryProvider userapi.UserDirectoryProvider, + userDirectoryProvider userapi.QuerySearchProfilesAPI, keyAPI keyserverAPI.ClientKeyAPI, extRoomsProvider api.ExtraPublicRoomsProvider, ) { diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 6da467073..f9f71ed7a 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -53,7 +53,7 @@ func Setup( rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, userAPI userapi.ClientUserAPI, - userDirectoryProvider userapi.UserDirectoryProvider, + userDirectoryProvider userapi.QuerySearchProfilesAPI, federation *gomatrixserverlib.FederationClient, syncProducer *producers.SyncAPIProducer, transactionsCache *transactions.Cache, diff --git a/clientapi/routing/userdirectory.go b/clientapi/routing/userdirectory.go index 4d41da734..f311457a0 100644 --- a/clientapi/routing/userdirectory.go +++ b/clientapi/routing/userdirectory.go @@ -36,7 +36,7 @@ func SearchUserDirectory( device *userapi.Device, userAPI userapi.ClientUserAPI, rsAPI api.ClientRoomserverAPI, - provider userapi.UserDirectoryProvider, + provider userapi.QuerySearchProfilesAPI, serverName gomatrixserverlib.ServerName, searchString string, limit int, 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/federationapi/api/api.go b/federationapi/api/api.go index ce6a0f2ed..87b037187 100644 --- a/federationapi/api/api.go +++ b/federationapi/api/api.go @@ -14,17 +14,13 @@ import ( // implements as proxy calls, with built-in backoff/retries/etc. Errors returned from functions in // this interface are of type FederationClientError type FederationClient interface { - gomatrixserverlib.BackfillClient gomatrixserverlib.FederatedStateClient 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) - GetEvent(ctx context.Context, s gomatrixserverlib.ServerName, eventID 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) - LookupMissingEvents(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, missing gomatrixserverlib.MissingEvents, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMissingEvents, err error) } // FederationClientError is returned from FederationClient methods in the event of a problem. @@ -43,17 +39,10 @@ type FederationInternalAPI interface { FederationClient gomatrixserverlib.KeyDatabase ClientFederationAPI - - KeyRing() *gomatrixserverlib.KeyRing + RoomserverFederationAPI 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). @@ -63,30 +52,6 @@ type FederationInternalAPI interface { 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, @@ -105,6 +70,31 @@ type ClientFederationAPI interface { 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) +} + type QueryServerKeysRequest struct { ServerName gomatrixserverlib.ServerName KeyIDToCriteria map[gomatrixserverlib.KeyID]gomatrixserverlib.PublicKeyNotaryQueryCriteria diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index c627aab5c..632994db9 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -49,7 +49,7 @@ func AddPublicRoutes( userAPI userapi.UserInternalAPI, federation *gomatrixserverlib.FederationClient, keyRing gomatrixserverlib.JSONVerifier, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.FederationRoomserverAPI, federationAPI federationAPI.FederationInternalAPI, keyAPI keyserverAPI.KeyInternalAPI, servers federationAPI.ServersInRoomProvider, diff --git a/federationapi/routing/backfill.go b/federationapi/routing/backfill.go index 82f6cbabf..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 { diff --git a/federationapi/routing/eventauth.go b/federationapi/routing/eventauth.go index e83cb8ad2..868785a9b 100644 --- a/federationapi/routing/eventauth.go +++ b/federationapi/routing/eventauth.go @@ -26,7 +26,7 @@ import ( func GetEventAuth( ctx context.Context, request *gomatrixserverlib.FederationRequest, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, eventID string, ) util.JSONResponse { 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 25faff0cb..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 { 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/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 b826d69c4..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 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..707b7b019 100644 --- a/federationapi/routing/query.go +++ b/federationapi/routing/query.go @@ -32,7 +32,7 @@ func RoomAliasToID( httpReq *http.Request, federation *gomatrixserverlib.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 6d24c8b40..51adc279c 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -47,11 +47,11 @@ import ( func Setup( fedMux, keyMux, wkMux *mux.Router, cfg *config.FederationAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, + rsAPI roomserverAPI.FederationRoomserverAPI, fsAPI federationAPI.FederationInternalAPI, keys gomatrixserverlib.JSONVerifier, federation *gomatrixserverlib.FederationClient, - userAPI userapi.UserInternalAPI, + userAPI userapi.FederationUserAPI, keyAPI keyserverAPI.KeyInternalAPI, mscCfg *config.MSCs, servers federationAPI.ServersInRoomProvider, @@ -497,7 +497,7 @@ func Setup( func ErrorIfLocalServerNotInRoom( ctx context.Context, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, roomID string, ) *util.JSONResponse { // Check if we think we're in this room. If we aren't then diff --git a/federationapi/routing/send.go b/federationapi/routing/send.go index 2c01afb1b..b9b6d33b7 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -82,7 +82,7 @@ func Send( request *gomatrixserverlib.FederationRequest, txnID gomatrixserverlib.TransactionID, cfg *config.FederationAPI, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, keyAPI keyapi.KeyInternalAPI, keys gomatrixserverlib.JSONVerifier, federation *gomatrixserverlib.FederationClient, @@ -182,7 +182,7 @@ func Send( type txnReq struct { gomatrixserverlib.Transaction - rsAPI api.RoomserverInternalAPI + rsAPI api.FederationRoomserverAPI keyAPI keyapi.KeyInternalAPI ourServerName gomatrixserverlib.ServerName keys gomatrixserverlib.JSONVerifier diff --git a/federationapi/routing/send_test.go b/federationapi/routing/send_test.go index 8d2d85040..011d4e342 100644 --- a/federationapi/routing/send_test.go +++ b/federationapi/routing/send_test.go @@ -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 e2b67776a..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,7 +97,7 @@ 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) { diff --git a/federationapi/routing/threepid.go b/federationapi/routing/threepid.go index 8ae7130c3..16f245cee 100644 --- a/federationapi/routing/threepid.go +++ b/federationapi/routing/threepid.go @@ -55,10 +55,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, + userAPI userapi.FederationUserAPI, ) util.JSONResponse { var body invites if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil { @@ -105,7 +105,7 @@ func ExchangeThirdPartyInvite( httpReq *http.Request, request *gomatrixserverlib.FederationRequest, roomID string, - rsAPI api.RoomserverInternalAPI, + rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { @@ -203,10 +203,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, + userAPI userapi.FederationUserAPI, ) (*gomatrixserverlib.Event, error) { verReq := api.QueryRoomVersionForRoomRequest{RoomID: inv.RoomID} verRes := api.QueryRoomVersionForRoomResponse{} @@ -270,7 +270,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) diff --git a/keyserver/api/api.go b/keyserver/api/api.go index 5564eb271..6cee2c014 100644 --- a/keyserver/api/api.go +++ b/keyserver/api/api.go @@ -29,12 +29,13 @@ import ( type KeyInternalAPI interface { SyncKeyAPI ClientKeyAPI + 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) - PerformDeleteKeys(ctx context.Context, req *PerformDeleteKeysRequest, res *PerformDeleteKeysResponse) QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) } @@ -49,6 +50,12 @@ type ClientKeyAPI interface { 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) diff --git a/roomserver/api/api.go b/roomserver/api/api.go index 33c3d157b..7e1e568ce 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -19,10 +19,12 @@ 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) + SetFederationAPI(fsAPI fsAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) SetUserAPI(userAPI userapi.UserInternalAPI) @@ -62,48 +64,6 @@ type RoomserverInternalAPI interface { res *PerformPublishResponse, ) - PerformInboundPeek( - ctx context.Context, - req *PerformInboundPeekRequest, - res *PerformInboundPeekResponse, - ) error - - QueryPublishedRooms( - ctx context.Context, - req *QueryPublishedRoomsRequest, - res *QueryPublishedRoomsResponse, - ) error - - // Query if we think we're still in a room. - QueryServerJoinedToRoom( - ctx context.Context, - req *QueryServerJoinedToRoomRequest, - res *QueryServerJoinedToRoomResponse, - ) error - - // Query whether a server is allowed to see an event - QueryServerAllowedToSeeEvent( - ctx context.Context, - req *QueryServerAllowedToSeeEventRequest, - res *QueryServerAllowedToSeeEventResponse, - ) error - - // Query missing events for a room from roomserver - QueryMissingEvents( - ctx context.Context, - req *QueryMissingEventsRequest, - res *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, - req *QueryStateAndAuthChainRequest, - res *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. @@ -115,8 +75,6 @@ type RoomserverInternalAPI interface { // QueryRoomsForUser retrieves a list of room IDs matching the given query. QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) error - // QueryServerBannedFromRoom returns whether a server is banned from a room by server ACLs. - QueryServerBannedFromRoom(ctx context.Context, req *QueryServerBannedFromRoomRequest, res *QueryServerBannedFromRoomResponse) error // PerformRoomUpgrade upgrades a room to a newer version PerformRoomUpgrade(ctx context.Context, req *PerformRoomUpgradeRequest, resp *PerformRoomUpgradeResponse) @@ -285,3 +243,35 @@ type ClientRoomserverAPI interface { 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 + 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 61c06e886..bc60999e6 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -19,7 +19,7 @@ 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) } diff --git a/roomserver/api/wrapper.go b/roomserver/api/wrapper.go index 9f7a09ddd..344e9b079 100644 --- a/roomserver/api/wrapper.go +++ b/roomserver/api/wrapper.go @@ -129,7 +129,7 @@ func GetStateEvent(ctx context.Context, rsAPI QueryEventsAPI, roomID string, tup } // 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, diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index 267cd4099..dc0a0a718 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -43,7 +43,7 @@ type RoomserverInternalAPI struct { ServerName gomatrixserverlib.ServerName KeyRing gomatrixserverlib.JSONVerifier ServerACLs *acls.ServerACLs - fsAPI fsAPI.FederationInternalAPI + fsAPI fsAPI.RoomserverFederationAPI asAPI asAPI.AppServiceQueryAPI NATSClient *nats.Conn JetStream nats.JetStreamContext @@ -87,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 diff --git a/roomserver/internal/input/input.go b/roomserver/internal/input/input.go index 10c210220..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 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/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..b006843fb 100644 --- a/roomserver/internal/perform/perform_leave.go +++ b/roomserver/internal/perform/perform_leave.go @@ -37,7 +37,7 @@ import ( type Leaver struct { Cfg *config.RoomServer DB storage.Database - FSAPI fsAPI.FederationInternalAPI + FSAPI fsAPI.RoomserverFederationAPI UserAPI userapi.UserInternalAPI 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/inthttp/client.go b/roomserver/inthttp/client.go index 3b29001e9..4fc75ff41 100644 --- a/roomserver/inthttp/client.go +++ b/roomserver/inthttp/client.go @@ -87,7 +87,7 @@ 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 diff --git a/setup/monolith.go b/setup/monolith.go index e033c14d7..a0e850d83 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -47,7 +47,7 @@ type Monolith struct { // Optional ExtPublicRoomsProvider api.ExtraPublicRoomsProvider - ExtUserDirectoryProvider userapi.UserDirectoryProvider + ExtUserDirectoryProvider userapi.QuerySearchProfilesAPI } // AddAllPublicRoutes attaches all public paths to the given router diff --git a/userapi/api/api.go b/userapi/api/api.go index 928b91e6d..dc8c12b74 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -26,34 +26,33 @@ import ( // UserInternalAPI is the internal API for information about users and devices. type UserInternalAPI interface { - UserProfileAPI - QueryAcccessTokenAPI - AppserviceUserAPI SyncUserAPI ClientUserAPI MediaUserAPI + FederationUserAPI - QueryOpenIDToken(ctx context.Context, req *QueryOpenIDTokenRequest, res *QueryOpenIDTokenResponse) error -} - -type QueryAcccessTokenAPI interface { - QueryAccessToken(ctx context.Context, req *QueryAccessTokenRequest, res *QueryAccessTokenResponse) error -} - -type UserLoginAPI interface { - QueryAccountByPassword(ctx context.Context, req *QueryAccountByPasswordRequest, res *QueryAccountByPasswordResponse) error + QuerySearchProfilesAPI // used by p2p demos } +// 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 } +// 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 @@ -63,6 +62,7 @@ type SyncUserAPI interface { QueryDeviceInfos(ctx context.Context, req *QueryDeviceInfosRequest, res *QueryDeviceInfosResponse) error } +// api functions required by the client api type ClientUserAPI interface { QueryAcccessTokenAPI LoginTokenInternalAPI @@ -97,14 +97,18 @@ type ClientUserAPI interface { PerformSaveThreePIDAssociation(ctx context.Context, req *PerformSaveThreePIDAssociationRequest, res *struct{}) error } -type UserDirectoryProvider interface { +// custom api functions required by pinecone / p2p demos +type QuerySearchProfilesAPI 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 +// 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 { 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 394bfa224..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 { diff --git a/userapi/userapi.go b/userapi/userapi.go index 5b11665db..03a46807f 100644 --- a/userapi/userapi.go +++ b/userapi/userapi.go @@ -44,8 +44,8 @@ func AddInternalRoutes(router *mux.Router, intAPI api.UserInternalAPI) { // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( base *base.BaseDendrite, cfg *config.UserAPI, - appServices []config.ApplicationService, keyAPI keyapi.KeyInternalAPI, - rsAPI rsapi.RoomserverInternalAPI, pgClient pushgateway.Client, + appServices []config.ApplicationService, keyAPI keyapi.UserKeyAPI, + rsAPI rsapi.UserRoomserverAPI, pgClient pushgateway.Client, ) api.UserInternalAPI { js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) From 4705f5761e620e7f8a35151eeb2007e884847152 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Fri, 6 May 2022 08:26:24 +0200 Subject: [PATCH 060/103] Add FAQ entry for anonymous stats (#2419) --- docs/FAQ.md | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/docs/FAQ.md b/docs/FAQ.md index 978212cce..47eaecf0f 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -74,3 +74,46 @@ If you are running with `GODEBUG=madvdontneed=1` and still see hugely inflated m ### 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" +} +``` \ No newline at end of file From 85704eff207f7690d197172abb991ae1ac238239 Mon Sep 17 00:00:00 2001 From: kegsay Date: Fri, 6 May 2022 12:39:26 +0100 Subject: [PATCH 061/103] Clean up interface definitions (#2427) * tidy up interfaces * remove unused GetCreatorIDForAlias * Add RoomserverUserAPI interface * Define more interfaces * Use AppServiceInternalAPI for consistent naming * clean up federationapi constructor a bit * Fix monolith in -http mode --- appservice/api/query.go | 36 +++--- appservice/appservice.go | 4 +- appservice/inthttp/client.go | 2 +- appservice/inthttp/server.go | 2 +- clientapi/clientapi.go | 2 +- clientapi/routing/createroom.go | 4 +- clientapi/routing/membership.go | 16 +-- clientapi/routing/profile.go | 8 +- clientapi/routing/routing.go | 2 +- clientapi/routing/server_notices.go | 2 +- clientapi/routing/upgrade_room.go | 2 +- cmd/dendrite-monolith-server/main.go | 6 +- federationapi/api/api.go | 76 +++++------- federationapi/federationapi.go | 17 ++- federationapi/federationapi_test.go | 4 +- federationapi/internal/perform.go | 30 ++--- federationapi/inthttp/client.go | 13 -- federationapi/inthttp/server.go | 14 --- federationapi/routing/devices.go | 2 +- federationapi/routing/keys.go | 4 +- federationapi/routing/routing.go | 118 +++++++++++++++---- federationapi/routing/send.go | 4 +- internal/httputil/httpapi.go | 79 ------------- keyserver/api/api.go | 18 ++- keyserver/internal/internal.go | 4 +- keyserver/inthttp/client.go | 2 +- roomserver/api/alias.go | 12 -- roomserver/api/api.go | 93 +-------------- roomserver/api/api_trace.go | 14 +-- roomserver/internal/alias.go | 19 --- roomserver/internal/api.go | 6 +- roomserver/internal/perform/perform_leave.go | 2 +- roomserver/inthttp/client.go | 17 +-- roomserver/inthttp/server.go | 14 --- setup/base/base.go | 4 +- setup/monolith.go | 2 +- userapi/api/api.go | 11 ++ 37 files changed, 236 insertions(+), 429 deletions(-) diff --git a/appservice/api/query.go b/appservice/api/query.go index 6db8be85b..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,30 +77,13 @@ 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, + asAPI AppServiceInternalAPI, profileAPI userapi.ClientUserAPI, ) (*authtypes.Profile, error) { localpart, _, err := gomatrixserverlib.SplitID('@', userID) diff --git a/appservice/appservice.go b/appservice/appservice.go index e026a7875..bd292767b 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -38,7 +38,7 @@ import ( ) // 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) } @@ -48,7 +48,7 @@ func NewInternalAPI( base *base.BaseDendrite, userAPI userapi.AppserviceUserAPI, rsAPI roomserverAPI.AppserviceRoomserverAPI, -) appserviceAPI.AppServiceQueryAPI { +) appserviceAPI.AppServiceInternalAPI { client := gomatrixserverlib.NewClient( gomatrixserverlib.WithTimeout(time.Second*30), gomatrixserverlib.WithKeepAlives(false), 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/clientapi/clientapi.go b/clientapi/clientapi.go index ad4609080..c1e86114b 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -34,7 +34,7 @@ func AddPublicRoutes( base *base.BaseDendrite, federation *gomatrixserverlib.FederationClient, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, transactionsCache *transactions.Cache, fsAPI federationAPI.ClientFederationAPI, userAPI userapi.ClientUserAPI, diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index a21abb0eb..d40d84a79 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -138,7 +138,7 @@ func CreateRoom( req *http.Request, device *api.Device, cfg *config.ClientAPI, profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { var r createRoomRequest resErr := httputil.UnmarshalJSONRequest(req, &r) @@ -165,7 +165,7 @@ func createRoom( r createRoomRequest, device *api.Device, cfg *config.ClientAPI, profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time, ) util.JSONResponse { // TODO (#267): Check room ID doesn't clash with an existing one, and we diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go index 7d91c7b03..cfdf6f2de 100644 --- a/clientapi/routing/membership.go +++ b/clientapi/routing/membership.go @@ -41,7 +41,7 @@ var errMissingUserID = errors.New("'user_id' must be supplied") func SendBan( req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -84,7 +84,7 @@ func SendBan( 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.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI) util.JSONResponse { + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse { event, err := buildMembershipEvent( ctx, targetUserID, reason, profileAPI, device, membership, @@ -127,7 +127,7 @@ func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, devic func SendKick( req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -167,7 +167,7 @@ func SendKick( func SendUnban( req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, roomVer, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -202,7 +202,7 @@ func SendUnban( func SendInvite( req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device, roomID string, cfg *config.ClientAPI, - rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { body, evTime, _, reqErr := extractRequestData(req, roomID, rsAPI) if reqErr != nil { @@ -239,7 +239,7 @@ func sendInvite( roomID, userID, reason string, cfg *config.ClientAPI, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, evTime time.Time, + asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time, ) (util.JSONResponse, error) { event, err := buildMembershipEvent( ctx, userID, reason, profileAPI, device, "invite", @@ -289,7 +289,7 @@ func buildMembershipEvent( device *userapi.Device, membership, roomID string, isDirect bool, cfg *config.ClientAPI, evTime time.Time, - rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceQueryAPI, + rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI, ) (*gomatrixserverlib.HeaderedEvent, error) { profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI) if err != nil { @@ -327,7 +327,7 @@ func loadProfile( userID string, cfg *config.ClientAPI, profileAPI userapi.ClientUserAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) (*authtypes.Profile, error) { _, serverName, err := gomatrixserverlib.SplitID('@', userID) if err != nil { diff --git a/clientapi/routing/profile.go b/clientapi/routing/profile.go index 97f86afe2..0685c7352 100644 --- a/clientapi/routing/profile.go +++ b/clientapi/routing/profile.go @@ -37,7 +37,7 @@ import ( func GetProfile( 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) @@ -65,7 +65,7 @@ func GetProfile( // GetAvatarURL implements GET /profile/{userID}/avatar_url func GetAvatarURL( req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, - userID string, asAPI appserviceAPI.AppServiceQueryAPI, + userID string, asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation) @@ -194,7 +194,7 @@ func SetAvatarURL( // GetDisplayName implements GET /profile/{userID}/displayname func GetDisplayName( req *http.Request, profileAPI userapi.ClientUserAPI, cfg *config.ClientAPI, - userID string, asAPI appserviceAPI.AppServiceQueryAPI, + userID string, asAPI appserviceAPI.AppServiceInternalAPI, federation *gomatrixserverlib.FederationClient, ) util.JSONResponse { profile, err := getProfile(req.Context(), profileAPI, cfg, userID, asAPI, federation) @@ -327,7 +327,7 @@ func SetDisplayName( func getProfile( 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) diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index f9f71ed7a..94becf465 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -51,7 +51,7 @@ func Setup( publicAPIMux, synapseAdminRouter, dendriteAdminRouter *mux.Router, cfg *config.ClientAPI, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, userAPI userapi.ClientUserAPI, userDirectoryProvider userapi.QuerySearchProfilesAPI, federation *gomatrixserverlib.FederationClient, diff --git a/clientapi/routing/server_notices.go b/clientapi/routing/server_notices.go index 9c34f2e1c..9edeed2f7 100644 --- a/clientapi/routing/server_notices.go +++ b/clientapi/routing/server_notices.go @@ -58,7 +58,7 @@ func SendServerNotice( cfgClient *config.ClientAPI, userAPI userapi.ClientUserAPI, rsAPI api.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, device *userapi.Device, senderDevice *userapi.Device, txnID *string, diff --git a/clientapi/routing/upgrade_room.go b/clientapi/routing/upgrade_room.go index 505bf8f53..744e2d889 100644 --- a/clientapi/routing/upgrade_room.go +++ b/clientapi/routing/upgrade_room.go @@ -42,7 +42,7 @@ func UpgradeRoom( cfg *config.ClientAPI, roomID string, profileAPI userapi.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, - asAPI appserviceAPI.AppServiceQueryAPI, + asAPI appserviceAPI.AppServiceInternalAPI, ) util.JSONResponse { var r upgradeRoomRequest if rErr := httputil.UnmarshalJSONRequest(req, &r); rErr != nil { diff --git a/cmd/dendrite-monolith-server/main.go b/cmd/dendrite-monolith-server/main.go index 2fa4675a4..845b9e465 100644 --- a/cmd/dendrite-monolith-server/main.go +++ b/cmd/dendrite-monolith-server/main.go @@ -89,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() @@ -138,7 +139,10 @@ func main() { 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, diff --git a/federationapi/api/api.go b/federationapi/api/api.go index 87b037187..fc25194e0 100644 --- a/federationapi/api/api.go +++ b/federationapi/api/api.go @@ -10,30 +10,6 @@ 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 { - gomatrixserverlib.FederatedStateClient - 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) - 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) -} - -// FederationClientError is returned from FederationClient methods in the event of a problem. -type FederationClientError struct { - Err string - RetryAfter time.Duration - Blacklisted bool -} - -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 @@ -43,22 +19,7 @@ type FederationInternalAPI interface { QueryServerKeys(ctx context.Context, request *QueryServerKeysRequest, response *QueryServerKeysResponse) 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 - // 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. + // Broadcasts an EDU to all servers in rooms we are joined to. Used in the yggdrasil demos. PerformBroadcastEDU( ctx context.Context, request *PerformBroadcastEDURequest, @@ -67,6 +28,10 @@ type FederationInternalAPI interface { } 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 } @@ -95,6 +60,30 @@ type RoomserverFederationAPI interface { LookupMissingEvents(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, missing gomatrixserverlib.MissingEvents, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMissingEvents, err error) } +// 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 { + gomatrixserverlib.FederatedStateClient + 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) + 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) +} + +// FederationClientError is returned from FederationClient methods in the event of a problem. +type FederationClientError struct { + Err string + RetryAfter time.Duration + Blacklisted bool +} + +func (e *FederationClientError) Error() string { + return fmt.Sprintf("%s - (retry_after=%s, blacklisted=%v)", e.Err, e.RetryAfter.String(), e.Blacklisted) +} + type QueryServerKeysRequest struct { ServerName gomatrixserverlib.ServerName KeyIDToCriteria map[gomatrixserverlib.KeyID]gomatrixserverlib.PublicKeyNotaryQueryCriteria @@ -174,13 +163,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/federationapi.go b/federationapi/federationapi.go index 632994db9..e52377c94 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -50,8 +50,8 @@ func AddPublicRoutes( federation *gomatrixserverlib.FederationClient, keyRing gomatrixserverlib.JSONVerifier, rsAPI roomserverAPI.FederationRoomserverAPI, - federationAPI federationAPI.FederationInternalAPI, - keyAPI keyserverAPI.KeyInternalAPI, + fedAPI federationAPI.FederationInternalAPI, + keyAPI keyserverAPI.FederationKeyAPI, servers federationAPI.ServersInRoomProvider, ) { cfg := &base.Cfg.FederationAPI @@ -67,12 +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( base.PublicFederationAPIMux, base.PublicKeyAPIMux, base.PublicWellKnownAPIMux, cfg, - rsAPI, federationAPI, keyRing, + rsAPI, f, keyRing, federation, userAPI, keyAPI, mscCfg, servers, producer, ) diff --git a/federationapi/federationapi_test.go b/federationapi/federationapi_test.go index 687241646..eedebc6cd 100644 --- a/federationapi/federationapi_test.go +++ b/federationapi/federationapi_test.go @@ -7,6 +7,7 @@ import ( "testing" "github.com/matrix-org/dendrite/federationapi" + "github.com/matrix-org/dendrite/federationapi/internal" "github.com/matrix-org/dendrite/internal/test" "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" @@ -27,10 +28,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, nil, nil, keyRing, nil, fsAPI, nil, 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://")) diff --git a/federationapi/internal/perform.go b/federationapi/internal/perform.go index aac36cc76..577cb70e0 100644 --- a/federationapi/internal/perform.go +++ b/federationapi/internal/perform.go @@ -563,20 +563,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 +586,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 { 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/routing/devices.go b/federationapi/routing/devices.go index 57286fa90..1a092645f 100644 --- a/federationapi/routing/devices.go +++ b/federationapi/routing/devices.go @@ -26,7 +26,7 @@ import ( // 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 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/routing.go b/federationapi/routing/routing.go index 51adc279c..9f95ed07e 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -18,10 +18,14 @@ 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" @@ -48,11 +52,11 @@ func Setup( fedMux, keyMux, wkMux *mux.Router, cfg *config.FederationAPI, rsAPI roomserverAPI.FederationRoomserverAPI, - fsAPI federationAPI.FederationInternalAPI, + fsAPI *fedInternal.FederationInternalAPI, keys gomatrixserverlib.JSONVerifier, federation *gomatrixserverlib.FederationClient, userAPI userapi.FederationUserAPI, - keyAPI keyserverAPI.KeyInternalAPI, + keyAPI keyserverAPI.FederationKeyAPI, mscCfg *config.MSCs, servers federationAPI.ServersInRoomProvider, producer *producers.SyncAPIProducer, @@ -65,7 +69,7 @@ func Setup( v1fedmux := fedMux.PathPrefix("/v1").Subrouter() v2fedmux := fedMux.PathPrefix("/v2").Subrouter() - wakeup := &httputil.FederationWakeups{ + wakeup := &FederationWakeups{ FsAPI: fsAPI, } @@ -119,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( @@ -129,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()) { @@ -145,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()) { @@ -167,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( @@ -176,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( @@ -185,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()) { @@ -200,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()) { @@ -215,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()) { @@ -230,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( @@ -239,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( @@ -248,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( @@ -258,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()) { @@ -289,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()) { @@ -320,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()) { @@ -352,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()) { @@ -369,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()) { @@ -386,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()) { @@ -418,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()) { @@ -442,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()) { @@ -455,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()) { @@ -474,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) @@ -518,3 +522,67 @@ func ErrorIfLocalServerNotInRoom( } 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 b9b6d33b7..55a113675 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -83,7 +83,7 @@ func Send( txnID gomatrixserverlib.TransactionID, cfg *config.FederationAPI, rsAPI api.FederationRoomserverAPI, - keyAPI keyapi.KeyInternalAPI, + keyAPI keyapi.FederationKeyAPI, keys gomatrixserverlib.JSONVerifier, federation *gomatrixserverlib.FederationClient, mu *internal.MutexByRoom, @@ -183,7 +183,7 @@ func Send( type txnReq struct { gomatrixserverlib.Transaction rsAPI api.FederationRoomserverAPI - keyAPI keyapi.KeyInternalAPI + keyAPI keyapi.FederationKeyAPI ourServerName gomatrixserverlib.ServerName keys gomatrixserverlib.JSONVerifier federation txnFederationClient diff --git a/internal/httputil/httpapi.go b/internal/httputil/httpapi.go index 3a818cc5e..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" @@ -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/keyserver/api/api.go b/keyserver/api/api.go index 6cee2c014..140f03569 100644 --- a/keyserver/api/api.go +++ b/keyserver/api/api.go @@ -29,15 +29,11 @@ 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) - - QueryDeviceMessages(ctx context.Context, req *QueryDeviceMessagesRequest, res *QueryDeviceMessagesResponse) - QuerySignatures(ctx context.Context, req *QuerySignaturesRequest, res *QuerySignaturesResponse) + SetUserAPI(i userapi.KeyserverUserAPI) } // API functions required by the clientapi @@ -62,6 +58,16 @@ type SyncKeyAPI interface { QueryOneTimeKeys(ctx context.Context, req *QueryOneTimeKeysRequest, res *QueryOneTimeKeysResponse) } +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 type KeyError struct { Err string `json:"error"` diff --git a/keyserver/internal/internal.go b/keyserver/internal/internal.go index e556f44b0..be71e5750 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -38,12 +38,12 @@ type KeyInternalAPI struct { DB storage.Database ThisServer gomatrixserverlib.ServerName FedClient fedsenderapi.FederationClient - UserAPI userapi.UserInternalAPI + 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 } 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/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 7e1e568ce..cbb4cebca 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -12,10 +12,6 @@ import ( // RoomserverInputAPI is used to write events to the room server. type RoomserverInternalAPI interface { - InputRoomEventsAPI - QueryLatestEventsAndStateAPI - QueryEventsAPI - SyncRoomserverAPI AppserviceRoomserverAPI ClientRoomserverAPI @@ -25,101 +21,18 @@ type RoomserverInternalAPI interface { // needed to avoid chicken and egg scenario when setting up the // interdependencies between the roomserver and other input APIs SetFederationAPI(fsAPI fsAPI.RoomserverFederationAPI, keyRing *gomatrixserverlib.KeyRing) - SetAppserviceAPI(asAPI asAPI.AppServiceQueryAPI) - SetUserAPI(userAPI userapi.UserInternalAPI) - - 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 - - PerformPeek( - ctx context.Context, - req *PerformPeekRequest, - res *PerformPeekResponse, - ) - - PerformUnpeek( - ctx context.Context, - req *PerformUnpeekRequest, - res *PerformUnpeekResponse, - ) - - PerformPublish( - ctx context.Context, - req *PerformPublishRequest, - res *PerformPublishResponse, - ) + 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 - - // QueryRoomsForUser retrieves a list of room IDs matching the given query. - QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) 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( - ctx context.Context, - req *QueryRoomVersionCapabilitiesRequest, - res *QueryRoomVersionCapabilitiesResponse, - ) error - - // Asks for the room version for a given room. - QueryRoomVersionForRoom( - ctx context.Context, - req *QueryRoomVersionForRoomRequest, - res *QueryRoomVersionForRoomResponse, - ) error - - // Set a room alias - SetRoomAlias( - ctx context.Context, - req *SetRoomAliasRequest, - res *SetRoomAliasResponse, - ) error - - // Get the room ID for an alias - GetRoomIDForAlias( - ctx context.Context, - req *GetRoomIDForAliasRequest, - res *GetRoomIDForAliasResponse, - ) error - - // Get the user ID of the creator of an alias - GetCreatorIDForAlias( - ctx context.Context, - req *GetCreatorIDForAliasRequest, - res *GetCreatorIDForAliasResponse, - ) error - - // Remove a room alias - RemoveRoomAlias( - ctx context.Context, - req *RemoveRoomAliasRequest, - res *RemoveRoomAliasResponse, - ) error } type InputRoomEventsAPI interface { diff --git a/roomserver/api/api_trace.go b/roomserver/api/api_trace.go index bc60999e6..711324644 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -23,11 +23,11 @@ func (t *RoomserverInternalAPITrace) SetFederationAPI(fsAPI fsAPI.RoomserverFede 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) } @@ -293,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/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 dc0a0a718..afef52da4 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -44,7 +44,7 @@ type RoomserverInternalAPI struct { KeyRing gomatrixserverlib.JSONVerifier ServerACLs *acls.ServerACLs fsAPI fsAPI.RoomserverFederationAPI - asAPI asAPI.AppServiceQueryAPI + asAPI asAPI.AppServiceInternalAPI NATSClient *nats.Conn JetStream nats.JetStreamContext Durable string @@ -177,11 +177,11 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.RoomserverFederatio } } -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/perform/perform_leave.go b/roomserver/internal/perform/perform_leave.go index b006843fb..c5b62ac00 100644 --- a/roomserver/internal/perform/perform_leave.go +++ b/roomserver/internal/perform/perform_leave.go @@ -38,7 +38,7 @@ type Leaver struct { Cfg *config.RoomServer DB storage.Database FSAPI fsAPI.RoomserverFederationAPI - UserAPI userapi.UserInternalAPI + UserAPI userapi.RoomserverUserAPI Inputer *input.Inputer } diff --git a/roomserver/inthttp/client.go b/roomserver/inthttp/client.go index 4fc75ff41..09358001b 100644 --- a/roomserver/inthttp/client.go +++ b/roomserver/inthttp/client.go @@ -91,11 +91,11 @@ func (h *httpRoomserverInternalAPI) SetFederationAPI(fsAPI fsInputAPI.Roomserver } // 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 @@ -137,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, diff --git a/roomserver/inthttp/server.go b/roomserver/inthttp/server.go index c5159a63c..9042e341b 100644 --- a/roomserver/inthttp/server.go +++ b/roomserver/inthttp/server.go @@ -353,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/setup/base/base.go b/setup/base/base.go index 9326be1c0..d7d5119fc 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -270,8 +270,8 @@ func (b *BaseDendrite) DatabaseConnection(dbProperties *config.DatabaseOptions, return nil, nil, fmt.Errorf("no database connections configured") } -// AppserviceHTTPClient returns the AppServiceQueryAPI for hitting the appservice component over HTTP. -func (b *BaseDendrite) AppserviceHTTPClient() appserviceAPI.AppServiceQueryAPI { +// 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") diff --git a/setup/monolith.go b/setup/monolith.go index a0e850d83..41a897024 100644 --- a/setup/monolith.go +++ b/setup/monolith.go @@ -39,7 +39,7 @@ type Monolith struct { Client *gomatrixserverlib.Client FedClient *gomatrixserverlib.FederationClient - AppserviceAPI appserviceAPI.AppServiceQueryAPI + AppserviceAPI appserviceAPI.AppServiceInternalAPI FederationAPI federationAPI.FederationInternalAPI RoomserverAPI roomserverAPI.RoomserverInternalAPI UserAPI userapi.UserInternalAPI diff --git a/userapi/api/api.go b/userapi/api/api.go index dc8c12b74..df9408acb 100644 --- a/userapi/api/api.go +++ b/userapi/api/api.go @@ -31,6 +31,8 @@ type UserInternalAPI interface { ClientUserAPI MediaUserAPI FederationUserAPI + RoomserverUserAPI + KeyserverUserAPI QuerySearchProfilesAPI // used by p2p demos } @@ -41,6 +43,15 @@ type AppserviceUserAPI interface { 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 From a1a5357f799887fc5b7e3bf5c81bbf3198704645 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 6 May 2022 12:46:01 +0100 Subject: [PATCH 062/103] Produce more useful event auth errors (update to matrix-org/gomatrixserverlib#305) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b6070b94f..38d28e11e 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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-20220505130352-f72a63510060 + github.com/matrix-org/gomatrixserverlib v0.0.0-20220506114534-f3951a683bad 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 diff --git a/go.sum b/go.sum index cbea7319e..96ac72637 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220505130352-f72a63510060 h1:tYi4mCOWgVLt8mpkG1LFRKcMfSTwp5NQ5wBKdtaxO9s= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220505130352-f72a63510060/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220506114534-f3951a683bad h1:QhPE4f2fijOSW3QvT/Ucwqz9KJwafKaiKwfhwgIpx3M= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220506114534-f3951a683bad/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= From 507f63d0fc8158f200f3e29fd36e5f09c83e62db Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 6 May 2022 13:51:48 +0100 Subject: [PATCH 063/103] Add `PolylithMode` base config option (#2428) * Add `PolylithMode` base config option * Polylith mode always uses HTTP APIs --- cmd/dendrite-polylith-multi/main.go | 4 ++-- setup/base/base.go | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/dendrite-polylith-multi/main.go b/cmd/dendrite-polylith-multi/main.go index 4fccaa922..e4845f649 100644 --- a/cmd/dendrite-polylith-multi/main.go +++ b/cmd/dendrite-polylith-multi/main.go @@ -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/setup/base/base.go b/setup/base/base.go index d7d5119fc..ef449cc35 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -96,6 +96,7 @@ type BaseDendriteOptions int const ( NoCacheMetrics BaseDendriteOptions = iota UseHTTPAPIs + PolylithMode ) // NewBaseDendrite creates a new instance to be used by a component. @@ -105,17 +106,20 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base platformSanityChecks() useHTTPAPIs := false cacheMetrics := true + isMonolith := true for _, opt := range options { switch opt { case NoCacheMetrics: cacheMetrics = false case UseHTTPAPIs: useHTTPAPIs = true + case PolylithMode: + isMonolith = false + useHTTPAPIs = true } } configErrors := &config.ConfigErrors{} - isMonolith := componentName == "Monolith" // TODO: better way? cfg.Verify(configErrors, isMonolith) if len(*configErrors) > 0 { for _, err := range *configErrors { From 6493c0c0f23bb930cc8a2ce071a66b7d4b5ec9eb Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Fri, 6 May 2022 15:33:34 +0200 Subject: [PATCH 064/103] Move LL cache (#2429) --- internal/caching/cache_lazy_load_members.go | 35 +++++---------------- internal/caching/caches.go | 5 ++- internal/caching/impl_inmemorylru.go | 15 ++++++++- syncapi/routing/context.go | 4 +-- syncapi/routing/messages.go | 2 +- syncapi/routing/routing.go | 2 +- syncapi/streams/stream_pdu.go | 2 +- syncapi/streams/streams.go | 2 +- syncapi/syncapi.go | 8 ++--- 9 files changed, 34 insertions(+), 41 deletions(-) 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/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/syncapi/routing/context.go b/syncapi/routing/context.go index f5f4b2dd0..87cc2aae0 100644 --- a/syncapi/routing/context.go +++ b/syncapi/routing/context.go @@ -45,7 +45,7 @@ func Context( rsAPI roomserver.SyncRoomserverAPI, syncDB storage.Database, roomID, eventID string, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) util.JSONResponse { filter, err := parseRoomEventFilter(req) if err != nil { @@ -155,7 +155,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/messages.go b/syncapi/routing/messages.go index f19dfaed3..b0c990ec0 100644 --- a/syncapi/routing/messages.go +++ b/syncapi/routing/messages.go @@ -63,7 +63,7 @@ func OnIncomingMessagesRequest( rsAPI api.SyncRoomserverAPI, cfg *config.SyncAPI, srp *sync.RequestPool, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) util.JSONResponse { var err error diff --git a/syncapi/routing/routing.go b/syncapi/routing/routing.go index 245ee5b66..6bc495d8d 100644 --- a/syncapi/routing/routing.go +++ b/syncapi/routing/routing.go @@ -39,7 +39,7 @@ func Setup( userAPI userapi.SyncUserAPI, rsAPI api.SyncRoomserverAPI, cfg *config.SyncAPI, - lazyLoadCache *caching.LazyLoadCache, + lazyLoadCache caching.LazyLoadCache, ) { v3mux := csMux.PathPrefix("/{apiversion:(?:r0|v3)}/").Subrouter() diff --git a/syncapi/streams/stream_pdu.go b/syncapi/streams/stream_pdu.go index f774a1af8..00b3dfe3b 100644 --- a/syncapi/streams/stream_pdu.go +++ b/syncapi/streams/stream_pdu.go @@ -32,7 +32,7 @@ type PDUStreamProvider struct { tasks chan func() workers atomic.Int32 // userID+deviceID -> lazy loading cache - lazyLoadCache *caching.LazyLoadCache + lazyLoadCache caching.LazyLoadCache rsAPI roomserverAPI.SyncRoomserverAPI } diff --git a/syncapi/streams/streams.go b/syncapi/streams/streams.go index af2a0387e..1ca4ee8c3 100644 --- a/syncapi/streams/streams.go +++ b/syncapi/streams/streams.go @@ -27,7 +27,7 @@ type Streams struct { func NewSyncStreamProviders( d storage.Database, userAPI userapi.SyncUserAPI, rsAPI rsapi.SyncRoomserverAPI, keyAPI keyapi.SyncKeyAPI, - eduCache *caching.EDUCache, lazyLoadCache *caching.LazyLoadCache, notifier *notifier.Notifier, + eduCache *caching.EDUCache, lazyLoadCache caching.LazyLoadCache, notifier *notifier.Notifier, ) *Streams { streams := &Streams{ PDUStreamProvider: &PDUStreamProvider{ diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index 686e2044f..6da8ce6d1 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -53,12 +53,8 @@ func AddPublicRoutes( } 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 ") @@ -146,6 +142,6 @@ func AddPublicRoutes( routing.Setup( base.PublicClientAPIMux, requestPool, syncDB, userAPI, - rsAPI, cfg, lazyLoadCache, + rsAPI, cfg, base.Caches, ) } From 85c00208c5b804b2d95a6a790e1dd977c33d030e Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 6 May 2022 15:41:16 +0100 Subject: [PATCH 065/103] Fix power level event auth bugs (update to matrix-org/gomatrixserverlib#306) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 38d28e11e..58cfbba11 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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-20220506114534-f3951a683bad + github.com/matrix-org/gomatrixserverlib v0.0.0-20220506144035-8958f9dfdffd 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 diff --git a/go.sum b/go.sum index 96ac72637..047bbf5c3 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220506114534-f3951a683bad h1:QhPE4f2fijOSW3QvT/Ucwqz9KJwafKaiKwfhwgIpx3M= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220506114534-f3951a683bad/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220506144035-8958f9dfdffd h1:11Wh+NMPDE5UDEx50RJnxeYj7zH5HEClzGfVMAjUy9U= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220506144035-8958f9dfdffd/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= From 6bc6184d70614a9ba2cc585c88f66e6a780ddb98 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 6 May 2022 15:52:44 +0100 Subject: [PATCH 066/103] Simplify `calculateLatest` (#2430) * Simplify `calculateLatest` * Comments --- .../internal/input/input_latest_events.go | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/roomserver/internal/input/input_latest_events.go b/roomserver/internal/input/input_latest_events.go index 7e58ef9d0..9ad8b0422 100644 --- a/roomserver/internal/input/input_latest_events.go +++ b/roomserver/internal/input/input_latest_events.go @@ -316,40 +316,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 } From 633ca06eb9f7652a6c4be04b3ffe8950419a8ee3 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 6 May 2022 16:34:52 +0100 Subject: [PATCH 067/103] Version 0.8.3rc1 --- internal/version.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/version.go b/internal/version.go index 2477bc9ac..e74548831 100644 --- a/internal/version.go +++ b/internal/version.go @@ -17,8 +17,8 @@ var build string const ( VersionMajor = 0 VersionMinor = 8 - VersionPatch = 2 - VersionTag = "" // example: "rc1" + VersionPatch = 3 + VersionTag = "rc1" // example: "rc1" ) func VersionString() string { From 4c15c73b3abfb0cca0c95c7a21305a8329e2c23c Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 11:13:04 +0100 Subject: [PATCH 068/103] Add `(user_id, device_id)` index on OTK table (#2435) --- keyserver/storage/postgres/one_time_keys_table.go | 2 ++ keyserver/storage/sqlite3/one_time_keys_table.go | 2 ++ 2 files changed, 4 insertions(+) diff --git a/keyserver/storage/postgres/one_time_keys_table.go b/keyserver/storage/postgres/one_time_keys_table.go index 0b143a1aa..d8c76b49b 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 = "" + diff --git a/keyserver/storage/sqlite3/one_time_keys_table.go b/keyserver/storage/sqlite3/one_time_keys_table.go index 897839aca..d2c0b7b20 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 = "" + From 79e2fbc66368d8f4754b9fff8005d3e77969fcc4 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 13:53:51 +0100 Subject: [PATCH 069/103] Update to matrix-org/gomatrixserverlib#307 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 58cfbba11..803272c56 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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-20220506144035-8958f9dfdffd + github.com/matrix-org/gomatrixserverlib v0.0.0-20220509120958-8d818048c34c 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 diff --git a/go.sum b/go.sum index 047bbf5c3..5fde89e06 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220506144035-8958f9dfdffd h1:11Wh+NMPDE5UDEx50RJnxeYj7zH5HEClzGfVMAjUy9U= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220506144035-8958f9dfdffd/go.mod h1:V5eO8rn/C3rcxig37A/BCeKerLFS+9Avg/77FIeTZ48= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220509120958-8d818048c34c h1:KqzqFWxvs90pcDaW9QEveW+Q5JcEYuNnKyaqXc+ohno= +github.com/matrix-org/gomatrixserverlib v0.0.0-20220509120958-8d818048c34c/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= From 09d754cfbf9268044d0f59fbe509640b8d71e011 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 14:15:24 +0100 Subject: [PATCH 070/103] One NATS instance per `BaseDendrite` (#2438) * One NATS instance per `BaseDendrite` * Fix roomserver --- appservice/appservice.go | 3 +-- clientapi/clientapi.go | 2 +- federationapi/federationapi.go | 4 +-- keyserver/keyserver.go | 2 +- roomserver/internal/input/input_test.go | 12 ++++----- roomserver/roomserver.go | 2 +- setup/base/base.go | 3 +++ setup/jetstream/nats.go | 36 ++++++++++--------------- syncapi/syncapi.go | 2 +- test/base.go | 18 +++++++++++++ userapi/userapi.go | 2 +- 11 files changed, 49 insertions(+), 37 deletions(-) create mode 100644 test/base.go diff --git a/appservice/appservice.go b/appservice/appservice.go index bd292767b..c5ae9ceb2 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -32,7 +32,6 @@ 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" "github.com/matrix-org/gomatrixserverlib" ) @@ -55,7 +54,7 @@ func NewInternalAPI( gomatrixserverlib.WithSkipVerify(base.Cfg.AppServiceAPI.DisableTLSValidation), ) - 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, &base.Cfg.AppServiceAPI.Database) diff --git a/clientapi/clientapi.go b/clientapi/clientapi.go index c1e86114b..f550c29bb 100644 --- a/clientapi/clientapi.go +++ b/clientapi/clientapi.go @@ -44,7 +44,7 @@ func AddPublicRoutes( ) { cfg := &base.Cfg.ClientAPI mscCfg := &base.Cfg.MSCs - js, natsClient := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) syncProducer := &producers.SyncAPIProducer{ JetStream: js, diff --git a/federationapi/federationapi.go b/federationapi/federationapi.go index e52377c94..bec9ac777 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -56,7 +56,7 @@ func AddPublicRoutes( ) { cfg := &base.Cfg.FederationAPI mscCfg := &base.Cfg.MSCs - js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) producer := &producers.SyncAPIProducer{ JetStream: js, TopicReceiptEvent: cfg.Matrix.JetStream.Prefixed(jetstream.OutputReceiptEvent), @@ -115,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/keyserver/keyserver.go b/keyserver/keyserver.go index 007a48a55..47d7f57f9 100644 --- a/keyserver/keyserver.go +++ b/keyserver/keyserver.go @@ -39,7 +39,7 @@ func AddInternalRoutes(router *mux.Router, intAPI api.KeyInternalAPI) { func NewInternalAPI( base *base.BaseDendrite, cfg *config.KeyServer, fedClient fedsenderapi.FederationClient, ) api.KeyInternalAPI { - js, _ := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) db, err := storage.NewDatabase(base, &cfg.Database) if err != nil { diff --git a/roomserver/internal/input/input_test.go b/roomserver/internal/input/input_test.go index 5d34842bf..a95c13550 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" "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 = test.Base(nil) code := m.Run() - pc.ShutdownDendrite() - pc.WaitForComponentsToFinish() + b.ShutdownDendrite() + b.WaitForComponentsToFinish() os.Exit(code) } diff --git a/roomserver/roomserver.go b/roomserver/roomserver.go index 46261eb3e..1480e8942 100644 --- a/roomserver/roomserver.go +++ b/roomserver/roomserver.go @@ -50,7 +50,7 @@ func NewInternalAPI( 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/setup/base/base.go b/setup/base/base.go index ef449cc35..0e7528a03 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -41,6 +41,7 @@ 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" "github.com/gorilla/mux" @@ -77,6 +78,7 @@ type BaseDendrite struct { InternalAPIMux *mux.Router DendriteAdminMux *mux.Router SynapseAdminMux *mux.Router + NATS *jetstream.NATSInstance UseHTTPAPIs bool apiHttpClient *http.Client Cfg *config.Dendrite @@ -240,6 +242,7 @@ 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 diff --git a/setup/jetstream/nats.go b/setup/jetstream/nats.go index 8d5289697..426f02bb6 100644 --- a/setup/jetstream/nats.go +++ b/setup/jetstream/nats.go @@ -13,31 +13,23 @@ import ( "github.com/sirupsen/logrus" natsserver "github.com/nats-io/nats-server/v2/server" - "github.com/nats-io/nats.go" 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 (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, @@ -49,23 +41,23 @@ func Prepare(process *process.ProcessContext, cfg *config.JetStream) (natsclient 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/syncapi/syncapi.go b/syncapi/syncapi.go index 6da8ce6d1..dbc6e240c 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -45,7 +45,7 @@ func AddPublicRoutes( ) { cfg := &base.Cfg.SyncAPI - js, natsClient := jetstream.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) + js, natsClient := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) syncDB, err := storage.NewSyncServerDatasource(base, &cfg.Database) if err != nil { diff --git a/test/base.go b/test/base.go new file mode 100644 index 000000000..32fc8dc53 --- /dev/null +++ b/test/base.go @@ -0,0 +1,18 @@ +package test + +import ( + "github.com/matrix-org/dendrite/setup/base" + "github.com/matrix-org/dendrite/setup/config" + "github.com/nats-io/nats.go" +) + +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/userapi/userapi.go b/userapi/userapi.go index 03a46807f..603b416bf 100644 --- a/userapi/userapi.go +++ b/userapi/userapi.go @@ -47,7 +47,7 @@ func NewInternalAPI( 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, From f69ebc6af2dfeeb7af7eaabbe0609976c397a685 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 9 May 2022 15:30:32 +0200 Subject: [PATCH 071/103] Add roomserver tests (1/?) (#2434) * Add EventJSONTable tests * Add eventJSON tests * Add EventStateKeysTable tests * Add EventTypesTable tests * Add Events Table tests Move variable declaration outside loops Switch to testify/assert for tests * Move variable declaration outside loop * Remove random data * Fix issue where the EventReferenceSHA256 is not set * Add more tests * Revert "Fix issue where the EventReferenceSHA256 is not set" This reverts commit 8ae34c4e5f78584f0edb479f5a893556d2b95d19. * Update GMSL * Add tests for duplicate entries * Test what happens if we select non-existing NIDs * Add test for non-existing eventType * Really update GMSL --- go.mod | 6 +- go.sum | 12 +- .../storage/postgres/event_json_table.go | 6 +- .../postgres/event_state_keys_table.go | 12 +- .../storage/postgres/event_types_table.go | 8 +- roomserver/storage/postgres/events_table.go | 32 ++-- roomserver/storage/postgres/storage.go | 16 +- .../storage/sqlite3/event_json_table.go | 6 +- .../storage/sqlite3/event_state_keys_table.go | 12 +- .../storage/sqlite3/event_types_table.go | 8 +- roomserver/storage/sqlite3/events_table.go | 35 ++-- roomserver/storage/sqlite3/storage.go | 16 +- .../storage/tables/event_json_table_test.go | 95 +++++++++++ .../tables/event_state_keys_table_test.go | 79 +++++++++ .../storage/tables/event_types_table_test.go | 79 +++++++++ .../storage/tables/events_table_test.go | 157 ++++++++++++++++++ roomserver/storage/tables/interface.go | 8 +- 17 files changed, 499 insertions(+), 88 deletions(-) create mode 100644 roomserver/storage/tables/event_json_table_test.go create mode 100644 roomserver/storage/tables/event_state_keys_table_test.go create mode 100644 roomserver/storage/tables/event_types_table_test.go create mode 100644 roomserver/storage/tables/events_table_test.go diff --git a/go.mod b/go.mod index 803272c56..d14ced5b7 100644 --- a/go.mod +++ b/go.mod @@ -48,17 +48,17 @@ require ( github.com/prometheus/client_golang v1.12.1 github.com/sirupsen/logrus v1.8.1 github.com/stretchr/testify v1.7.0 - github.com/tidwall/gjson v1.14.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 5fde89e06..8b518935c 100644 --- a/go.sum +++ b/go.sum @@ -1130,8 +1130,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= @@ -1281,8 +1281,8 @@ golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm 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-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= @@ -1543,8 +1543,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/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..86d226ce7 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{ @@ -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/storage.go b/roomserver/storage/postgres/storage.go index da8d25848..34e891490 100644 --- a/roomserver/storage/postgres/storage.go +++ b/roomserver/storage/postgres/storage.go @@ -68,16 +68,16 @@ func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache c } 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 { @@ -112,19 +112,19 @@ func (d *Database) create(db *sql.DB) error { } func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { - eventStateKeys, err := prepareEventStateKeysTable(db) + 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 } 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..feb06150a 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, } @@ -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/storage.go b/roomserver/storage/sqlite3/storage.go index e6cf1a53f..9522d3058 100644 --- a/roomserver/storage/sqlite3/storage.go +++ b/roomserver/storage/sqlite3/storage.go @@ -77,16 +77,16 @@ func Open(base *base.BaseDendrite, dbProperties *config.DatabaseOptions, cache c } 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 { @@ -121,19 +121,19 @@ func (d *Database) create(db *sql.DB) error { } func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.RoomServerCaches) error { - eventStateKeys, err := prepareEventStateKeysTable(db) + 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 } 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..d5d699c4c --- /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() + 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..95609787a 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) From 1a7f4c8aa978d7e2f6046b6628ecf523460eee28 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 15:22:33 +0100 Subject: [PATCH 072/103] Don't try to re-fetch the event if it is listed in `adds_state_event_ids` (#2437) * Don't try to re-fetch the event in the output message * Try that again * Add the initial event into the set --- syncapi/consumers/roomserver.go | 75 ++++++++++++++++++++++++--------- 1 file changed, 55 insertions(+), 20 deletions(-) diff --git a/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go index 7712c8403..e1c2ea823 100644 --- a/syncapi/consumers/roomserver.go +++ b/syncapi/consumers/roomserver.go @@ -154,41 +154,76 @@ func (s *OutputRoomEventConsumer) onNewRoomEvent( ctx context.Context, msg api.OutputNewRoomEvent, ) error { ev := msg.Event - addsStateEvents := []*gomatrixserverlib.HeaderedEvent{} - foundEventIDs := map[string]bool{} - if len(msg.AddsStateEventIDs) > 0 { - for _, eventID := range msg.AddsStateEventIDs { - foundEventIDs[eventID] = false + + // 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 msg.AddsStateEventIDs { + if eventID == ev.EventID() { + knownEventIDs[eventID] = true + addsStateEvents = append(addsStateEvents, ev) + } else { + knownEventIDs[eventID] = false } - foundEvents, err := s.db.Events(ctx, msg.AddsStateEventIDs) + } + + // Work out which events we want to look up in the sync API database. + // At this stage the only event that should be excluded is the event + // supplied in the request, if it appears in the adds_state_event_ids. + missingEventIDs := make([]string, 0, len(msg.AddsStateEventIDs)) + for eventID, known := range knownEventIDs { + if !known { + missingEventIDs = append(missingEventIDs, eventID) + } + } + + // 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, msg.AddsStateEventIDs) 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) From 79da75d483d3ee554722000975e13776e4e8a656 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 16:19:35 +0100 Subject: [PATCH 073/103] Federation consumer `adds_state_event_ids` tweak (#2441) * Don't ask roomserver for events we already have in federation API * Check number of events returned is as expected * Preallocate array * Improve shape a bit --- federationapi/consumers/roomserver.go | 25 +++++++++++-------------- roomserver/api/output.go | 13 +++++++++++++ syncapi/consumers/roomserver.go | 23 ++++------------------- 3 files changed, 28 insertions(+), 33 deletions(-) diff --git a/federationapi/consumers/roomserver.go b/federationapi/consumers/roomserver.go index ff2c8e5d4..80317ee69 100644 --- a/federationapi/consumers/roomserver.go +++ b/federationapi/consumers/roomserver.go @@ -146,28 +146,25 @@ 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 { + 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 } 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/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go index e1c2ea823..63bde8166 100644 --- a/syncapi/consumers/roomserver.go +++ b/syncapi/consumers/roomserver.go @@ -154,35 +154,20 @@ func (s *OutputRoomEventConsumer) onNewRoomEvent( ctx context.Context, msg api.OutputNewRoomEvent, ) error { ev := msg.Event - addsStateEvents := []*gomatrixserverlib.HeaderedEvent{} + addsStateEvents, missingEventIDs := msg.NeededStateEventIDs() // 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 msg.AddsStateEventIDs { - if eventID == ev.EventID() { - knownEventIDs[eventID] = true - addsStateEvents = append(addsStateEvents, ev) - } else { - knownEventIDs[eventID] = false - } - } - - // Work out which events we want to look up in the sync API database. - // At this stage the only event that should be excluded is the event - // supplied in the request, if it appears in the adds_state_event_ids. - missingEventIDs := make([]string, 0, len(msg.AddsStateEventIDs)) - for eventID, known := range knownEventIDs { - if !known { - missingEventIDs = append(missingEventIDs, eventID) - } + 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, msg.AddsStateEventIDs) + alreadyKnown, err := s.db.Events(ctx, missingEventIDs) if err != nil { return fmt.Errorf("s.db.Events: %w", err) } From a443d1e5f3796942f68067741f4bdd482548bfd7 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 16:25:22 +0100 Subject: [PATCH 074/103] Don't store invites in sync API that aren't relevant to local users (#2439) --- syncapi/consumers/roomserver.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/syncapi/consumers/roomserver.go b/syncapi/consumers/roomserver.go index 63bde8166..f0ca2106f 100644 --- a/syncapi/consumers/roomserver.go +++ b/syncapi/consumers/roomserver.go @@ -347,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) From 236b16aa6c97bc0894388dce7f6b420ef7a1fd88 Mon Sep 17 00:00:00 2001 From: kegsay Date: Mon, 9 May 2022 17:23:02 +0100 Subject: [PATCH 075/103] Begin adding syncapi component tests (#2442) * Add very basic syncapi tests * Add a way to inject jetstream messages * implement add_state_ids * bugfixes * Unbreak tests * Remove now un-needed API call * Linting --- federationapi/federationapi_keys_test.go | 2 +- setup/base/base.go | 12 +- setup/jetstream/nats.go | 8 ++ syncapi/sync/requestpool.go | 10 +- syncapi/syncapi.go | 2 +- syncapi/syncapi_test.go | 162 +++++++++++++++++++++++ test/base.go | 72 ++++++++++ test/http.go | 45 +++++++ test/jetstream.go | 35 +++++ 9 files changed, 337 insertions(+), 11 deletions(-) create mode 100644 syncapi/syncapi_test.go create mode 100644 test/http.go create mode 100644 test/jetstream.go 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/setup/base/base.go b/setup/base/base.go index 0e7528a03..5cbd7da9c 100644 --- a/setup/base/base.go +++ b/setup/base/base.go @@ -86,6 +86,7 @@ type BaseDendrite struct { DNSCache *gomatrixserverlib.DNSCache Database *sql.DB DatabaseWriter sqlutil.Writer + EnableMetrics bool } const NoListener = "" @@ -96,7 +97,7 @@ const HTTPClientTimeout = time.Second * 30 type BaseDendriteOptions int const ( - NoCacheMetrics BaseDendriteOptions = iota + DisableMetrics BaseDendriteOptions = iota UseHTTPAPIs PolylithMode ) @@ -107,12 +108,12 @@ 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: @@ -160,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") } @@ -246,6 +247,7 @@ func NewBaseDendrite(cfg *config.Dendrite, componentName string, options ...Base apiHttpClient: &apiClient, Database: db, // set if monolith with global connection pool only DatabaseWriter: writer, // set if monolith with global connection pool only + EnableMetrics: enableMetrics, } } diff --git a/setup/jetstream/nats.go b/setup/jetstream/nats.go index 426f02bb6..248b0e656 100644 --- a/setup/jetstream/nats.go +++ b/setup/jetstream/nats.go @@ -13,6 +13,7 @@ import ( "github.com/sirupsen/logrus" natsserver "github.com/nats-io/nats-server/v2/server" + "github.com/nats-io/nats.go" natsclient "github.com/nats-io/nats.go" ) @@ -21,6 +22,13 @@ type NATSInstance struct { sync.Mutex } +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 { diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 99d1e40c3..8ab130911 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -65,11 +65,13 @@ func NewRequestPool( userAPI userapi.SyncUserAPI, keyAPI keyapi.SyncKeyAPI, rsAPI roomserverAPI.SyncRoomserverAPI, streams *streams.Streams, notifier *notifier.Notifier, - producer PresencePublisher, + producer PresencePublisher, enableMetrics bool, ) *RequestPool { - prometheus.MustRegister( - activeSyncRequests, waitingSyncRequests, - ) + if enableMetrics { + prometheus.MustRegister( + activeSyncRequests, waitingSyncRequests, + ) + } rp := &RequestPool{ db: db, cfg: cfg, diff --git a/syncapi/syncapi.go b/syncapi/syncapi.go index dbc6e240c..d8bacb2da 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -65,7 +65,7 @@ func AddPublicRoutes( JetStream: js, } - requestPool := sync.NewRequestPool(syncDB, cfg, userAPI, keyAPI, rsAPI, streams, notifier, federationPresenceProducer) + requestPool := sync.NewRequestPool(syncDB, cfg, userAPI, keyAPI, rsAPI, streams, notifier, federationPresenceProducer, base.EnableMetrics) userAPIStreamEventProducer := &producers.UserAPIStreamEventProducer{ JetStream: js, diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go new file mode 100644 index 000000000..12b5178d8 --- /dev/null +++ b/syncapi/syncapi_test.go @@ -0,0 +1,162 @@ +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/jetstream" + "github.com/matrix-org/dendrite/syncapi/types" + "github.com/matrix-org/dendrite/test" + userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/nats-io/nats.go" +) + +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 +} + +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.KeyInternalAPI +} + +func TestSyncAPI(t *testing.T) { + test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { + testSync(t, dbType) + }) +} + +func testSync(t *testing.T, dbType test.DBType) { + user := test.NewUser() + room := test.NewRoom(t, user) + alice := userapi.Device{ + ID: "ALICEID", + UserID: user.ID, + AccessToken: "ALICE_BEARER_TOKEN", + DisplayName: "Alice", + AccountType: userapi.AccountTypeUser, + } + + base, close := test.CreateBaseDendrite(t, dbType) + defer close() + + jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) + defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) + var msgs []*nats.Msg + for _, ev := range room.Events() { + var addsStateIDs []string + if ev.StateKey() != nil { + addsStateIDs = append(addsStateIDs, ev.EventID()) + } + msgs = append(msgs, test.NewOutputEventMsg(t, base, room.ID, api.OutputEvent{ + Type: rsapi.OutputTypeNewRoomEvent, + NewRoomEvent: &rsapi.OutputNewRoomEvent{ + Event: ev, + AddsStateEventIDs: addsStateIDs, + }, + })) + } + AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) + test.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()) + } + } +} diff --git a/test/base.go b/test/base.go index 32fc8dc53..664442c03 100644 --- a/test/base.go +++ b/test/base.go @@ -1,11 +1,83 @@ +// 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 test import ( + "errors" + "fmt" + "io/fs" + "os" + "strings" + "testing" + "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" "github.com/nats-io/nats.go" ) +func CreateBaseDendrite(t *testing.T, dbType DBType) (*base.BaseDendrite, func()) { + var cfg config.Dendrite + cfg.Defaults(false) + cfg.Global.JetStream.InMemory = true + + switch dbType { + case 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 := 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 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{} diff --git a/test/http.go b/test/http.go new file mode 100644 index 000000000..a458a3385 --- /dev/null +++ b/test/http.go @@ -0,0 +1,45 @@ +package test + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/url" + "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 +} diff --git a/test/jetstream.go b/test/jetstream.go new file mode 100644 index 000000000..488c22beb --- /dev/null +++ b/test/jetstream.go @@ -0,0 +1,35 @@ +package test + +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 +} From 6b3c183396233a6d03102535b238b713617ae2ac Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 17:31:14 +0100 Subject: [PATCH 076/103] Version 0.8.3 (#2431) * Version 0.8.3 * Update changelog --- CHANGES.md | 36 ++++++++++++++++++++++++++++++++++++ internal/version.go | 2 +- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 6278bcba4..b13908f73 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,41 @@ # Changelog +## 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 diff --git a/internal/version.go b/internal/version.go index e74548831..08c02cfcd 100644 --- a/internal/version.go +++ b/internal/version.go @@ -18,7 +18,7 @@ const ( VersionMajor = 0 VersionMinor = 8 VersionPatch = 3 - VersionTag = "rc1" // example: "rc1" + VersionTag = "" // example: "rc1" ) func VersionString() string { From 1b3fa9689ca28de2337b67253089e286694c60e9 Mon Sep 17 00:00:00 2001 From: database64128 Date: Tue, 10 May 2022 00:51:30 +0800 Subject: [PATCH 077/103] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20mediaapi/thumbn?= =?UTF-8?q?ailer:=20fix=20build=20with=20bimg=20(#2440)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: kegsay --- mediaapi/thumbnailer/thumbnailer_bimg.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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{ From 77722c5a4f5330f6fe517edc2d11bcba8c1fc274 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 10 May 2022 11:08:10 +0100 Subject: [PATCH 078/103] Back out matrix-org/dendrite#2421 by restoring `http.Client`s This creates problems with non-HTTPS endpoints and should fix #2444. --- appservice/appservice.go | 23 +++++++++++++-------- appservice/query/query.go | 8 +++---- appservice/workers/transaction_scheduler.go | 8 +++---- clientapi/threepid/invites.go | 4 ++-- internal/pushgateway/client.go | 22 ++++++++++++-------- userapi/util/phonehomestats.go | 12 ++++++----- 6 files changed, 44 insertions(+), 33 deletions(-) diff --git a/appservice/appservice.go b/appservice/appservice.go index c5ae9ceb2..8fe1b2fc4 100644 --- a/appservice/appservice.go +++ b/appservice/appservice.go @@ -16,6 +16,8 @@ package appservice import ( "context" + "crypto/tls" + "net/http" "sync" "time" @@ -33,7 +35,6 @@ import ( "github.com/matrix-org/dendrite/setup/base" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" - "github.com/matrix-org/gomatrixserverlib" ) // AddInternalRoutes registers HTTP handlers for internal API calls @@ -45,15 +46,19 @@ func AddInternalRoutes(router *mux.Router, queryAPI appserviceAPI.AppServiceInte // can call functions directly on the returned API or via an HTTP interface using AddInternalRoutes. func NewInternalAPI( base *base.BaseDendrite, - userAPI userapi.AppserviceUserAPI, - rsAPI roomserverAPI.AppserviceRoomserverAPI, + userAPI userapi.UserInternalAPI, + rsAPI roomserverAPI.RoomserverInternalAPI, ) appserviceAPI.AppServiceInternalAPI { - client := gomatrixserverlib.NewClient( - gomatrixserverlib.WithTimeout(time.Second*30), - gomatrixserverlib.WithKeepAlives(false), - gomatrixserverlib.WithSkipVerify(base.Cfg.AppServiceAPI.DisableTLSValidation), - ) - + client := &http.Client{ + Timeout: time.Second * 30, + Transport: &http.Transport{ + DisableKeepAlives: true, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: base.Cfg.AppServiceAPI.DisableTLSValidation, + }, + Proxy: http.ProxyFromEnvironment, + }, + } js, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) // Create a connection to the appservice postgres DB diff --git a/appservice/query/query.go b/appservice/query/query.go index b7b0b335a..dacd3caa8 100644 --- a/appservice/query/query.go +++ b/appservice/query/query.go @@ -23,7 +23,6 @@ import ( "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/setup/config" - "github.com/matrix-org/gomatrixserverlib" opentracing "github.com/opentracing/opentracing-go" log "github.com/sirupsen/logrus" ) @@ -33,7 +32,7 @@ const userIDExistsPath = "/users/" // AppServiceQueryAPI is an implementation of api.AppServiceQueryAPI type AppServiceQueryAPI struct { - HTTPClient *gomatrixserverlib.Client + HTTPClient *http.Client Cfg *config.Dendrite } @@ -65,8 +64,9 @@ func (a *AppServiceQueryAPI) RoomAliasExists( if err != nil { return err } + req = req.WithContext(ctx) - resp, err := a.HTTPClient.DoHTTPRequest(ctx, req) + resp, err := a.HTTPClient.Do(req) if resp != nil { defer func() { err = resp.Body.Close() @@ -130,7 +130,7 @@ func (a *AppServiceQueryAPI) UserIDExists( if err != nil { return err } - resp, err := a.HTTPClient.DoHTTPRequest(ctx, req) + resp, err := a.HTTPClient.Do(req.WithContext(ctx)) if resp != nil { defer func() { err = resp.Body.Close() diff --git a/appservice/workers/transaction_scheduler.go b/appservice/workers/transaction_scheduler.go index 47d447c2c..4dab00bd7 100644 --- a/appservice/workers/transaction_scheduler.go +++ b/appservice/workers/transaction_scheduler.go @@ -42,7 +42,7 @@ var ( // size), then send that off to the AS's /transactions/{txnID} endpoint. It also // handles exponentially backing off in case the AS isn't currently available. func SetupTransactionWorkers( - client *gomatrixserverlib.Client, + client *http.Client, appserviceDB storage.Database, workerStates []types.ApplicationServiceWorkerState, ) error { @@ -58,7 +58,7 @@ func SetupTransactionWorkers( // worker is a goroutine that sends any queued events to the application service // it is given. -func worker(client *gomatrixserverlib.Client, db storage.Database, ws types.ApplicationServiceWorkerState) { +func worker(client *http.Client, db storage.Database, ws types.ApplicationServiceWorkerState) { log.WithFields(log.Fields{ "appservice": ws.AppService.ID, }).Info("Starting application service") @@ -200,7 +200,7 @@ func createTransaction( // send sends events to an application service. Returns an error if an OK was not // received back from the application service or the request timed out. func send( - client *gomatrixserverlib.Client, + client *http.Client, appservice config.ApplicationService, txnID int, transaction []byte, @@ -213,7 +213,7 @@ func send( return err } req.Header.Set("Content-Type", "application/json") - resp, err := client.DoHTTPRequest(context.TODO(), req) + resp, err := client.Do(req) if err != nil { return err } diff --git a/clientapi/threepid/invites.go b/clientapi/threepid/invites.go index 6e7426a7f..9670fecad 100644 --- a/clientapi/threepid/invites.go +++ b/clientapi/threepid/invites.go @@ -231,7 +231,7 @@ func queryIDServerStoreInvite( profile = &authtypes.Profile{} } - client := gomatrixserverlib.NewClient() + client := http.Client{} data := url.Values{} data.Add("medium", body.Medium) @@ -253,7 +253,7 @@ func queryIDServerStoreInvite( } req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - resp, err := client.DoHTTPRequest(ctx, req) + resp, err := client.Do(req.WithContext(ctx)) if err != nil { return nil, err } diff --git a/internal/pushgateway/client.go b/internal/pushgateway/client.go index 231327a1e..95f5afd90 100644 --- a/internal/pushgateway/client.go +++ b/internal/pushgateway/client.go @@ -3,28 +3,32 @@ package pushgateway import ( "bytes" "context" + "crypto/tls" "encoding/json" "fmt" "net/http" "time" - "github.com/matrix-org/gomatrixserverlib" "github.com/opentracing/opentracing-go" ) type httpClient struct { - hc *gomatrixserverlib.Client + hc *http.Client } // NewHTTPClient creates a new Push Gateway client. func NewHTTPClient(disableTLSValidation bool) Client { - return &httpClient{ - hc: gomatrixserverlib.NewClient( - gomatrixserverlib.WithTimeout(time.Second*30), - gomatrixserverlib.WithKeepAlives(false), - gomatrixserverlib.WithSkipVerify(disableTLSValidation), - ), + hc := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DisableKeepAlives: true, + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: disableTLSValidation, + }, + Proxy: http.ProxyFromEnvironment, + }, } + return &httpClient{hc: hc} } func (h *httpClient) Notify(ctx context.Context, url string, req *NotifyRequest, resp *NotifyResponse) error { @@ -41,7 +45,7 @@ func (h *httpClient) Notify(ctx context.Context, url string, req *NotifyRequest, } hreq.Header.Set("Content-Type", "application/json") - hresp, err := h.hc.DoHTTPRequest(ctx, hreq) + hresp, err := h.hc.Do(hreq) if err != nil { return err } diff --git a/userapi/util/phonehomestats.go b/userapi/util/phonehomestats.go index e24daba6b..ad93a50e3 100644 --- a/userapi/util/phonehomestats.go +++ b/userapi/util/phonehomestats.go @@ -39,7 +39,7 @@ type phoneHomeStats struct { cfg *config.Dendrite db storage.Statistics isMonolith bool - client *gomatrixserverlib.Client + client *http.Client } type timestampToRUUsage struct { @@ -55,9 +55,10 @@ func StartPhoneHomeCollector(startTime time.Time, cfg *config.Dendrite, statsDB cfg: cfg, db: statsDB, isMonolith: cfg.IsMonolith, - client: gomatrixserverlib.NewClient( - gomatrixserverlib.WithTimeout(time.Second * 30), - ), + client: &http.Client{ + Timeout: time.Second * 30, + Transport: http.DefaultTransport, + }, } // start initial run after 5min @@ -151,7 +152,8 @@ func (p *phoneHomeStats) collect() { } request.Header.Set("User-Agent", "Dendrite/"+internal.VersionString()) - if _, err = p.client.DoHTTPRequest(ctx, request); err != nil { + _, err = p.client.Do(request) + if err != nil { logrus.WithError(err).Error("unable to send anonymous stats") return } From e2a932ec0b4b1568b48726c9a855485f596d07ce Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 10 May 2022 11:23:36 +0100 Subject: [PATCH 079/103] Add indexes to `syncapi_output_room_events` table that satisfy the filters (#2446) --- syncapi/storage/postgres/output_room_events_table.go | 5 +++++ syncapi/storage/sqlite3/output_room_events_table.go | 5 +++++ 2 files changed, 10 insertions(+) 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/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 = "" + From 1897e2f1c07f1b06a540aa2b4ccedfc67008e52a Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Tue, 10 May 2022 12:44:29 +0100 Subject: [PATCH 080/103] Version 0.8.4 --- CHANGES.md | 8 ++++++++ internal/version.go | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index b13908f73..c058da6a1 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,13 @@ # Changelog +## 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 diff --git a/internal/version.go b/internal/version.go index 08c02cfcd..5097bb2a6 100644 --- a/internal/version.go +++ b/internal/version.go @@ -17,7 +17,7 @@ var build string const ( VersionMajor = 0 VersionMinor = 8 - VersionPatch = 3 + VersionPatch = 4 VersionTag = "" // example: "rc1" ) From 6db08b2874307c516b10ef9c9e996807fbfdb1ff Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Tue, 10 May 2022 14:41:12 +0200 Subject: [PATCH 081/103] Add roomserver tests (2/?) (#2445) * Add invite table tests; move variable declarations * Add Membership table tests * Move variable declarations * Add PrevEvents table tests * Add Published table test * Add Redactions tests Fix bug in SQLite markRedactionValidatedSQL * PR comments, better readability for invite tests --- roomserver/storage/postgres/invite_table.go | 10 +- .../storage/postgres/membership_table.go | 20 +-- .../storage/postgres/previous_events_table.go | 4 +- .../storage/postgres/published_table.go | 6 +- .../storage/postgres/redactions_table.go | 4 +- roomserver/storage/postgres/storage.go | 20 +-- roomserver/storage/sqlite3/invite_table.go | 10 +- .../storage/sqlite3/membership_table.go | 20 +-- .../storage/sqlite3/previous_events_table.go | 4 +- roomserver/storage/sqlite3/published_table.go | 6 +- .../storage/sqlite3/redactions_table.go | 8 +- roomserver/storage/sqlite3/storage.go | 20 +-- .../storage/tables/invite_table_test.go | 92 +++++++++++++ .../storage/tables/membership_table_test.go | 130 ++++++++++++++++++ .../tables/previous_events_table_test.go | 61 ++++++++ .../storage/tables/published_table_test.go | 79 +++++++++++ .../storage/tables/redactions_table_test.go | 89 ++++++++++++ 17 files changed, 517 insertions(+), 66 deletions(-) create mode 100644 roomserver/storage/tables/invite_table_test.go create mode 100644 roomserver/storage/tables/membership_table_test.go create mode 100644 roomserver/storage/tables/previous_events_table_test.go create mode 100644 roomserver/storage/tables/published_table_test.go create mode 100644 roomserver/storage/tables/redactions_table_test.go 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 6ed5293e4..c01753c3a 100644 --- a/roomserver/storage/postgres/membership_table.go +++ b/roomserver/storage/postgres/membership_table.go @@ -160,12 +160,12 @@ type membershipStatements struct { selectServerInRoomStmt *sql.Stmt } -func createMembershipTable(db *sql.DB) error { +func CreateMembershipTable(db *sql.DB) error { _, err := db.Exec(membershipSchema) return err } -func prepareMembershipTable(db *sql.DB) (tables.Membership, error) { +func PrepareMembershipTable(db *sql.DB) (tables.Membership, error) { s := &membershipStatements{} return s, sqlutil.StatementList{ @@ -234,8 +234,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 } @@ -262,8 +262,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 } @@ -298,8 +298,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 } @@ -320,9 +320,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 } @@ -342,12 +342,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/storage.go b/roomserver/storage/postgres/storage.go index 34e891490..88df72009 100644 --- a/roomserver/storage/postgres/storage.go +++ b/roomserver/storage/postgres/storage.go @@ -89,22 +89,22 @@ func (d *Database) create(db *sql.DB) error { 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 { 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 } @@ -140,7 +140,7 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room if err != nil { return err } - prevEvents, err := preparePrevEventsTable(db) + prevEvents, err := PreparePrevEventsTable(db) if err != nil { return err } @@ -148,19 +148,19 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room 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 } 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 7ed86b612..6f0fe8b64 100644 --- a/roomserver/storage/sqlite3/membership_table.go +++ b/roomserver/storage/sqlite3/membership_table.go @@ -136,12 +136,12 @@ type membershipStatements struct { selectServerInRoomStmt *sql.Stmt } -func createMembershipTable(db *sql.DB) error { +func CreateMembershipTable(db *sql.DB) error { _, err := db.Exec(membershipSchema) return err } -func prepareMembershipTable(db *sql.DB) (tables.Membership, error) { +func PrepareMembershipTable(db *sql.DB) (tables.Membership, error) { s := &membershipStatements{ db: db, } @@ -212,8 +212,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 } @@ -239,8 +239,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 } @@ -275,8 +275,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 } @@ -307,9 +307,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 } @@ -326,12 +326,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/storage.go b/roomserver/storage/sqlite3/storage.go index 9522d3058..a4e32d528 100644 --- a/roomserver/storage/sqlite3/storage.go +++ b/roomserver/storage/sqlite3/storage.go @@ -98,22 +98,22 @@ func (d *Database) create(db *sql.DB) error { 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 { 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 } @@ -149,7 +149,7 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room if err != nil { return err } - prevEvents, err := preparePrevEventsTable(db) + prevEvents, err := PreparePrevEventsTable(db) if err != nil { return err } @@ -157,19 +157,19 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room 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 } 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..96d7bfed0 --- /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() + 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..87662ed4c --- /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() + + 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) + }) +} From c15bfefd0dbbd9619c2606b59b784f2a7926ca20 Mon Sep 17 00:00:00 2001 From: kegsay Date: Wed, 11 May 2022 11:29:23 +0100 Subject: [PATCH 082/103] Add RoomExists flag to QueryMembershipForUser (#2450) Fixes https://github.com/matrix-org/complement/pull/369 --- clientapi/routing/membership.go | 12 ++++++++++++ clientapi/routing/state.go | 6 ++++++ roomserver/api/query.go | 1 + roomserver/internal/query/query.go | 4 +++- syncapi/routing/context.go | 6 ++++++ syncapi/routing/messages.go | 14 ++++++++++---- 6 files changed, 38 insertions(+), 5 deletions(-) diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go index cfdf6f2de..77f627eb2 100644 --- a/clientapi/routing/membership.go +++ b/clientapi/routing/membership.go @@ -188,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{ @@ -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/state.go b/clientapi/routing/state.go index c6e9e91d0..12984c39a 100644 --- a/clientapi/routing/state.go +++ b/clientapi/routing/state.go @@ -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 diff --git a/roomserver/api/query.go b/roomserver/api/query.go index ef2e6bb57..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 diff --git a/roomserver/internal/query/query.go b/roomserver/internal/query/query.go index 5b33ec3c3..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 { diff --git a/syncapi/routing/context.go b/syncapi/routing/context.go index 87cc2aae0..96438e184 100644 --- a/syncapi/routing/context.go +++ b/syncapi/routing/context.go @@ -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, diff --git a/syncapi/routing/messages.go b/syncapi/routing/messages.go index b0c990ec0..e55c661d6 100644 --- a/syncapi/routing/messages.go +++ b/syncapi/routing/messages.go @@ -68,10 +68,16 @@ func OnIncomingMessagesRequest( 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{ @@ -244,17 +250,17 @@ func OnIncomingMessagesRequest( } } -func checkIsRoomForgotten(ctx context.Context, roomID, userID string, rsAPI api.SyncRoomserverAPI) (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 From 9599b3686e02356e48a537a820b075523252ac64 Mon Sep 17 00:00:00 2001 From: kegsay Date: Wed, 11 May 2022 13:44:32 +0100 Subject: [PATCH 083/103] More syncapi tests (#2451) * WIP tests for flakey create event * Uncomment all database test --- syncapi/syncapi_test.go | 148 +++++++++++++++++++++++++++++++++++----- test/event.go | 3 +- 2 files changed, 132 insertions(+), 19 deletions(-) diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go index 12b5178d8..7809cdaba 100644 --- a/syncapi/syncapi_test.go +++ b/syncapi/syncapi_test.go @@ -11,10 +11,12 @@ import ( 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" userapi "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" "github.com/nats-io/nats.go" ) @@ -39,6 +41,14 @@ func (s *syncRoomserverAPI) QueryLatestEventsAndState(ctx context.Context, req * 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 @@ -60,16 +70,22 @@ func (s *syncUserAPI) PerformLastSeenUpdate(ctx context.Context, req *userapi.Pe } type syncKeyAPI struct { - keyapi.KeyInternalAPI + keyapi.SyncKeyAPI } -func TestSyncAPI(t *testing.T) { +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) { - testSync(t, dbType) + testSyncAccessTokens(t, dbType) }) } -func testSync(t *testing.T, dbType test.DBType) { +func testSyncAccessTokens(t *testing.T, dbType test.DBType) { user := test.NewUser() room := test.NewRoom(t, user) alice := userapi.Device{ @@ -85,20 +101,7 @@ func testSync(t *testing.T, dbType test.DBType) { jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) defer jetstream.DeleteAllStreams(jsctx, &base.Cfg.Global.JetStream) - var msgs []*nats.Msg - for _, ev := range room.Events() { - var addsStateIDs []string - if ev.StateKey() != nil { - addsStateIDs = append(addsStateIDs, ev.EventID()) - } - msgs = append(msgs, test.NewOutputEventMsg(t, base, room.ID, api.OutputEvent{ - Type: rsapi.OutputTypeNewRoomEvent, - NewRoomEvent: &rsapi.OutputNewRoomEvent{ - Event: ev, - AddsStateEventIDs: addsStateIDs, - }, - })) - } + msgs := toNATSMsgs(t, base, room.Events()) AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) test.MustPublishMsgs(t, jsctx, msgs...) @@ -160,3 +163,112 @@ func testSync(t *testing.T, dbType test.DBType) { } } } + +// 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() + room := test.NewRoom(t, user) + alice := userapi.Device{ + ID: "ALICEID", + UserID: user.ID, + AccessToken: "ALICE_BEARER_TOKEN", + DisplayName: "Alice", + AccountType: userapi.AccountTypeUser, + } + + base, close := test.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 { + test.MustPublishMsgs(t, jsctx, msg) + time.Sleep(50 * 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:]) + } +} + +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] = test.NewOutputEventMsg(t, base, ev.RoomID(), api.OutputEvent{ + Type: rsapi.OutputTypeNewRoomEvent, + NewRoomEvent: &rsapi.OutputNewRoomEvent{ + Event: ev, + AddsStateEventIDs: addsStateIDs, + }, + }) + } + return result +} diff --git a/test/event.go b/test/event.go index b2e2805ba..40cb8f0e1 100644 --- a/test/event.go +++ b/test/event.go @@ -64,7 +64,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() From 19a9166eb0de86b643c17a3b96c770635468b4f5 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 11 May 2022 15:39:36 +0100 Subject: [PATCH 084/103] New documentation: https://matrix-org.github.io/dendrite/ --- .gitignore | 7 + README.md | 3 +- docs/CODE_STYLE.md | 60 ----- docs/CONTRIBUTING.md | 114 ++++++--- docs/DESIGN.md | 140 ----------- docs/FAQ.md | 73 +++--- docs/Gemfile | 5 + docs/Gemfile.lock | 283 +++++++++++++++++++++ docs/INSTALL.md | 298 ++--------------------- docs/PROFILING.md | 10 +- docs/WIRING-Current.md | 71 ------ docs/WIRING.md | 229 ----------------- docs/_config.yml | 19 ++ docs/_sass/custom/custom.scss | 3 + docs/administration.md | 10 + docs/administration/1_createusers.md | 53 ++++ docs/administration/2_registration.md | 53 ++++ docs/administration/3_presence.md | 39 +++ docs/administration/4_adminapi.md | 25 ++ docs/development.md | 10 + docs/index.md | 24 ++ docs/installation.md | 10 + docs/installation/1_planning.md | 110 +++++++++ docs/installation/2_domainname.md | 93 +++++++ docs/installation/3_database.md | 106 ++++++++ docs/installation/4_signingkey.md | 79 ++++++ docs/installation/5_install_monolith.md | 21 ++ docs/installation/6_install_polylith.md | 33 +++ docs/installation/7_configuration.md | 145 +++++++++++ docs/installation/8_starting_monolith.md | 41 ++++ docs/installation/9_starting_polylith.md | 73 ++++++ docs/{ => other}/p2p.md | 49 ++-- docs/other/peeking.md | 33 +++ docs/peeking.md | 26 -- docs/serverkeyformat.md | 29 --- docs/sytest.md | 10 +- docs/tracing/jaeger.png | Bin 264127 -> 0 bytes docs/tracing/opentracing.md | 18 +- docs/tracing/setup.md | 22 +- 39 files changed, 1483 insertions(+), 944 deletions(-) delete mode 100644 docs/CODE_STYLE.md delete mode 100644 docs/DESIGN.md create mode 100644 docs/Gemfile create mode 100644 docs/Gemfile.lock delete mode 100644 docs/WIRING-Current.md delete mode 100644 docs/WIRING.md create mode 100644 docs/_config.yml create mode 100644 docs/_sass/custom/custom.scss create mode 100644 docs/administration.md create mode 100644 docs/administration/1_createusers.md create mode 100644 docs/administration/2_registration.md create mode 100644 docs/administration/3_presence.md create mode 100644 docs/administration/4_adminapi.md create mode 100644 docs/development.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/installation/1_planning.md create mode 100644 docs/installation/2_domainname.md create mode 100644 docs/installation/3_database.md create mode 100644 docs/installation/4_signingkey.md create mode 100644 docs/installation/5_install_monolith.md create mode 100644 docs/installation/6_install_polylith.md create mode 100644 docs/installation/7_configuration.md create mode 100644 docs/installation/8_starting_monolith.md create mode 100644 docs/installation/9_starting_polylith.md rename docs/{ => other}/p2p.md (71%) create mode 100644 docs/other/peeking.md delete mode 100644 docs/peeking.md delete mode 100644 docs/serverkeyformat.md delete mode 100644 docs/tracing/jaeger.png 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/README.md b/README.md index 2a8c36508..9c38dee90 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. @@ -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](docs/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: 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 47eaecf0f..571726d61 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,54 +36,57 @@ 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/) + +* [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). -### Is it possible to prevent communication with the outside world? +## Is it possible to prevent communication with the outside world? -Yes, you can do this by disabling federation - set `disable_federation` to `true` in the `global` section of the Dendrite configuration file. +Yes, you can do this by disabling federation - set `disable_federation` to `true` in the `global` section of the Dendrite configuration file. -### Should I use PostgreSQL or SQLite for my databases? +## Should I use PostgreSQL or SQLite for my databases? -Please use PostgreSQL wherever possible, especially if you are planning to run a homeserver that caters to more than a couple of users. +Please use PostgreSQL wherever possible, especially if you are planning to run a homeserver that caters to more than a couple of users. -### Dendrite is using a lot of CPU +## Dendrite is using a lot of CPU -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. +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. -### Dendrite is using a lot of RAM +## Dendrite is using a lot of RAM -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. +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 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. - -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? +## What is being reported when enabling anonymous stats? If anonymous stats reporting is enabled, the following data is send to the defined endpoint. @@ -116,4 +127,4 @@ If anonymous stats reporting is enabled, the following data is send to the defin "uptime_seconds": 30, "version": "0.8.2" } -``` \ No newline at end of file +``` 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/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..d77af87a8 --- /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](INSTALL.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..868aba6ec --- /dev/null +++ b/docs/installation/7_configuration.md @@ -0,0 +1,145 @@ +--- +title: Populate the configuration +parent: Installation +nav_order: 7 +permalink: /installation/configuration +--- + +# Populate the configuration + +The configuration file is used to configure Dendrite. A sample configuration file, +called [`dendrite-config.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-config.yaml), +is present in the top level of the Dendrite repository. + +You will need to duplicate this file, 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 8b1e61feb66ea4c58fdad1b1a881ef12817bfae7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 264127 zcmeFYcT^MY);^3B5vBMbih>jiO*$w@M^q3(QBXQWKzi>G2t`y>1f=&SO{CY*5VPkP;w3fP|J1_$Hq7mi2vWz0dEQ_pfiQU)BmUOfvV(+fj zmWqmsN%PSIePBbNqM{i*a~e3(*8^LKJETeS1U{l`M*fUnq zIyyFS+|<;A^lWoyrVpuKf4IQT&XmhYI7yT>I`wHyyy;6Lg6E|2lQ^3YEfw$TdHb^$ zUk1O;plXkzlL|aV_3iOzSeibsftBR9(_WW(zezv6YZjeS;!(umz5&zxfKi*KqDuRL zLDrrcbPI75yiV00?=6?!R39AJ22O%TNy)y;pKlYpw0-&j z%>M3z0dEm?*pqX7KPAsQ-*?IF)_JQx^?dB1wOemkwE30F_vlO34Uahf`n23NRJ%C8 z%@JgRu;vAqJrJ4y?%?q-{H~Q{7@DU|@|*8!v zZs~HdSf2TGiv1P!rN>{B8TiAm@;YvO!M$u7xiKHcvdGK%Hmp=#TH;1w=!3I447q2H z=;F=@p2oZ;#J*R(HWJD)@+GH&w=}e_DpXx8fYwv_Ep^})tR^(sXV6S4;LM$PTr{d>bJobZUQ1|NV$hlJ=xuh1fr`Z~RXAiQ_v%JiP z=O&lFGfPsyq8H^-JB;q2#-B_J3Vwgfs_l8?SJY73fZ~n%@(GO6;BsDm{;VnD2AYTo zd1ilYJs(m3?UFUADq;M|`fb1SrSu}}XU4*^I+;th<~ShZZEsGue&6p(I2U*meYoHp z{N+}ON2w>)hxGXh4c(^_x-U#WvTkPZr@V+dXMFYG;`!|&=2aa>q5QX^#5gur(U;m?k5{7l=Ou!8U(?OvEW z4Oe5nb{Jh%I9kMLj=42l<;-cT=WK83-tk`44)eX#xhq0%(9S5qo){)o#aMP4+sE%BWqmF+{#ydXQ;0%#!xrqcdDqy!>zZtRowFH_vb~Jox(2 z;U>4{L#dB058eyLUFlXVU^EFYyDxKNw7atV)S{plhvR)W-u;idi+nq$zCY%03QN!5 zTk%MY_8Jtt{TElPU4Fu^>psfOwB@R$gBA{45H_VK%X?G5>>XH6ONf4WXJS=xnRCIq z+L$Bx_0MV(V3BV-G56OG)*1$bGkmYSzRG+J`Rdu2A%DjG?StzHE2@O>7RjX4 z`HR%dihfIgWhW;9Qs87GjWfJA0=_e4G zFx;*0CE1kOnh=~C9CwsD4f<6${^Uron(dk`hpmvUaABNR_Rzi6!nG${pHGcEaANiq ztAA+mnWTb+0FD>xJvFb>dDcsJxZ&RSzHj6}Htx?E0`= ziLHx04 zUFJR0$kv{t=P%s8NwQuMmHKk0*?}+?@Yb`cy1Q{C0JeQ`frVcrK_bCCVTpg2-(TC) zz57=-cOyRzJe;yQSyZ_1kUR4fi#qW=UfuVG^+tDQT5=RP1JL zJL=9fbXObOdX9e@j<>qvAXIH`8~ANU2>J25)l&5>W6?4IG!iA%DrG}-J&h*%u zSZnOen7_+s8J7%^jAt@^GBXXy4f(G2<74Bqb@}7f8{hn#*L5f1_5M|Q)h8W@+2eN+ z)ieWn-FZLe8*a=a%x>( zQ+O(0=d^OiC|$U#ez5+>?ySUe)r!^k&b3G-9ybL=*MpsTfyhulycg1YYR%92)$!XW zH{NU9jtN-ZTo?1G^l4kaw|{0ob>r9E_SfO>28&I<@XhR<&UGjj%RbATR%q+eZ2fGO za)a^}Wh}I!nrYl~{rkEBf);%qJ%ONyqDg_Iiw7IKq$%{M3M#btBq5IZ#={%1nBeoz zx!PIAuMTo|pY1(!Bvhr;ed{gH(~CYT%LLC(GPW!0 zr|a48YB$tC_su#9o$+7CUe7SWmSI$_j=ZKJwG;@PxH-d zjcHmgK3I0?3C!TkXiYUqt9sjyKO7T*;1)=GwF{G%sysUM7!MfwOv`WlD_3thO zp9#n`1tLDv_@Pm|d3|jscS*r;rCiW-`}xvJl5eSTsg@DkQpt07)oWl!$N95!qVuHi zCt;4U$qI{xy#@L0X5pl3NiC?1a&X=8#_9PtQJfL7V{TIDrwyx}dht0G?lI%$Mz-K* zh}VsU9?1OipJsZ;y~jWhlPPI2mUPBMRx zADmu6H=gWUwYjS}_*U(FoX;zaEQ!SMY%1vc6clTYjriUstwb!p>x0EaEBQ6qYJ;%b zpBIXCU*+8Oi}&8?E(tTB*CS;qd%S@b9hdD-G<#cO_D<$^yN(TUW6gMF45!$ErGpFS zgx`Vn;P?C#HSw&*-j?7!t=^=DQca^uFsN-T#3F$FGrE;i-0jlrI{XesX5Md=Yer$o zzi-o!yh^}q`r-EO@2lamT5>#2#%MYs>{%|dsQC4UJ_vn!Jig<(dtqOIUZGP#T#;+P zZXs>m#NFs87!q0@x_NK}Yl}EhMkzJ4EWAE?h(>;|)V6dD-8gJ+5~#pzW*o`F-9hNI z)~W4m^6_G1B1>#gaqvxO*})>AWZQcLGX*__LZuLy=uo!y=l5%Cr~;a}s35nfKAhV+ zlF)@>u5^dGq>9P<_dVSWBoXDW1+yXEWj6 zbQ0re-9A6Ny9<}14;wktE)WO?h z61j=bV$gf47sCY(8cx`sr@ymTqd!;z%8vK8#+r6II#joS{WDawr#PtSfW1?|Tlo~{ zf9|WF5~QN}bDWxrD$c3o|Ktat-lUEI9o1C*}+`G!2OPdO}po%hdI ze4Lf88|ysgz3=X2%X>%cwwT0q<+HrJyoz2g?Bw+ysQ>eH;6J774n95}^5WwD{{CYA z(qisj_TrLqa&qDlQsPolqQEyqy#w8RtO7*cyl?ztkpCFxfvvZVm!pS|qq`d~Wn3$3 zcV8c+>(?n4`p?%t?$b8F@xQL*=Kasd0v=GD@`<>ln1uL$jt!iuNI5G1*fGG?#rT1v zD==rkHI$|A$Vw^xIpP2F(SKd?AE!R^w)ML2?h2ggqx@g9{?C*D`-lJU6aQS(}KZ&UG)dH!=0m}up*isJuiH084m8PE>Ej#nKY=sg9tfR|BT zr&fSJg8$e8`=>CK=k0uQsHjw_G#}i18gOcThN0ER*Ft5B=6$U2OIpQ}GlCA6O;Sq| z)6Us#a>Ty+6#lU#?}=Ly=kJ$7*MG-z@`^qBTu_)N2m;0xK>^Gq&mg3oI3f^P;a^B0C^CY=8SHIanOwMhNQ`EF)&hx5bGyL0p5VMO-KlWIGgX;${|k%q4%+kl`=&faUCPTl2s_(t zckkcwr+?Ujmbgyy@B5_c^NX~!M8i`PBL53>hNyD*6_02Hn50sQ&4Y z=g+UyvYa=jr_PXdcVm`c$+c@E?>XQ+;Uw0wUbDP|6nBr*jD^Mq z_S(E*#xecN-fRp7*1@Q(tOm8gUxG?_*OrZjKDU+){%+Ze&n{d8*6Pu@o8$h!9QsdP^SGy!+XFu z#zm^Dg}5X9fIBxnwM)eNmimob-nTjb zIGILxCG{phNs@*98!_}R~|WK0jT z{i|~>p#u!!=#+jb{241SE}ug!#cwjtogwrco~}Fi7tyIq0T|5}rdd9y4y@{_GiDR= zfZd*Z0Y`r_>HJyB|H=&xvjGoPROI-VF&{9(vzIcIIqYo$5GT`JHR@kn{r`93{_n*7 zEl&LZ$m2MO0xE}^_ywMmMy{=96nt~&M(@OV3eYE^})@c+;iE-E#LC7a;WZT{JTPm549%G+eO{-LuaS zHEXzy$?~_v$HfM}$b7=|e-IlT=(M6|VTj%Gf7`b({t*d>NU=t%<%zOlnE@!MEPU)w<>yxOHR(CLI&c_M%J-bCmLnG$&W zhJTAUl}*rx5b}BIJ7*TS_l>;{Mmgqsge$Mo3Uy`KH~jLOa+Te5z`7mq9B)PTJu|Wx ztw2S)2QScJQwD65Q=sLq6l??+Cj{yw!NRYX0ob0A;cq`%;oG0GH|5^8%pNk&o<23R z>DPC1j60D5AN^*Z!sqG*|Gv-{N=Aj2&rRF=59vYI3^G&>6I2}8w_aV)b%>E$PMxqA zXR3SAwjb1n+AM{&E{SGv`HkltZu+-PWeY&pZFroIe~TYm(`Xt$qt@{r2%4>J>g3uS z4B_9K_8wS{2Z=czujU+=$YCI%JW zf)GJfv~0@nc`eB%WXuwZHUjj5CWq7gTNk)&^g7S{Ey}kO{}MEbD*H8Ip96ofVf8hb zhd3B%8n&n;g%}h6$SIyeoEV``N}oK=gKItCrCYu==#NoAHyxI?5lanMU!D_l*n!Dd zgsYHhRh*fX2!no~@-JDb?8L>o#Hbt}_+`e19xe2B0`pIIZg;~yG-zudNRXrgHh=EW z843@kT*nRB3EehKcC6M2M|}4&+=N4NuK0^7Y}6T;87plDU|kO~B`kqVVZ&)=x5D4G zB9x3}G2H$gCCt-)gR87e07#eEd6r(jxOHJ>?tK-r#%C@sCg^d?_d=ixEXQ*!>Gmq5iH$WlakSFS^583b1?pK=)Gp+Q zboMXBz3N!5R~p1i#37KlYvkcbvGVAC1L{l}XZTTSV2@Do?ZZ4bV^E{`sqv$6IpOl@ zZ%dr$gCB1gIJrH){nZDu+==ht5BPS2TT&W6k)#T~<yLIy4`TTPe z>XHd9vZ`a#JU)c9VRXC>H5Ui6?a%kluV80hx8A)^O%0W5*C3oQbR$U z3$tcHc78@2(&E^6shIsB<76NH2BKmWvY*sU?Cc2pU>B;2Rt=9RFl>)&j^ zIft~bsIynbr}Y5|?bBoP{a9*fZS}iY0AH3pV}qujTUE5~^ekK8AVxv~ps#`W z1CA6}2O@;;=}!2{Lvbg|-7iK=$9u2ZA?F{(2Ir8bG3i*~tc|tBOd+Mu+0ea-K00>u z!99my3KGd3ZFNp%|B@Pv1%QE-(|$of?Kb} zgRM%LPjdr^Ez5Iq&=o1R0bwMRKh1ijiXNQl=9Iq9J1BdD)7tRT{akm$hHt--*9KvY zdb1Z-%qAV~L>{aewM`yjLXYhgkl>^5U>Cn>&#uu-vU#5~U=UaLps8}*-^lm;joSk! zM+4_xLK&{j{JsPej0l+K1& z3ev%cNnl6TK>$G5!-H1!Mns7t8vjJ8Hu6-Ph0{m_tbnc8?#d92>U+5I zZi(KVlY`ik@tBNn1OTC@GW}-*UB^GUzZCj-Yj_-3dKn;bc%A0u<+@>AEN3fJw9|dE zt|SXh!Q_VhQX1yFxuV@Vm7J7?kwN%a|MbF&nU;ghYz|BnO$p}aA%{zULUThHs_l4p zD*MCET`iC}2b#R;pYbYD|5e7eTRz?P z?>B0YQf*_rkCEE~O0$JspWRAP3Z0D3lr??2tM@7xc-j`L2(d%r=Ws;X{iETY8uC`8 zipx;OW6J^J^tzp_nM9jQXPNhGEObIPxi-}HSZ)*O-G&-V)b{g8_8_hdi`Zw@xRRz7 z@|nrr9N`G5Z3;tTf!Nuf!G-MaDSSTK@FWAJoQ^x^pJ2=S8-(1SD0lNM&AXtoUe%eN z0$;b?Ac1yo`D}I2%fg!aT+=Q5CJM>f-GSJkS$sAh%`>?%3Oc(*M6_%+JL4%Ot`;|7B#MH&9?0&F1N}L_XloNDNyaw z5xv|p0&fZ?<9@8CtX^?04?>t$eZsE9%EDK5HV|>odiJf?*=zel;-D3E?5%Tjea@En z^c~F0F7q2R+cEwwG)r37mZNnRJ_mw+XW$pGR^ zMjuAy5m8B{-jR0gwTawkY@@8L2# zEq#7H+>nAUOXrl(J4gQX*-0a9ppQ59-~f_D^-m_umtfV^hfoDKI9~M*MtVwim0NXX(rOG(J#RRnAYol;g3SPc`=~wh zIk|yAU6LuH6qI`#C3skksElrbV;#S_jNlGDlO?+v)_ELl%|`H|1Kzc-rm6B8x`U82 z$uZ>*$0&rv|CE&UMzjNXLA+=2$l^v^diYSDDjLFgj!?*d?aq>H+hN}n@%6}gf9xqz zQ3|TTWn!qH|9bt~k}h4P;V$L9>bza;gpt!93?d|40yUt}5mm6$mVSQ&C*}r)m|Z=b7R2C6HF~ zKDv#`babH@));K41$uG_q%_mVX}@Pgf|T!f#?Ui(7u=N;1t-H=W;P}s<}!n}rTVnQlf z@9ZrAXhdtGwe8|!ZS*oX&9UK0!!yS;H(ibkW!MInwsoGU-P6W6ftl?K??22`J%6rt z29pyS(o#u?Zb65TL^;lE#26fB+X3pD0X8H1P~8^thCBMw*_6OQ^OHZZ^M~&v@%g4# zl9!xU4@E@L^bDtc%&0lUp{t;&kLLSu+TbDQNlIS4X|{o0g|EE55p$U~r-cT-568K3 z1F_y8?A8Wjm3f4e!!ihDO02xQRS92vR_SC(_{6^CI3<*}F9*R{cXT{*zln!j{|b_qyIeyzU5$&TSTnWU?Ux#j+uEkF-4L=DSDJU>(OrF0M5s6OGt&@){O*WBr2Mi* ziby_C8p#)=%nAeWqQR1NZ?A;FpOuU~$xPwj0?HY9DO40=xVjrK9{rNlPYexcOKMe$ zmm<3p^ppcT0$oTy^iF;z*-Xpg)u;OmNk1i}?=h;H_wf&R>C1$>QXB6_dcU+wJu{r69h~y*PHH|lPB>r;r zj#H}NtOm{ z^ZEs*?<=t=EJo;~4lZ;iLjP=1Sb~;PnJTzkvSk2sZ7?Nl6r;S~)PSk^esh?{Qq9(vs-CgUc>D`v!nXd(1e3++ zf*%dk_p#`(=4J7`)*-DqS0L`QjqKBH^t7_t_3rGx{)m)-#`@;EmjvESO|~8;yiZI% zZ>r|uC{(65zyTE%;3bT+ifwGFmIRj%D1pEe=^m{nxn6OvG-Z{HZ44`H>#8AWQ=#^IP z-4E3k>|B&YU04d{oo|(wNZ9u(E}A~&P`y{~4lQMRF|shmehejmH zb{vXEFPu{(O$@ksxoCkd%XPts-cmhm7y57Im22ZDWvyX8AFrX?L7a1*`NGYxB=29e zgygj>g}mWu#+xKeV>m(+VYG&O6CC4R41^PId+&vK@X0P17t{izvx|)eCzg^vbJ?=2 zb^|(zL#$!Fv2DA!+npiEpIZwd+?2YY;U4mXLKzvTM)WuhDZ;VSvB+SGY0k)Z(Pum=>-?yMG54F9D1YxG$em#u@Psln9MN1+#9bLOn-a=V zER&x-COEK=CjVoxkSM>tnxVeKR^qQ?(Px`tF(%vH40WBMB{qjQ2R9DwI-H#_c%DRK zzB($z*|e!6Y$p5KYW3NtI<|wtj6ni-eAQrHB)7aEM7MhNKEO{&IZS=G-5k%M9;##O zu%++q5Qg7h)Ljr-E;W-KOySz9uY_*!dL;1n0qD3X!<40PWPc2FnhYo*m0Ynb{?iu|MuDsYfMt0`&J>olJFul z#@F#M-n`5M#^vxXl%IiMOL_sX*}#Ki>R0)03ZxbmMlCR=?ANX{h$fT8!+&Jn9u+-i zXiiFoO6T=6W{{<=US-l2`l3`Gjxm%bCwtoDEcFDXxvf6)kel+G3`al$BVBPdV%(@4 zfbt?wV#YR9Z;HJU; z84E3UjVrQU%dCUC?TbL&<{VU~sTZ(%MM&vciXr4!C=`_!-I{Fkte+i3rGgGM$u=)5 zL}8_J%IwwD)4(B&-AxPiFv+CprjZSwHDGaV_an@tcek2H2#Q@SCqH#%! z>^iy7edm5ZX4s4QBa|OagLIw1dC%xqM0a(} zYI58sYHTH7>smgJ@UVLeUXC}onLrSDsTykbhZ8!@m+#)Y2H<;*O^svrWl zqri&B>U#Vax_5(PtmMFAqL{A}GQ~L|#78-C)2#v|tx15z|G8|%5@ehuolR_-D%*wz`#} zj&goQ_OLntX#gi`xt2GZP{}lGYl63NQ>$8n#W3bviWn~}ZV$#S!V)v3i$%w7m?^Yh zKUr+4J8Hk$cDHE1KUkn}i6l5xTN#ZH?A>=FTV!~Em0?Ynx{3V8CLxS{O@VG}PfY=I zbUp;8`a6Tda*(vrR5G_7K4hXOFx!ylT+hBwOns9fa`(~d=xvn3p`{)Yr@usW9om3T zQ;G)9c%F?~#DWWQa=7{qxD-cCTTvN4ho6d$*jQlDX&QzFL)!kr90PS7djq}xD1n_GF zne+_gYPV*j@14wn9AQjKZ>m|F0RZ=~*wab6=STx8Vl~RRO0n&%md5jmd**`P&Byas3Iyt^%qQTxGE-nk+V z!{k(xTKbElhV0iDOJ*ABQvjrHs-O42W0ZA9L~ujI$N zdF>PH+^#vFb!1W8YNu0juA_62Gh@@Kxw(e@L>SxQ5aUP_5AXP7Bd> zFbK#PgSk%1zB!WKPz8@&Bl$QU9uLdJ09fP$^KFjN8!W0^QsD23W=qZqTuy^V)KyXl z=7^S=o93DAYBTvJ{U#UGZ=oG24IO$+eUr21s+3aK!G|gM^8u!7T4zXs13{K}3c*t) z;_Gk&Q^s(Xusdg-lfpk9cj$NDO0GZY+z;9tUVkNzF%b&cO9^(*V6?`w00+Tn*~3CV zCUv2n$4r$0;l2MO2MVnnd4JtY4QGt z;lcwKa)Wvdvg3=-P2NNcr#&XmeDp9?w%Og+Up}11DEmtc5L3aHWuY1?L2Zn%aT~5_ zP~(wN$db|N2#mV;(&fi5Ln35Di7!S5Z2X~XXv3b{SPvh|(pUxGTyYgUOu!JC8ClJX zvUHq5hpbVVl3Sj>DUWqD!E8q7N%a;?1;yhIo}^G9LOS*dr532W{Xf$P=Tl|FN9YUs zm3{k~N+8tcg@y|h?47gg;#&a-Q=snKoolLwxT%}NJ~Fwn3dJ;M5# zOE{9%oq|Bc=S!9zMcd8j8>Mi*Deg=gS_5c^IR2k1(WV(smX-Q#=!u?cs+#*LVI9_1Rcua0yi%fIttF78l2SpHjHuG>n@h7O zR`u=<1yQT1ox@%#RPT(bX}gYK))7G^-XG%NiiEl)S|aO+uoAVX{p_wHR>B4z zS}4(2`B(;zoVmr?pb1LlMJCwzsL?q#(WpakE6x)qc)3R)7GXZZkgUsoy(2wQicGA{ zv=xK9`C>M3y8=Kbfb5zs)(<8Qr8KZ7bQWbx=u$gVtO4UFVCGxAdh0cL#t`fg%h}U& zXa%9sLLS;ejk4tHB+KZl2-I2G#lx&_x^MG>^0N26*0seA9OVr8A0!z9(8{QJ27scV zvdpfle&beVbXCM3+ml1ozIxA_atCyj`5l|8#4uJfEh3}C<)N)VXI|Fu6ppeVH>KU* z55Cdax|39S^_5}dZg6s7ih}xR_~%XE$JAvEHGB2)E=eD)A*Zg^GquE&p~NvdGqb0W z1n&)_e%T@6AawP$&gIrBf^M>eL4Q*HD#K#4N>}&yerIpjc%Btqpl~g>%fy$k_JSVE zg)a?x;p48_4A+RiXkQhuLZ2~1&H#1FVg+WQo3j*DtuhZ z9C9QEf3keFbxOhSid%ITA;7G^eE0_VPz-r@m+RM1scQ)5YQqzi*j-Q2YyAYVaTKen znCxcveh7olSU7?hIRxQ2)Zw3!7e&#o?pD@m(m)ejaGGrkwamMo{>CdFLp&QA6spj+ za_8^b68M{sBYXAfLr+NP(5eHCtDX<=@hPnZWCdSbA$wZ~6-v1Tdk#;mGW|RfT(BI7 zU+&vj@y)Vy8V51yKo{2h+ShVMa+8Sf2+xgbL=whMzqph?!uvF;rmVQ$S9VaghZf5z zfC}%{T$9zeNsj3mVzordY?fXCZP>19Yel~B&FU#QEPmoFtuDF7$v^SN$1V;kLh`># zGOf8$!gNGw*86=)P9KgQ8v|CssSm+}!_vlI)mA$y*^{6!Q_1$UT)P{$WJHS*_10ri;6Gj zbsbw5Kil21(R+o#h)RDrwmxF)UIUl^bTyNGk|U5vDE9%90Mp~?HBX?RZ?=bRei*tB z5<0RFX&mPg4@Cj2!eq*C+-B9`ONISu@2M25B_q~yts-a!DmNpWnxIdkwu2BBVlF;#8FQ?0!T^zf^|sv{pZC83a25V638ySp-9H{0gb%WW8p zh6VdpMl#UYHA6-7Uen@-cDQcqvSBd%l=Jc*;3u-I{i1$6N1Jf68`e*(;G+8Cr zDZtpsnCa?Q(^&3oDHq6@RjT{I2%Bj9!r4Z<`()ydryIa|@{2wjnhHRdgLPQ6Y zWi*ld{;SuHH1#0*3GF&yB-0ZwXks$7dHTv{#1^h)TjNb-sQG2T`*E_Z^Gq8U(N6J8 zfYpBWRQZ;=vYPfGl*^(aTqc#^{yx^{iRsZo<$vuHMK!(!zFo?7s zgT?)#^|FXZ(`^Dxhz5TyfWY5?Xsj!QjCOUo9n>Ai)gZY+)Bb8?--jNlYG=8S ztuaFK@Nep=5#KdFIj-XfSMC}sY-7 zhG6yc$Ll}4f6C;n6c8CNS1~SAu10hOCo9}ZaP%$jr7(kj@q=0Z$NhcDpmJY-3-ywF zD;djoE2A&0L_w2FMy!j(Zezht6QJBmtgIVZ9h$!aG}S9L$v>--E!jc#R)A$I+OEo; zBnC#C+Dp4MsRt_E`#z#mc;S-^$tUp%%%?7aSdwbrb}LJ9wA1Ak277BTh2;q|^F+z; zg2wy2$a;X=w1y8!k5|pNRHob}zyS>gzeP+@Vd-%2QvW(y<9j#{*|nYq1*sDW3E*0 zxmUSKkZM~uTc~rx#et#~fRtJ-rs=-APD?VFYtjPk+$!UoVzWYN={5+g z3}MYp;^o=mqv8o7x7|Rw`2@y9&kKZ~blMO}oh^v5naH4#m{`lg^73D4A*O1tBe@|C z{vRKnRM0>x1N)rQUwtN;&LSULrK^H-26rq=J2{%DHaBAzbn9ssq%CEi8|Vq2YGi#e zfL+qaCVp*eQAyqgSjdWpJ78F0aVcvuC$OL@W1AN+xCj>IM$P9EqHx`1`HH$FOO2SC zkTi`Ajz6C!;yM$lwqM(MpUYCb?;^+y8#kFHQ2XKA?dOt{e$RSfQDKjtpmf;-Diub64^? zFssvwLR~M1yhKXPDg=gWT^t!~mp9FQ&H6E()eELd8$^IE#BNI4HcsYqW@g+3Lh4>N zK|vX7^k=;Q19F*M>KYSt_#Yc$fG%8*y5xqYj+Ns8U;uU>(*zU%_2c(+_JxWwhZM=H zdg2i4njO`fE;3ZT-Aa$0lp4-uDki+6%LQ3MvB9hls<@S#qsp+&oD< z{9tN74AWj{O*S1FDJ^KYwHvfrCmiZ-ox&eei(05_WUy8(Ev0@R?N5*Guu`j;>DPnwg zKYeJ4VWkmxfYUc_wCW|SaCx5mni|ixVs=Y0k;ccd5L4ydFIUXuu(T^*VDzSx2fJm! zHOX;l1=*#-H+1&B>%6kzG!_R6HxY#=Bq*=iJl5e2Nlt0toKP zU3mS9b3N%3jU~wru2<}zFjVOK12mAtmz#tufATds1?ZOZzWBui{19DaHYYT^rY$k* z^C38Y0vwUaiCDK4H#^TLgmKx;diKL4fl!n3a<7h-^+mX^`3aCdf9`Dwd&%@2$S+L@W|L(?!f| zI|`8Od3{m`=#J5lJ_i8)tc_=Tta8*g?K z%}>-H|FF0xDSdV=6I1W+2-$CWDqn+OD@P7Xfnjd#X_r_^f#-aDxP}xq=m;;d6gG~0UDE+7)|MkT1B=URHHn= zj{=O%22xXorltS|N88?ywWbb;DHy8#rwr_8bS--$m4Z0>STC_FBhJ^)~Pde z37aQ=s`k}IQ2C%fUg(Y|!0QyGe7g~I_fyu)YH`kN!Elw;hNycWD1n=5cf-j4E9( zU~mmy;6ytZAXi`CT3vi4OIaLeF+;UlrbAyS3@RzOI#i#QI57~?P=aekJgq$M>rp8- zRvKk&a5b4_RbTX4G7G>0Y&_rXZ~-J!?<*)8C)jWwF^Q(#8fn1(B4yD}DcE9Kc+dzb za&kyvdDeR|5~4GOZm=0YxUfPQri#)~)!DrKF?lv}sBo<+c{2#~87t=Bgv>4e1Xz!o zOJ6(7avEqSZ|Ev)0LrngyIQ%p_retpv8S#`c7-=n-4Rek1C0Nqqu8ZKWhfP7>elq< za%O_4U8d!2t#D7q8pT3ePFo9nYC;@s$Qn0!$kszwx#$-_8IiuSBdT(-GZy|c^&7wM zDm}Bc*g+p+a#U8-nwbfsBosxH@nZVP)E|j3un4>q4k(IS{sr;TA>l=9B?sTG96z9g z+&7f8x7?Qjlds10qtRFppeJ-~ut&ts0s>(cyX>eOpq1O0E%ps0tTdn07O7NZwoRaZ z2JW^UzZ$DdkT((`9al)wlgEgR|OZxz_v^rR`fX9nvPog%Z=;Nv11&;559oFy#+s7`OowoLT>Fr6NuK>%qG=Leu2 zq(6Y@;tpb*#VTuttx*zWl9vY?MMZNiWB*-guEB#7&8p1q4i)(9vE}o z=B`8*FW+ssvcif&A9JAG2G|u_%lt7rMzab}OLU#1!PFJgqoJmUiV3A!P$#DY{rvj4 zDQ!BUes|K~eKj$LZ$4%-oR;r7z5Pjb0HDDsB$n)D#k{&^`}aFP=z+f4y<3fdKmHu` znK7nzwQVir##`xb@rf${)o+XIRrT6d@u2fykEtdL~{chy-!~#Jg`P1OKiX&+Q2KENT zu66cnI-C&UoLyEyWSH+k>84zTLLBX{wE;6~$HJ9Xp!xF+(6BcBe3?BkB&|-(?u)ax zB?7i=!xv7WqSPTJ-6?*P)<9ZvG0*JtRE*Dtijb;kS4C##%sjKdpu6Z{Dl|7Zyf8A& z3Z8JhA9~^+M4iMi3y2pTD8Hzq8}65XVKJotG&3Rgpx8F>hmxa<#`524{6Bq9^9BbN5e! z$B@H}1U_ScD>O;nF)EHBiq?;9GMA)VJ0_}vA?l*CXIJD=7%e0NL9W5?Az9~dWSq4k zALZlzDC(~Bt2T`5_=ZY9ecN)fx9nNKo}awB(G zFSJo)H2Q7vhA0t$##tIU{Q?4zB`+Gko34%s%U0v0KPxgN+DCQEe^~>>4A@;Dn*rRlP5GeUJ%dgGH65^ljYdE40lfxX@ zfxv$6$wLC!fc@1H`RbxqRcQuAs*#1MW=XEUx0egFD`E_Xd3+X`+7o5RfpA(U3c}!p&pga{?qG2rkk2 zpI1p<@u^F2(hP34WqRiSVedVoqS}@rAVGp8 zl`KdU5Xm_=2uRL3H#ujT)TB4tbKU#CbMHO-?DO-DHwJr;9?*8LHCN4=RbPGem21eJ zbFGs69RNiTcGfs_ZkAOY{ispLME0v1ZJB+Dwn{or^Rpf&@j@z5o-t2kZoS~}je=%V zvGs6WLJazep6dluA&I_s!ASEvFXR>7x4UgUc;l=fM$Ss9eMq73hWT(VDv6 z@p6kd;`-!>2*@JJL|GPUk^89F7sURJZlpb)=jEWS(l=iI47A{Qo|0|qp1$dcC=v;N z`72v&Mm2k)s=%!Shusp5ZZP@dBEP6<;D_+KXU7T8i&?ypTKvR(pn8WfI{?F`TX9|Z zC|zbVG&|@viVSGiVT&P0OCK2|3ds%Vd0Eqz&I$+-dN4fpmQH%Uqt&xql*uhcJ_400W zOtsI_3o5^Us$LzKoT|k;HlB2@JDu`1H~s8rV1b8$?Ht*wc)eZ4Ruj*SOyr#_<98P7 zgero9twYX6I*N$u_T?+lf(P^p=c6qmPU7qt?lU>=+j8NuV=~1Q>BeLZGY_vBJEmCv z5@Hs+ z_hQO4lGz#&3ine&&eX>%gfqdmm%rb3u(h1e)5_l1U<%6F^q496xG}>oGcVm8PcDZu z0KLEH=7~tF=cj1HGa}nIxT|ojhA}lGh|m{H$JtsAm(8Lt3_U^w)8S_@H%(=|HoVGq@w#e&N8t#B&`!Y9uBzDC# z$g<62iW7&wZjTJ?d2B-3FFsKPl(a#@1h*D!Ep^+V7@)aS-Sqp=T=>&qBz4R!hPuXVW=VayzdA%p3I!9kpb(`}DRd zK69(yOeDeBkE5dJdr)5aM*N$Mpk$M{7n!-Rn!LyrNj~~@anu$hK_lU}(Z94MF6fja zRG>d^sdJ;aQt`w}LU!3fbg?mFdX?E~-Mq!2)vQ;S&UE{%4#J}sYY|_+%o}T!S!_kC zT|8_YgyT&0>eg;AP_VVYw~iErFDl^7n*OD@h@qC){&@rE^Y^M$7CmQQwkPfLxYm{n z$qg^6>rShe>e2B#7cf{2O6<|ISkygtSb3pZZDM+1G6#+(tFz7p`XeH7Z`_skD3caR zjmfH=Q|0pWPj>4{Ce7%0o0;mi7Y{(zv(_V>F*|zaTIgTMZkQ`2@B(Xh&|ufId4s)$9HZ&54~|J77g zhi=`-Q4RS@hd8TuEvkq@DJ1wBjgg;#zA2`mv)Q(&2JL(rIOi%H;244VUJ|kOctAF0p0&ebzS6ak!aL8 z;A4!n$gTVD?zt&a^fOC{^q9vTfed|6B%_sUJ0^P7Ni@PSHkbUB+&1ZIVR4D|4EtoeR>-M*&i<% zwo3*p+0Yvs9=kZ;Q5+TgQPt0>csSlgiJNyuyz4l>7=))` zAUV5;e7Z`CYKtci1fpCQ`~*(Tr}ii#poJxfRt*^X{`lAJm{m6&ZEux@hrPTfPOYX4 zyh*K6ugv*d+BigCmM$NEL_44mUGMSmxzDeZquoHkUk-X%bYR7MaUF#$`8LxvuIw(* zdx<#R0+7hW*TtdxUt&*&=Z(>(sNO%pwt7{Qv#x3SrBvSyn{;6`o?Yf5y9c{p^))}+ zt(D-J@p3;3d*6@9ibRKENMygSW%sfJG{T|0fA9Qb;<_v;Xk}s7_Y`AdT{aja=`ZSu zv4}kTV!JkwcvvE2HNik~F@T3KZa(qssFco&--}}s^#i;Ay{21OJ3KJ<7VE6eAdR7ttS_#7A2mu&lW)&J?V=zYstQN zuWitjb+f69H`jWUco8q~wc$1N6M}WuUQP2mcb%wvzGBT4Sz0b-`PCb-=j5Ijv;OwX zMSY7jnm6XO_LH6TiFNDCPJ1CTK=^|3+T+DXZ#N+)E1`C+qK*VmjkiUDudR}*lNYNw ztP*;RJZEykU+@QXmzsu=O~J#foW-?)vl&ji;1D^LD|0-Iv45P6%hh(6xF9I9uSjl8 zwm+e4ov5mHI6-k_kN5GdBUGoG9poy;LjMlvy^dDd_hv1+zu!QnJS_pX=rxqBJ0vtm z7;Xla4ZDA7Bb>|D8#BUIi|8*)tX_$L_|elK{?5SPMYQj_3ii0|;%Fv*^Yr z)`P&LXg`y_7WsIn{QEDpmtb(zqFKxQ7?v7dtQE>2V506u#Kr*j&|)ctnZ5MIPc5_u ztBy2Mu2a!-BZ!5O5tYGc!%|d})8+&qcj?N{aSt#EUprKwzxVOMHzlA#^J4;KR1M)C z3-`g(W-DVczN&(VHf*pibaT~9&VMSuZO4iRO=E#x|7c?utc{CtK0X2)Ug)qcEYpZ( z{ZS`SF~R%AV@dUmCMFK&BmfL*k|a6>WRi>?V>g0ahRxoPGl#Y!c2Q1F`Bf#l_ys83 z9N4<%PY|KWjap6u6o&uf)3<55Fg#ez8$5<9`Xp`zNk%jjH~<@?w; zA)%V~?z1Bn?dfdlQ@0#cF`|0qjJQPh8M752n}x(XcAc#ksOjcwqY#c8c@0Z1 zZ?eWdeWgBV(wrx$En~omV|Q=xa8wEbPiaYjIpy0=ttnp@GV4g3OY>{UJtNtM_l?&2 zAB@*L>GsASjC@SX(OXg;pMYN+=58fCpJooP6a};HP1aI=biNj#LWeH@cKIRwT8I)GR%!6=shWI~F{s&kWSfo-og#45 zOoSYDJVKGZ2eh}GJv5`0qjyovP%m_5(XAM}4kJ&j9-C{6Ue8#-iM`5ScKb#6Ux4lG z*=UX~%&s37l%ro~oyym$npgg?=VSzl%)!E{EnVrA#_``xsHdNS2Gy*kAXQ=bA~-bi!>1-@ zS{j_ncuY`M%+#B?ONForXCGl1ZhG4Y!RbulS!^W@+qcXE9RUZ0;m{4A>doa^$O+0- zlt{Tda;TH#3tbG`oU838V+wyepE0Sp8SM#(Ro03kCktiTod~qpVUm@Ob;iSvANOWq zRmNJY$}G;tY{JQrM@G?p4ruhQ32FrNVY}^RE6feDPdQ~4Ah>e%IV`H@vAXy~xD-yj zmu?z8(!>nYmDT66WnTwLa9A9dzf7|LqFs9H9!dkD+)LQQ$85EV*8m_YZXNQZIa@L; zjr1&0i3gmapAz#kRWV!l;#MXC)@Pn)e1z6oV?S83zBcA8*I~!FbwU)Xq;x8QaR|z5WaFVlS}64Ceib-BUfQ=H2u=wXnLXj zJ!;js0!Bp~bH^*|X#I+@;)8%&sw2XJ4~OLR54&#GF8eXIr1jgYMC-eR#iwWfG#F^- z5IB{^HjOra&>CY!5p2MELB@OfbjqHkyUh;fPz5(aaY0SLv~xS8YI-HnzWQ9SS`d9M zRO`3se$QacpqorIDc1wTemP_#*pZEEz{P8n(+%nfU_rHJL18pn$e=S}?1wkqEr~#T zpu9HahPDqyyhCD?Zsz*-H;DiKd4$6>==g$ahbHzM0sOw|&?EmMWVxD-*Q0e;$k|f{ z^f^xtC#pvWd7y~ljh?{=4_^3p8HJ7XxHNN(ldBEFOZiRc_+o?4tao6rh-cP^9}ZS5 zfS`%jFooMn%BD7yqyswdV&&nDI6DNg4W+dc&v+9Ovlh>ogojOHtClBV{9yNDQhV4= z#A}{=HYlNJ+sQFG$lA$8h7ob|sq;eU?zk7{wjZ(|$QCS@o;sgOAM~I&3_@$l;ji^% zdK@bSpMwUek88QX*V8 zG@Iu^VtO(kYr$+~46)aZp(fd%*Mf&y6eT5qYN%C(W0sh4*lgwPhpeJSPVVI_+lw5h zuhHIBcITHvVO;hiuBd7sst0ym8KcLcHj)~S2Z+HxDjYxL;#N%lD&NTlvZjn7pU@dk zYu26Vew>$wQG=dW_o#Azkzr8*`nw|SkL2j0H)E4uYKaI|x=NLe*o%atkhpQ94xr4@ zugG?}Ajm1|xm;bc-LgU9Py{=HGL57{a}Qg0jw;INC;EvdJ3T+F$0V$q>P|(^8~W59 zoS^z@b~>h}jdeRt!r11Q>>`cj3xtwFb>fHM;{497&az?4+GUpJXJB)WyD#Oql{CkPx47Gz!s9XzSza9qn)2`E_4 ztHyhx^itiPzfr)d{cV<9+hwB{pxg3kz&O1JOTH!=YbeP1Yg=21#Yt01gD$avZIN*4 z*Mmr(pe#9{v63W$cyfeY2IMi$m9+zuN1iS%9cCHJnd)ZUqEvI>2i(wM+Jgveiwqv_ zNAs29_G@xQxLLeffGp8W&3jprFdmXuU6*;E-2wDil=e@&B+A8BTj<_g2Hg%mf@vd- zPDz;zT&!7x>F-qmw_^LIK5cRY+PdA+1aI!1*3@ZJfzL3frO1cXP0JQaXUpKv?O#Sa zPP0#9jb#`ZCulI!AcZPov1rZd*D2K<_<8$lfO-nA!oW@KXj^^Q&Kesa^+oI!UqvV<%o*PSOssWo3?`7 z(=td<4Ato+{zshl1XJ1)?FO04%cy7VZjKj zR}-{Nk-GQxU&cIh?pBX3W-W#<2PAoHrlc=f7zhbdH$)Ib0bM-)+LV zRY8^@V}br8SDdsAFDtu^^J|*_;JZxpDCPAc&h1ayvH^G{qN)!f2!u4v48_=8X=A{d3SEnviVZTy>Gi3e?>gEyGb2xlPP0`8A*fWb3dLXNtGA0T>Jx^l1TsH(iDLDR+5BUp# zC(7nb{3O3?3>K!A(e^23jHz-`fVXd;y6rWNIMX^e0L%gFn) z%LNFfJPM$&e_GtdGl5V%5QLv7?l;kk6asY`k^WN{33dq6GrZRH>>=eAuE{kll$wWv z$Tt7&Nzlnm=_ar7C4DtXj{I!%Ac z&vdERA%~U#V0`&4?M_(zC>o$Z0X-x;hqK)lUQmbL7#-k1`}gjWTwDKW%Gm}bwz7nx zK|BlmC8dL^i7f%?N(zAx-X(2N>}a20yP7QsnmxKey>!aSE^e+L1zB}fE+#yL0hhis=0Fsp_5g7Svvv;E*LT)MxU1k^&!5j|&`nSN)TM zzm^F5r+xDcTp$y!5_V@WVDZ*iGIq!Sam-TG*46fM_?x<&Z^hKblJlA8Y|tEl$|^4= zc^eH%RT2Yyg^B)s4ai?~iTn@Izvo~J-SW?bEJ=Yo)h&x$MBV8)5bn&)t*iXI`p(JB zcY8-}?eco_1I5dD-_TctfLg(h4!(k`fRN?iw9P*s1{1KM>y4^t`zQe;HtlwM)SX&^ z?J+zIpZp8b#&6I2hk^Bi^impWA*fd|FVxY?8xa8&K8tv7WR?(pAYO` z>74xbui!lehrl8%>Ta-H^)|+i)J2v}IZc=Vu9gKL`MQnqh0`*p*ALYACskyHxP)Z97EO5-u~7izIMv9wWZ&MO)xrxhP?;a zi^g4xF?!Ro9)Ndfg%TpWk!N`Dcg>7l8{AuQsW8h8@4{=cN>HP4vtN5sqZ|#SOl}Sh z*z`FQ2gQbechO?ifu*`m*+<8V(H4NtjwKWty5-U$i08JEJ$)F7S~8xuVr~n9C<#Qr zlZTZHpKcU1t9|?@U-Xxzu#f5y@|x%*`n#L|&u3Fl0sKeT?33cwzk8Q|ULCs^SjYYH zE*6e|??n6GzNs?>_*)8V?qK;pukHU|j{ko-{$UOO-?EMg9q7T3Xk6XTX;L{4pmJWy z?6$5AcQeWqFK`1eR<%!IDV)(*aFSS zK_0fhef<6Y;+z~*8x&HJ+tC{qbR6;@Z)@*3pzj@@)m3O~072e%I}ZLIm;CQKjC~g1 zEgQ9!l5PBX!=(PUMEV~q9Sgjjkx%Z@3>q{$%S!iu^Jf3;0R6w0Uqcftb=1KZvo$m* z@h(~1e|%$HAOzr5jW?QY0C^Q+hvfH_{_PK3(gcB}e|e%99M^{8A92cma(CVwAnt31 zXm|a%hE!9XsQEWTjsLk)1w~+~nb!sT2CpIM=D0Zj;~O)P1vi$#)6(#ca7BA%_CL4k z&SGHIxHiHd5}oF|{a37n|NF*`z^NOGUG~`*2a%gm=K7y+S7Q_~J?xyCg@aINGJW+Q zZ`aO9aAWdTVefG6_m=lI>is9r{nz7(HARK#({3P-sIg=DE2#{yQD!pHP?~h{+aAk; zzvO0h0M}6GkEI)qs|PHClJ4QwH3E$!G?en|=gqQ-yh;Euz!y$r%R)UVD?lo{$t8$}%^wrwm#zfG>b z1eHD0nhQWcGfE1Kd}`$hGw0(GZyZf$fGn7xbjFY8A}i6Vv|*iFU^Y{Xm5Z zoF{(3czL<6{q>`%m&Mirn!CPKCuKm9j_#UJ<>T^S$pY$O6o^1BfYM^_%a6jKtreQ{ zq4aJ+_cc$@E6bNdi);y`)q^?CJ+2++km+v}Jhv1hFTD2!4>m4WaJ zMdJHn`tQ*O_%J{trk$_6y9%gV-hL$fZ(Q6P2n?u{@xo8Koj+TZPnC^<|3OYN8ecR0 zm#zHv)CLAvAuJ#C;V3zFKy(r{&lpopTCdOL6_;Rb6V?Gaf|ZLsssqU_*|?^`jm+!` zSg@gMbo}6&a*<1Sp#GpftWEamuiaOH7TW6OtmwtO==fMVCR*)YZgkDbZ{Mp1l5ZQw z+AaWmTq9aL1(fJsMb278dO1wy^LqWP8+OT8-r5p`Sry1=Zcw}UIctQ!eMNuREXkn6 z+bxOk;-9cV^Uz7)doeopG7exWh*Elt$Z)W>d(0K@F=CBwj%Ud>hh_8nK@cRy&BWig z^T&qxpD#?F+!e-O*qU%#j*n9?6#xAOW{`@P)8x>}>JLF=0TlFwjrUdD*GfDcoqWWz zD5?#ITp<}F;h;o2Yyu4&??u#B88(y)U#tD$ zF|t{ob)TZN-iH!PPYCa_(|RNB#FPP5ChSD;LFY|KfUHAoDu z_CWV{`1LQ$Ca1E#;+lX3tp4^NRt-eM7i&OZr0Ma5hMgGPOm;b0JBMnr%|T~Z5U@%@ z(j!uaUS7co>ug%Wm5KLFIRAX@zy0K&`^5VZ32C{QX&1>T+8-CgBA1+S1iB)Ed!9l} zPz>z@p2z$qVB05F@&ii|{I4Z5ovfCkn6=<;u1Jq1f>^S4)WPSCWk{PtofsA6Bl z8)m#q7E~_}Fr?gR6NVOf5Yck+(t{l?tv_j$EC%1Otj>*=OtxYE6r_h@^-t=!E=F~H z?grx#;2VI4vFTM_@sss4@DEm+104NxXr^Br)f_HPmf1+nP~!Z*RJ?&o`@YXe=57m0 z%zOzYwCW>c1wiJm`&7GuOF*J)%x7eIGaSXa?323i+W3Mw_&}<%P7Yh{!(AD_moo%m zZ{If1%s$k#e|B3#Cskj=Ruckwi1z6kw(cG+1Hb82Y?`~NZv%BT^fh(maLG7!mR5lDNPHxXheb4yE>MqCf)QyvMJ z32_zxMEXanx&v?tYAwOI5bFp99y_;Z*%0&SHXwcLRYh)HkwV)Dv==k2b4!O`Yff+a zpypdRWhT14mg509T2f#Rs{;Hz&p0zM1$@T^Xh15*QQ1Vbc(0fwK@Y8qM-Ce!#nx)(D3)}h zgWh5V{_7vHe5^>v-qjcUk@^4atztIV8g)vZskMtalTyhskOwR4v{Oqpxm}UT`EkLX z;A3&|(Tpd=u#vAlbf5OBLl?hNTvksFfWr11oxa*;cOvW@eLr66MZDqy)>y!hBi``zbdMI$~Ul5bJ=E$3<)XMFnz zOtYvquB^K|bRpZjtlEE61W{RzfvqCI7FtDW-{LO!=mLDIf9Qt>WOW{<#P? za!r|<=^fS^2{9j-;y1^#(9$>}gk!UkHtOzpyo;=6#Bh+)(4&_}O~gtSeQDD8PPw$Q zY~p%o@+85kVV5sojg7tmV-XzzKv$)0o*s3l1CcJ|Elz;=V&BfV6~fhR#$SPw&$6DZ zuw(5Acr8vSPHpTCY@E_3M{y&!eA+%cr$}}}fp*0r_}eE)?QH}IYP=kn^C}GGG=n7% zf&7@18LZHrVl;HJBCVXw)2Hmjq<;uG_h6RW4705ntZ*U~0GmsD-P)jwINp1cN86$4ktu8~m`X z!FwqXRThkO5Gd=@D%}tEXWIfsY`z_Abvmhl9Wa(pgovVzR@8gm8$jUbdxPhxX=L#d zay{P}lfqU^@qM_uOz+5TWm@b9E7_1VPO`gbS*KMHDEjjtrtg2TB)AOTMR)?}Zm~>d zf4&pf9c_3!BH3m%={g#FJp@Cl_o$iNrUKO?pqU&-x`mRvQ%K*9rb6b_9Qf!d

{Mj-X+y_?NEzki*-pR1W;tOuDm!BU_ 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�v0H}T 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

kejWM%L$oaW^Ro5(Ke{=ffO9CA98>@9T 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 |<──────────────── 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`. From 24f7be968d6459e887de7c18e17a2c7bb8294796 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Wed, 11 May 2022 15:46:45 +0100 Subject: [PATCH 085/103] Fix link --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index d77af87a8..64836152c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ 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](INSTALL.md)** for building and deploying your own Dendrite homeserver +* **[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 From 58af7f61b6c3719e15cb4088a343cd08b404f5be Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Wed, 11 May 2022 18:15:18 +0200 Subject: [PATCH 086/103] Fix OTK upload spam (#2448) * Fix OTK spam * Update comment * Optimize selectKeysCountSQL to only return max 100 keys * Return CurrentPosition if the request timed out * Revert "Return CurrentPosition if the request timed out" This reverts commit 7dbdda964189f5542048c06ce5ffc6d4da1814e6. Co-authored-by: kegsay --- keyserver/storage/postgres/one_time_keys_table.go | 4 +++- keyserver/storage/sqlite3/one_time_keys_table.go | 4 +++- syncapi/sync/requestpool.go | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/keyserver/storage/postgres/one_time_keys_table.go b/keyserver/storage/postgres/one_time_keys_table.go index d8c76b49b..2117efcae 100644 --- a/keyserver/storage/postgres/one_time_keys_table.go +++ b/keyserver/storage/postgres/one_time_keys_table.go @@ -53,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/sqlite3/one_time_keys_table.go b/keyserver/storage/sqlite3/one_time_keys_table.go index d2c0b7b20..7a923d0e5 100644 --- a/keyserver/storage/sqlite3/one_time_keys_table.go +++ b/keyserver/storage/sqlite3/one_time_keys_table.go @@ -52,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/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 8ab130911..30c490df0 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -248,7 +248,15 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi. defer userStreamListener.Close() 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 + err = internal.DeviceOTKCounts(syncReq.Context, rp.keyAPI, syncReq.Device.UserID, syncReq.Device.ID, syncReq.Response) + if err != nil { + syncReq.Log.WithError(err).Error("failed to get OTK counts") + } return util.JSONResponse{ Code: http.StatusOK, JSON: syncReq.Response, From 3437adf5970a11342c22089ec1a18f46d0708740 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 12 May 2022 10:11:46 +0100 Subject: [PATCH 087/103] Wait 100ms for events to be processed by syncapi --- syncapi/syncapi_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go index 7809cdaba..d3d898394 100644 --- a/syncapi/syncapi_test.go +++ b/syncapi/syncapi_test.go @@ -199,7 +199,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) for i, msg := range msgs { test.MustPublishMsgs(t, jsctx, msg) - time.Sleep(50 * time.Millisecond) + 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, From 0d1505a4c1ccca7c5cf4a64faf5d1044d8aa96f9 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Thu, 12 May 2022 11:35:35 +0200 Subject: [PATCH 088/103] Fix `create-account` with global database settings (#2455) * Fix create-account with global database settings * Avoid warning about open registration --- cmd/create-account/main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/cmd/create-account/main.go b/cmd/create-account/main.go index 7a5660522..7f6d5105e 100644 --- a/cmd/create-account/main.go +++ b/cmd/create-account/main.go @@ -25,6 +25,7 @@ import ( "strings" "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" @@ -99,8 +100,14 @@ func main() { } } + // avoid warning about open registration + cfg.ClientAPI.RegistrationDisabled = true + + b := base.NewBaseDendrite(cfg, "") + defer b.Close() // nolint: errcheck + accountDB, err := storage.NewUserAPIDatabase( - nil, + b, &cfg.UserAPI.AccountDatabase, cfg.Global.ServerName, cfg.UserAPI.BCryptCost, From fc670f03a2ac13adc7e022bcb21b82ad874d6706 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Thu, 12 May 2022 12:05:55 +0100 Subject: [PATCH 089/103] Separate sample configs for monolith and polylith (#2456) * Update sample configs * Update references * Remove sections that are dead in the monolith sample --- README.md | 2 +- build/docker/README.md | 7 +- build/docker/config/dendrite.yaml | 348 ------------------ dendrite-sample.monolith.yaml | 279 ++++++++++++++ ...nfig.yaml => dendrite-sample.polylith.yaml | 215 +++++------ docs/FAQ.md | 2 +- docs/installation/7_configuration.md | 10 +- 7 files changed, 377 insertions(+), 486 deletions(-) delete mode 100644 build/docker/config/dendrite.yaml create mode 100644 dendrite-sample.monolith.yaml rename dendrite-config.yaml => dendrite-sample.polylith.yaml (54%) diff --git a/README.md b/README.md index 9c38dee90..ed09e971c 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ $ ./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 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 94dcf4558..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: 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. - 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/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 54% rename from dendrite-config.yaml rename to dendrite-sample.polylith.yaml index 7709e0c87..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 @@ -54,16 +31,6 @@ global: # 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: postgres://user:pass@hostname/database?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: "" @@ -74,105 +41,90 @@ 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 - # Configures opt-in anonymous stats reporting. + # 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: - # Whether this instance sends anonymous usage stats enabled: false - - # The endpoint to report the anonymized homeserver usage statistics to. - # Defaults to https://matrix.org/report-usage-stats/push endpoint: https://matrix.org/report-usage-stats/push - # Server notices allows server admins to send messages to all users. + # 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 @@ -184,11 +136,13 @@ client_api: # 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. @@ -200,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 @@ -216,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 @@ -253,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 @@ -264,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 @@ -278,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. @@ -300,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 @@ -316,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 @@ -327,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 @@ -344,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. @@ -381,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/FAQ.md b/docs/FAQ.md index 571726d61..47f39b9e6 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -58,7 +58,7 @@ Bridges known to work (as of v0.5.1): * [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). +Remember to add the config file(s) to the `app_service_api` section of the config file. ## Is it possible to prevent communication with the outside world? diff --git a/docs/installation/7_configuration.md b/docs/installation/7_configuration.md index 868aba6ec..e676afbe6 100644 --- a/docs/installation/7_configuration.md +++ b/docs/installation/7_configuration.md @@ -7,11 +7,13 @@ permalink: /installation/configuration # Populate the configuration -The configuration file is used to configure Dendrite. A sample configuration file, -called [`dendrite-config.yaml`](https://github.com/matrix-org/dendrite/blob/main/dendrite-config.yaml), -is present in the top level of the Dendrite repository. +The configuration file is used to configure Dendrite. Sample configuration files are +present in the top level of the Dendrite repository: -You will need to duplicate this file, calling it `dendrite.yaml` for example, and then +* [`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: From 870f9b0c3f288950ab843b048485a0767e177bd1 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Fri, 13 May 2022 09:33:55 +0200 Subject: [PATCH 090/103] Shuffle config Verify/Defaults a bit around (#2459) --- setup/config/config_appservice.go | 7 +++++-- setup/config/config_clientapi.go | 22 +++++++++++----------- setup/config/config_federationapi.go | 16 ++++++++-------- setup/config/config_jetstream.go | 7 ++++--- setup/config/config_keyserver.go | 7 +++++-- setup/config/config_mediaapi.go | 17 ++++++++--------- setup/config/config_roomserver.go | 7 +++++-- setup/config/config_syncapi.go | 11 ++++++----- setup/config/config_userapi.go | 13 ++++++++----- 9 files changed, 60 insertions(+), 47 deletions(-) diff --git a/setup/config/config_appservice.go b/setup/config/config_appservice.go index d93b6ebe0..ff3287714 100644 --- a/setup/config/config_appservice.go +++ b/setup/config/config_appservice.go @@ -50,11 +50,14 @@ func (c *AppServiceAPI) Defaults(generate bool) { } func (c *AppServiceAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "app_service_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "app_service_api.internal_api.bind", string(c.InternalAPI.Connect)) 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.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 6104ed8b9..bb786a145 100644 --- a/setup/config/config_clientapi.go +++ b/setup/config/config_clientapi.go @@ -67,19 +67,13 @@ func (c *ClientAPI) Defaults(generate bool) { } 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 { @@ -93,6 +87,12 @@ func (c *ClientAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { ) } } + 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 f62a23e1f..a7a515fda 100644 --- a/setup/config/config_federationapi.go +++ b/setup/config/config_federationapi.go @@ -34,24 +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) { - 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)) - } 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)) } // The config for setting a proxy to use for server->server requests 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 9e2d54cdc..5f2f22c8a 100644 --- a/setup/config/config_keyserver.go +++ b/setup/config/config_keyserver.go @@ -18,9 +18,12 @@ func (c *KeyServer) Defaults(generate bool) { } func (c *KeyServer) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "key_server.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "key_server.internal_api.bind", string(c.InternalAPI.Connect)) 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.connect", string(c.InternalAPI.Connect)) } diff --git a/setup/config/config_mediaapi.go b/setup/config/config_mediaapi.go index 273de322a..9717aa59e 100644 --- a/setup/config/config_mediaapi.go +++ b/setup/config/config_mediaapi.go @@ -42,26 +42,19 @@ 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.base_path", string(c.BasePath)) checkPositive(configErrs, "media_api.max_file_size_bytes", int64(c.MaxFileSizeBytes)) checkPositive(configErrs, "media_api.max_thumbnail_generators", int64(c.MaxThumbnailGenerators)) @@ -70,4 +63,10 @@ func (c *MediaAPI) Verify(configErrs *ConfigErrors, isMonolith bool) { 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_roomserver.go b/setup/config/config_roomserver.go index 8a3227349..bd6aa1167 100644 --- a/setup/config/config_roomserver.go +++ b/setup/config/config_roomserver.go @@ -18,9 +18,12 @@ func (c *RoomServer) Defaults(generate bool) { } func (c *RoomServer) Verify(configErrs *ConfigErrors, isMonolith bool) { - checkURL(configErrs, "room_server.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "room_server.internal_ap.bind", string(c.InternalAPI.Connect)) 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.connect", string(c.InternalAPI.Connect)) } diff --git a/setup/config/config_syncapi.go b/setup/config/config_syncapi.go index 48fd9f506..7d5e3808a 100644 --- a/setup/config/config_syncapi.go +++ b/setup/config/config_syncapi.go @@ -22,12 +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)) } + 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 4aa3b57bb..d1e2b7fe1 100644 --- a/setup/config/config_userapi.go +++ b/setup/config/config_userapi.go @@ -26,19 +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) { - checkURL(configErrs, "user_api.internal_api.listen", string(c.InternalAPI.Listen)) - checkURL(configErrs, "user_api.internal_api.connect", string(c.InternalAPI.Connect)) + 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)) } - checkPositive(configErrs, "user_api.openid_token_lifetime_ms", c.OpenIDTokenLifetimeMS) + 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)) } From b57fdcc82d0c57ae3eed401e7b9891cc9b53a8d3 Mon Sep 17 00:00:00 2001 From: Till Faelligen Date: Fri, 13 May 2022 10:24:26 +0200 Subject: [PATCH 091/103] Only try to get OTKs if the context isn't done yet --- syncapi/sync/requestpool.go | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index 30c490df0..fdf46cdde 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -253,9 +253,12 @@ func (rp *RequestPool) OnIncomingSyncRequest(req *http.Request, device *userapi. // 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 - err = internal.DeviceOTKCounts(syncReq.Context, rp.keyAPI, syncReq.Device.UserID, syncReq.Device.ID, syncReq.Response) - if err != nil { - syncReq.Log.WithError(err).Error("failed to get OTK counts") + // 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, From cafc2d2c10daeeaf8012a50163d07815b5516043 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 13 May 2022 11:36:04 +0100 Subject: [PATCH 092/103] Update NATS Server to version 2.8.2 (#2460) --- go.mod | 2 +- go.sum | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d14ced5b7..ca817b00f 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 diff --git a/go.sum b/go.sum index 8b518935c..7544768c3 100644 --- a/go.sum +++ b/go.sum @@ -889,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= @@ -1280,7 +1280,7 @@ 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-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= From be9be2553f0f18baed07755e81669fd374f3525a Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 13 May 2022 11:52:04 +0100 Subject: [PATCH 093/103] Resolve over old and new extremities (#2457) * Feed existing state into state res when calculating state from new extremities * Remove duplicates * Fix bug * Sort and unique * Update to matrix-org/gomatrixserverlib#308 * Trim the slice properly * Update gomatrixserverlib again * Update to matrix-org/gomatrixserverlib#308 --- go.mod | 2 +- go.sum | 4 ++-- .../internal/input/input_latest_events.go | 19 +++++++++++++------ roomserver/types/types.go | 15 +++++++++++++++ 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ca817b00f..ecdeb77fa 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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-20220509120958-8d818048c34c + 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 diff --git a/go.sum b/go.sum index 7544768c3..145c2a04c 100644 --- a/go.sum +++ b/go.sum @@ -795,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-20220509120958-8d818048c34c h1:KqzqFWxvs90pcDaW9QEveW+Q5JcEYuNnKyaqXc+ohno= -github.com/matrix-org/gomatrixserverlib v0.0.0-20220509120958-8d818048c34c/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= diff --git a/roomserver/internal/input/input_latest_events.go b/roomserver/internal/input/input_latest_events.go index 9ad8b0422..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 diff --git a/roomserver/types/types.go b/roomserver/types/types.go index 65fbee04e..ce4e5fd1e 100644 --- a/roomserver/types/types.go +++ b/roomserver/types/types.go @@ -18,6 +18,7 @@ package types import ( "encoding/json" "sort" + "strings" "github.com/matrix-org/gomatrixserverlib" "golang.org/x/crypto/blake2b" @@ -166,6 +167,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 { From 1698c395794ab853bf377b293cc0e0cc074cfe00 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 13 May 2022 11:52:42 +0100 Subject: [PATCH 094/103] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ed09e971c..e8b7bd0e2 100644 --- a/README.md +++ b/README.md @@ -53,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 [the Installation documentation](docs/installation). 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: From b40b548432b465e37c6045b6735f9eaf426902f0 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 13 May 2022 12:06:47 +0100 Subject: [PATCH 095/103] The Pinecone `gobind` demo must listen on `localhost` for `baseURL` to be correct --- build/gobind-pinecone/monolith.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/gobind-pinecone/monolith.go b/build/gobind-pinecone/monolith.go index 310ac7dda..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) } From 6af35385ba06f75610396b91452cbf381c1f5443 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Fri, 13 May 2022 13:17:15 +0100 Subject: [PATCH 096/103] Version 0.8.5 (#2461) * Version 0.8.5 * Update changelog * Update changelog --- CHANGES.md | 15 +++++++++++++++ internal/version.go | 2 +- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index c058da6a1..3deebd8a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,20 @@ # 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 diff --git a/internal/version.go b/internal/version.go index 5097bb2a6..04c9a8a88 100644 --- a/internal/version.go +++ b/internal/version.go @@ -17,7 +17,7 @@ var build string const ( VersionMajor = 0 VersionMinor = 8 - VersionPatch = 4 + VersionPatch = 5 VersionTag = "" // example: "rc1" ) From 05607d6b8734738bd5c32288e3d0ef8e827d11d0 Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Mon, 16 May 2022 19:33:16 +0200 Subject: [PATCH 097/103] Add roomserver tests (3/4) (#2447) * Add Room Aliases tests * Add Rooms table test * Move StateKeyTuplerSorter to the types package * Add StateBlock tests Some optimizations * Add State Snapshot tests Some optimization * Return []int64 and convert to pq.Int64Array for postgres * Move []types.EventNID back to rows.Next() * Update tests, rename SelectRoomIDs --- roomserver/storage/postgres/events_table.go | 6 +- .../storage/postgres/room_aliases_table.go | 6 +- roomserver/storage/postgres/rooms_table.go | 16 +-- .../storage/postgres/state_block_table.go | 53 ++------ .../postgres/state_block_table_test.go | 86 ------------ .../storage/postgres/state_snapshot_table.go | 10 +- roomserver/storage/postgres/storage.go | 16 +-- roomserver/storage/shared/storage.go | 2 +- roomserver/storage/sqlite3/events_table.go | 4 +- .../storage/sqlite3/room_aliases_table.go | 6 +- roomserver/storage/sqlite3/rooms_table.go | 16 +-- .../storage/sqlite3/state_block_table.go | 49 ++----- .../storage/sqlite3/state_block_table_test.go | 86 ------------ .../storage/sqlite3/state_snapshot_table.go | 10 +- roomserver/storage/sqlite3/storage.go | 16 +-- roomserver/storage/tables/interface.go | 2 +- .../storage/tables/room_aliases_table_test.go | 96 +++++++++++++ roomserver/storage/tables/rooms_table_test.go | 128 ++++++++++++++++++ .../storage/tables/state_block_table_test.go | 92 +++++++++++++ .../tables/state_snapshot_table_test.go | 86 ++++++++++++ roomserver/types/types.go | 33 +++++ roomserver/types/types_test.go | 64 +++++++++ 22 files changed, 570 insertions(+), 313 deletions(-) delete mode 100644 roomserver/storage/postgres/state_block_table_test.go delete mode 100644 roomserver/storage/sqlite3/state_block_table_test.go create mode 100644 roomserver/storage/tables/room_aliases_table_test.go create mode 100644 roomserver/storage/tables/rooms_table_test.go create mode 100644 roomserver/storage/tables/state_block_table_test.go create mode 100644 roomserver/storage/tables/state_snapshot_table_test.go diff --git a/roomserver/storage/postgres/events_table.go b/roomserver/storage/postgres/events_table.go index 86d226ce7..a4d05756d 100644 --- a/roomserver/storage/postgres/events_table.go +++ b/roomserver/storage/postgres/events_table.go @@ -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 } 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 88df72009..70ea4d8ba 100644 --- a/roomserver/storage/postgres/storage.go +++ b/roomserver/storage/postgres/storage.go @@ -80,19 +80,19 @@ func (d *Database) create(db *sql.DB) error { 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 { return err } - if err := createRoomAliasesTable(db); err != nil { + if err := CreateRoomAliasesTable(db); err != nil { return err } if err := CreateInvitesTable(db); err != nil { @@ -128,15 +128,15 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room 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 } @@ -144,7 +144,7 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room if err != nil { return err } - roomAliases, err := prepareRoomAliasesTable(db) + roomAliases, err := PrepareRoomAliasesTable(db) if err != nil { return err } 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/events_table.go b/roomserver/storage/sqlite3/events_table.go index feb06150a..1dda34c36 100644 --- a/roomserver/storage/sqlite3/events_table.go +++ b/roomserver/storage/sqlite3/events_table.go @@ -247,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 { 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 a4e32d528..8325fdad5 100644 --- a/roomserver/storage/sqlite3/storage.go +++ b/roomserver/storage/sqlite3/storage.go @@ -89,19 +89,19 @@ func (d *Database) create(db *sql.DB) error { 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 { return err } - if err := createRoomAliasesTable(db); err != nil { + if err := CreateRoomAliasesTable(db); err != nil { return err } if err := CreateInvitesTable(db); err != nil { @@ -137,15 +137,15 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room 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 } @@ -153,7 +153,7 @@ func (d *Database) prepare(db *sql.DB, writer sqlutil.Writer, cache caching.Room if err != nil { return err } - roomAliases, err := prepareRoomAliasesTable(db) + roomAliases, err := PrepareRoomAliasesTable(db) if err != nil { return err } diff --git a/roomserver/storage/tables/interface.go b/roomserver/storage/tables/interface.go index 95609787a..116e11c4e 100644 --- a/roomserver/storage/tables/interface.go +++ b/roomserver/storage/tables/interface.go @@ -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/room_aliases_table_test.go b/roomserver/storage/tables/room_aliases_table_test.go new file mode 100644 index 000000000..8fb57d5a4 --- /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() + 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..9872fb800 --- /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() + 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 ce4e5fd1e..62695aaee 100644 --- a/roomserver/types/types.go +++ b/roomserver/types/types.go @@ -21,6 +21,7 @@ import ( "strings" "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" "golang.org/x/crypto/blake2b" ) @@ -97,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 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) + } + } +} From cd82460513d5abf04e56c01667d56499d4c354be Mon Sep 17 00:00:00 2001 From: kegsay Date: Tue, 17 May 2022 10:45:50 +0100 Subject: [PATCH 098/103] Add docs which explain how to calculate coverage (#2468) --- docs/coverage.md | 84 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 docs/coverage.md 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 From 6de29c1cd23d218f04d2e570932db8967d6adc4f Mon Sep 17 00:00:00 2001 From: kegsay Date: Tue, 17 May 2022 13:23:35 +0100 Subject: [PATCH 099/103] bugfix: E2EE device keys could sometimes not be sent to remote servers (#2466) * Fix flakey sytest 'Local device key changes get to remote servers' * Debug logs * Remove internal/test and use /test only Remove a lot of ancient code too. * Use FederationRoomserverAPI in more places * Use more interfaces in federationapi; begin adding regression test * Linting * Add regression test * Unbreak tests * ALL THE LOGS * Fix a race condition which could cause events to not be sent to servers If a new room event which rewrites state arrives, we remove all joined hosts then re-calculate them. This wasn't done in a transaction so for a brief period we would have no joined hosts. During this interim, key change events which arrive would not be sent to destination servers. This would sporadically fail on sytest. * Unbreak new tests * Linting --- cmd/generate-keys/main.go | 2 +- federationapi/api/api.go | 40 ++- federationapi/consumers/keychange.go | 8 +- federationapi/consumers/roomserver.go | 29 +-- federationapi/federationapi.go | 4 +- federationapi/federationapi_test.go | 236 +++++++++++++++++- federationapi/internal/api.go | 8 +- federationapi/internal/perform.go | 18 +- federationapi/queue/destinationqueue.go | 31 +-- federationapi/queue/queue.go | 9 +- federationapi/routing/query.go | 2 +- federationapi/routing/routing.go | 2 +- federationapi/routing/send.go | 2 +- federationapi/routing/send_test.go | 2 +- federationapi/routing/threepid.go | 16 +- federationapi/storage/interface.go | 3 +- .../storage/postgres/joined_hosts_table.go | 5 + federationapi/storage/shared/storage.go | 26 +- internal/caching/cache_typing_test.go | 2 +- internal/test/client.go | 158 ------------ internal/test/kafka.go | 76 ------ internal/test/server.go | 152 ----------- keyserver/internal/device_list_update.go | 4 +- keyserver/internal/internal.go | 2 +- keyserver/keyserver.go | 2 +- roomserver/api/api.go | 1 + roomserver/internal/input/input_test.go | 4 +- roomserver/internal/query/query_test.go | 2 +- .../storage/tables/events_table_test.go | 2 +- .../tables/previous_events_table_test.go | 2 +- .../storage/tables/published_table_test.go | 2 +- .../storage/tables/room_aliases_table_test.go | 2 +- roomserver/storage/tables/rooms_table_test.go | 2 +- syncapi/storage/storage_test.go | 6 +- .../storage/tables/output_room_events_test.go | 2 +- syncapi/storage/tables/topology_test.go | 2 +- syncapi/syncapi_test.go | 15 +- test/event.go | 18 ++ test/http.go | 47 ++++ {internal/test => test}/keyring.go | 0 internal/test/config.go => test/keys.go | 84 ------- test/room.go | 66 +++-- {internal/test => test}/slice.go | 0 test/{ => testrig}/base.go | 11 +- test/{ => testrig}/jetstream.go | 2 +- test/user.go | 52 +++- userapi/storage/storage_test.go | 20 +- userapi/userapi_test.go | 3 +- 48 files changed, 566 insertions(+), 618 deletions(-) delete mode 100644 internal/test/client.go delete mode 100644 internal/test/kafka.go delete mode 100644 internal/test/server.go rename {internal/test => test}/keyring.go (100%) rename internal/test/config.go => test/keys.go (61%) rename {internal/test => test}/slice.go (100%) rename test/{ => testrig}/base.go (92%) rename test/{ => testrig}/jetstream.go (98%) 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/federationapi/api/api.go b/federationapi/api/api.go index fc25194e0..53d4701f3 100644 --- a/federationapi/api/api.go +++ b/federationapi/api/api.go @@ -12,12 +12,16 @@ import ( // FederationInternalAPI is used to query information from the federation sender. type FederationInternalAPI interface { - FederationClient + 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( @@ -60,17 +64,43 @@ type RoomserverFederationAPI interface { LookupMissingEvents(ctx context.Context, s gomatrixserverlib.ServerName, roomID string, missing gomatrixserverlib.MissingEvents, roomVersion gomatrixserverlib.RoomVersion) (res gomatrixserverlib.RespMissingEvents, err error) } -// FederationClient is a subset of gomatrixserverlib.FederationClient functions which the fedsender +// 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 FederationClient interface { - gomatrixserverlib.FederatedStateClient +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) + + 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) } // FederationClientError is returned from FederationClient methods in the event of a problem. diff --git a/federationapi/consumers/keychange.go b/federationapi/consumers/keychange.go index 0ece18e97..95c9a7fdd 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 } + logrus.Infof("DEBUG: %v joined rooms for user %v", queryRes.RoomIDs, m.UserID) // 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 { @@ -128,6 +129,9 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool { } if len(destinations) == 0 { + logger.WithField("num_rooms", len(queryRes.RoomIDs)).Debug("user is in no federated rooms") + destinations, err = t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, false) + logrus.Infof("GetJoinedHostsForRooms exclude self=false -> %v %v", destinations, err) return true } // Pack the EDU and marshal it diff --git a/federationapi/consumers/roomserver.go b/federationapi/consumers/roomserver.go index 80317ee69..7a0816ff2 100644 --- a/federationapi/consumers/roomserver.go +++ b/federationapi/consumers/roomserver.go @@ -21,6 +21,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/nats-io/nats.go" + "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" "github.com/matrix-org/dendrite/federationapi/queue" @@ -36,7 +37,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 +52,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 +90,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,7 +138,7 @@ 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 { +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. @@ -164,7 +157,7 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) err addsStateEvents = append(addsStateEvents, eventsRes.Events...) } - addsJoinedHosts, err := joinedHostsFromEvents(gomatrixserverlib.UnwrapEventHeaders(addsStateEvents)) + addsJoinedHosts, err := JoinedHostsFromEvents(gomatrixserverlib.UnwrapEventHeaders(addsStateEvents)) if err != nil { return err } @@ -173,13 +166,13 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent) err // expressed as a delta against the current state. // TODO(#290): handle EventIDMismatchError and recover the current state by // talking to the roomserver + logrus.Infof("room %s adds joined hosts: %v removes %v", ore.Event.RoomID(), addsJoinedHosts, ore.RemovesStateEventIDs) 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 @@ -238,7 +231,7 @@ func (s *OutputRoomEventConsumer) joinedHostsAtEvent( return nil, err } - combinedAddsJoinedHosts, err := joinedHostsFromEvents(combinedAddsEvents) + combinedAddsJoinedHosts, err := JoinedHostsFromEvents(combinedAddsEvents) if err != nil { return nil, err } @@ -284,10 +277,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 bec9ac777..ff159beea 100644 --- a/federationapi/federationapi.go +++ b/federationapi/federationapi.go @@ -93,8 +93,8 @@ 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, diff --git a/federationapi/federationapi_test.go b/federationapi/federationapi_test.go index eedebc6cd..ae244c566 100644 --- a/federationapi/federationapi_test.go +++ b/federationapi/federationapi_test.go @@ -3,18 +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/federationapi/api" "github.com/matrix-org/dendrite/federationapi/internal" - "github.com/matrix-org/dendrite/internal/test" + 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) { @@ -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 577cb70e0..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" @@ -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. @@ -650,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/queue/destinationqueue.go b/federationapi/queue/destinationqueue.go index 747940403..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. diff --git a/federationapi/queue/queue.go b/federationapi/queue/queue.go index d152886f5..4c25c4ce6 100644 --- a/federationapi/queue/queue.go +++ b/federationapi/queue/queue.go @@ -26,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" @@ -39,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 @@ -85,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 { diff --git a/federationapi/routing/query.go b/federationapi/routing/query.go index 707b7b019..316c61a14 100644 --- a/federationapi/routing/query.go +++ b/federationapi/routing/query.go @@ -30,7 +30,7 @@ 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.FederationRoomserverAPI, senderAPI federationAPI.FederationInternalAPI, diff --git a/federationapi/routing/routing.go b/federationapi/routing/routing.go index 9f95ed07e..e25f9866e 100644 --- a/federationapi/routing/routing.go +++ b/federationapi/routing/routing.go @@ -54,7 +54,7 @@ func Setup( rsAPI roomserverAPI.FederationRoomserverAPI, fsAPI *fedInternal.FederationInternalAPI, keys gomatrixserverlib.JSONVerifier, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, userAPI userapi.FederationUserAPI, keyAPI keyserverAPI.FederationKeyAPI, mscCfg *config.MSCs, diff --git a/federationapi/routing/send.go b/federationapi/routing/send.go index 55a113675..c25dabce9 100644 --- a/federationapi/routing/send.go +++ b/federationapi/routing/send.go @@ -85,7 +85,7 @@ func Send( rsAPI api.FederationRoomserverAPI, keyAPI keyapi.FederationKeyAPI, keys gomatrixserverlib.JSONVerifier, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, mu *internal.MutexByRoom, servers federationAPI.ServersInRoomProvider, producer *producers.SyncAPIProducer, diff --git a/federationapi/routing/send_test.go b/federationapi/routing/send_test.go index 011d4e342..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" ) diff --git a/federationapi/routing/threepid.go b/federationapi/routing/threepid.go index 16f245cee..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" @@ -57,7 +58,7 @@ var ( func CreateInvitesFrom3PIDInvites( req *http.Request, rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, - federation *gomatrixserverlib.FederationClient, + federation federationAPI.FederationClient, userAPI userapi.FederationUserAPI, ) util.JSONResponse { var body invites @@ -107,7 +108,7 @@ func ExchangeThirdPartyInvite( roomID string, 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() @@ -205,7 +211,7 @@ func ExchangeThirdPartyInvite( func createInviteFrom3PIDInvite( ctx context.Context, rsAPI api.FederationRoomserverAPI, cfg *config.FederationAPI, - inv invite, federation *gomatrixserverlib.FederationClient, + inv invite, federation federationAPI.FederationClient, userAPI userapi.FederationUserAPI, ) (*gomatrixserverlib.Event, error) { verReq := api.QueryRoomVersionForRoomRequest{RoomID: inv.RoomID} @@ -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 e3038651b..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) diff --git a/federationapi/storage/postgres/joined_hosts_table.go b/federationapi/storage/postgres/joined_hosts_table.go index 5c95b72a8..bb6f6bfa3 100644 --- a/federationapi/storage/postgres/joined_hosts_table.go +++ b/federationapi/storage/postgres/joined_hosts_table.go @@ -24,6 +24,7 @@ import ( "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/gomatrixserverlib" + "github.com/sirupsen/logrus" ) const joinedHostsSchema = ` @@ -111,6 +112,7 @@ func (s *joinedHostsStatements) InsertJoinedHosts( roomID, eventID string, serverName gomatrixserverlib.ServerName, ) error { + logrus.Debugf("FederationJoinedHosts: INSERT %v %v %v", roomID, eventID, serverName) stmt := sqlutil.TxStmt(txn, s.insertJoinedHostsStmt) _, err := stmt.ExecContext(ctx, roomID, eventID, serverName) return err @@ -119,6 +121,7 @@ func (s *joinedHostsStatements) InsertJoinedHosts( func (s *joinedHostsStatements) DeleteJoinedHosts( ctx context.Context, txn *sql.Tx, eventIDs []string, ) error { + logrus.Debugf("FederationJoinedHosts: DELETE WITH EVENTS %v", eventIDs) stmt := sqlutil.TxStmt(txn, s.deleteJoinedHostsStmt) _, err := stmt.ExecContext(ctx, pq.StringArray(eventIDs)) return err @@ -127,6 +130,7 @@ func (s *joinedHostsStatements) DeleteJoinedHosts( func (s *joinedHostsStatements) DeleteJoinedHostsForRoom( ctx context.Context, txn *sql.Tx, roomID string, ) error { + logrus.Debugf("FederationJoinedHosts: DELETE ALL IN ROOM %v", roomID) stmt := sqlutil.TxStmt(txn, s.deleteJoinedHostsForRoomStmt) _, err := stmt.ExecContext(ctx, roomID) return err @@ -207,6 +211,7 @@ func joinedHostsFromStmt( ServerName: gomatrixserverlib.ServerName(serverName), }) } + logrus.Debugf("FederationJoinedHosts: SELECT %v => %+v", roomID, result) return result, rows.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/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/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/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 be71e5750..f8d0d69c3 100644 --- a/keyserver/internal/internal.go +++ b/keyserver/internal/internal.go @@ -37,7 +37,7 @@ import ( type KeyInternalAPI struct { DB storage.Database ThisServer gomatrixserverlib.ServerName - FedClient fedsenderapi.FederationClient + FedClient fedsenderapi.KeyserverFederationAPI UserAPI userapi.KeyserverUserAPI Producer *producers.KeyChange Updater *DeviceListUpdater diff --git a/keyserver/keyserver.go b/keyserver/keyserver.go index 47d7f57f9..3ffd3ba1e 100644 --- a/keyserver/keyserver.go +++ b/keyserver/keyserver.go @@ -37,7 +37,7 @@ 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, _ := base.NATS.Prepare(base.ProcessContext, &cfg.Matrix.JetStream) diff --git a/roomserver/api/api.go b/roomserver/api/api.go index cbb4cebca..80e7aed64 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -183,6 +183,7 @@ type FederationRoomserverAPI interface { 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. diff --git a/roomserver/internal/input/input_test.go b/roomserver/internal/input/input_test.go index a95c13550..7c65f9eac 100644 --- a/roomserver/internal/input/input_test.go +++ b/roomserver/internal/input/input_test.go @@ -12,7 +12,7 @@ import ( "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/test" + "github.com/matrix-org/dendrite/test/testrig" "github.com/matrix-org/gomatrixserverlib" "github.com/nats-io/nats.go" ) @@ -22,7 +22,7 @@ var jc *nats.Conn func TestMain(m *testing.M) { var b *base.BaseDendrite - b, js, jc = test.Base(nil) + b, js, jc = testrig.Base(nil) code := m.Run() b.ShutdownDendrite() b.WaitForComponentsToFinish() 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/storage/tables/events_table_test.go b/roomserver/storage/tables/events_table_test.go index d5d699c4c..6f72a59b5 100644 --- a/roomserver/storage/tables/events_table_test.go +++ b/roomserver/storage/tables/events_table_test.go @@ -39,7 +39,7 @@ func mustCreateEventsTable(t *testing.T, dbType test.DBType) (tables.Events, fun } func Test_EventsTable(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) ctx := context.Background() test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { diff --git a/roomserver/storage/tables/previous_events_table_test.go b/roomserver/storage/tables/previous_events_table_test.go index 96d7bfed0..63d540696 100644 --- a/roomserver/storage/tables/previous_events_table_test.go +++ b/roomserver/storage/tables/previous_events_table_test.go @@ -38,7 +38,7 @@ func mustCreatePreviousEventsTable(t *testing.T, dbType test.DBType) (tab tables func TestPreviousEventsTable(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, close := mustCreatePreviousEventsTable(t, dbType) diff --git a/roomserver/storage/tables/published_table_test.go b/roomserver/storage/tables/published_table_test.go index 87662ed4c..fff6dc186 100644 --- a/roomserver/storage/tables/published_table_test.go +++ b/roomserver/storage/tables/published_table_test.go @@ -38,7 +38,7 @@ func mustCreatePublishedTable(t *testing.T, dbType test.DBType) (tab tables.Publ func TestPublishedTable(t *testing.T) { ctx := context.Background() - alice := test.NewUser() + alice := test.NewUser(t) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { tab, close := mustCreatePublishedTable(t, dbType) diff --git a/roomserver/storage/tables/room_aliases_table_test.go b/roomserver/storage/tables/room_aliases_table_test.go index 8fb57d5a4..624d92ae6 100644 --- a/roomserver/storage/tables/room_aliases_table_test.go +++ b/roomserver/storage/tables/room_aliases_table_test.go @@ -36,7 +36,7 @@ func mustCreateRoomAliasesTable(t *testing.T, dbType test.DBType) (tab tables.Ro } func TestRoomAliasesTable(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) room2 := test.NewRoom(t, alice) ctx := context.Background() diff --git a/roomserver/storage/tables/rooms_table_test.go b/roomserver/storage/tables/rooms_table_test.go index 9872fb800..0a02369a1 100644 --- a/roomserver/storage/tables/rooms_table_test.go +++ b/roomserver/storage/tables/rooms_table_test.go @@ -38,7 +38,7 @@ func mustCreateRoomsTable(t *testing.T, dbType test.DBType) (tab tables.Rooms, c } func TestRoomsTable(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) ctx := context.Background() test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { diff --git a/syncapi/storage/storage_test.go b/syncapi/storage/storage_test.go index 1150c2f3d..563c92e34 100644 --- a/syncapi/storage/storage_test.go +++ b/syncapi/storage/storage_test.go @@ -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/tables/output_room_events_test.go b/syncapi/storage/tables/output_room_events_test.go index 8bbf879d4..69bbd04c9 100644 --- a/syncapi/storage/tables/output_room_events_test.go +++ b/syncapi/storage/tables/output_room_events_test.go @@ -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 2334aae2e..f4f75bdf3 100644 --- a/syncapi/storage/tables/topology_test.go +++ b/syncapi/storage/tables/topology_test.go @@ -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/syncapi_test.go b/syncapi/syncapi_test.go index d3d898394..5ecfd8772 100644 --- a/syncapi/syncapi_test.go +++ b/syncapi/syncapi_test.go @@ -15,6 +15,7 @@ import ( "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" @@ -86,7 +87,7 @@ func TestSyncAPIAccessTokens(t *testing.T) { } func testSyncAccessTokens(t *testing.T, dbType test.DBType) { - user := test.NewUser() + user := test.NewUser(t) room := test.NewRoom(t, user) alice := userapi.Device{ ID: "ALICEID", @@ -96,14 +97,14 @@ func testSyncAccessTokens(t *testing.T, dbType test.DBType) { AccountType: userapi.AccountTypeUser, } - base, close := test.CreateBaseDendrite(t, dbType) + 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{}) - test.MustPublishMsgs(t, jsctx, msgs...) + testrig.MustPublishMsgs(t, jsctx, msgs...) testCases := []struct { name string @@ -173,7 +174,7 @@ func TestSyncAPICreateRoomSyncEarly(t *testing.T) { } func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { - user := test.NewUser() + user := test.NewUser(t) room := test.NewRoom(t, user) alice := userapi.Device{ ID: "ALICEID", @@ -183,7 +184,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { AccountType: userapi.AccountTypeUser, } - base, close := test.CreateBaseDendrite(t, dbType) + base, close := testrig.CreateBaseDendrite(t, dbType) defer close() jsctx, _ := base.NATS.Prepare(base.ProcessContext, &base.Cfg.Global.JetStream) @@ -198,7 +199,7 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { sinceTokens := make([]string, len(msgs)) AddPublicRoutes(base, &syncUserAPI{accounts: []userapi.Device{alice}}, &syncRoomserverAPI{rooms: []*test.Room{room}}, &syncKeyAPI{}) for i, msg := range msgs { - test.MustPublishMsgs(t, jsctx, msg) + 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{ @@ -262,7 +263,7 @@ func toNATSMsgs(t *testing.T, base *base.BaseDendrite, input []*gomatrixserverli if ev.StateKey() != nil { addsStateIDs = append(addsStateIDs, ev.EventID()) } - result[i] = test.NewOutputEventMsg(t, base, ev.RoomID(), api.OutputEvent{ + result[i] = testrig.NewOutputEventMsg(t, base, ev.RoomID(), api.OutputEvent{ Type: rsapi.OutputTypeNewRoomEvent, NewRoomEvent: &rsapi.OutputNewRoomEvent{ Event: ev, diff --git a/test/event.go b/test/event.go index 40cb8f0e1..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)) diff --git a/test/http.go b/test/http.go index a458a3385..37b3648f8 100644 --- a/test/http.go +++ b/test/http.go @@ -2,10 +2,15 @@ package test import ( "bytes" + "context" "encoding/json" + "fmt" "io" + "net" "net/http" "net/url" + "path/filepath" + "sync" "testing" ) @@ -43,3 +48,45 @@ func NewRequest(t *testing.T, method, path string, opts ...HTTPRequestOpt) *http } 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/base.go b/test/testrig/base.go similarity index 92% rename from test/base.go rename to test/testrig/base.go index 664442c03..facb49f3e 100644 --- a/test/base.go +++ b/test/testrig/base.go @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -package test +package testrig import ( "errors" @@ -24,22 +24,23 @@ import ( "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 DBType) (*base.BaseDendrite, func()) { +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 DBTypePostgres: + 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 := PrepareDBConnectionString(t, dbType) + connStr, close := test.PrepareDBConnectionString(t, dbType) cfg.Global.DatabaseOptions = config.DatabaseOptions{ ConnectionString: config.DataSource(connStr), MaxOpenConnections: 10, @@ -47,7 +48,7 @@ func CreateBaseDendrite(t *testing.T, dbType DBType) (*base.BaseDendrite, func() ConnMaxLifetimeSeconds: 60, } return base.NewBaseDendrite(&cfg, "Test", base.DisableMetrics), close - case DBTypeSQLite: + 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 :( diff --git a/test/jetstream.go b/test/testrig/jetstream.go similarity index 98% rename from test/jetstream.go rename to test/testrig/jetstream.go index 488c22beb..74cf95062 100644 --- a/test/jetstream.go +++ b/test/testrig/jetstream.go @@ -1,4 +1,4 @@ -package test +package testrig import ( "encoding/json" 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/storage/storage_test.go b/userapi/storage/storage_test.go index 5683fe067..5bee880d3 100644 --- a/userapi/storage/storage_test.go +++ b/userapi/storage/storage_test.go @@ -43,7 +43,7 @@ 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() + alice := test.NewUser(t) localpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) @@ -74,7 +74,7 @@ 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() + alice := test.NewUser(t) aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) @@ -128,7 +128,7 @@ func Test_Accounts(t *testing.T) { } func Test_Devices(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) localpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) deviceID := util.RandomString(8) @@ -212,7 +212,7 @@ func Test_Devices(t *testing.T) { } func Test_KeyBackup(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) room := test.NewRoom(t, alice) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { @@ -291,7 +291,7 @@ func Test_KeyBackup(t *testing.T) { } func Test_LoginToken(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { db, close := mustCreateDatabase(t, dbType) defer close() @@ -321,7 +321,7 @@ func Test_LoginToken(t *testing.T) { } func Test_OpenID(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) token := util.RandomString(24) test.WithAllDatabases(t, func(t *testing.T, dbType test.DBType) { @@ -341,7 +341,7 @@ func Test_OpenID(t *testing.T) { } func Test_Profile(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) @@ -379,7 +379,7 @@ func Test_Profile(t *testing.T) { } func Test_Pusher(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) @@ -430,7 +430,7 @@ func Test_Pusher(t *testing.T) { } func Test_ThreePID(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) @@ -467,7 +467,7 @@ func Test_ThreePID(t *testing.T) { } func Test_Notification(t *testing.T) { - alice := test.NewUser() + alice := test.NewUser(t) aliceLocalpart, _, err := gomatrixserverlib.SplitID('@', alice.ID) assert.NoError(t, err) room := test.NewRoom(t, alice) diff --git a/userapi/userapi_test.go b/userapi/userapi_test.go index e614765a2..40e37c5d6 100644 --- a/userapi/userapi_test.go +++ b/userapi/userapi_test.go @@ -24,7 +24,6 @@ import ( "github.com/gorilla/mux" "github.com/matrix-org/dendrite/internal/httputil" - internalTest "github.com/matrix-org/dendrite/internal/test" "github.com/matrix-org/dendrite/test" "github.com/matrix-org/dendrite/userapi" "github.com/matrix-org/dendrite/userapi/inthttp" @@ -135,7 +134,7 @@ func TestQueryProfile(t *testing.T) { t.Run("HTTP API", func(t *testing.T) { router := mux.NewRouter().PathPrefix(httputil.InternalPathPrefix).Subrouter() userapi.AddInternalRoutes(router, userAPI) - apiURL, cancel := internalTest.ListenAndServe(t, router, false) + apiURL, cancel := test.ListenAndServe(t, router, false) defer cancel() httpAPI, err := inthttp.NewUserAPIClient(apiURL, &http.Client{}) if err != nil { From ac92e047728efc3d50d6dddbe392ca44afd63a38 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Tue, 17 May 2022 13:31:48 +0100 Subject: [PATCH 100/103] Remove debug logging --- federationapi/consumers/keychange.go | 5 +---- federationapi/consumers/roomserver.go | 2 -- federationapi/storage/postgres/joined_hosts_table.go | 5 ----- 3 files changed, 1 insertion(+), 11 deletions(-) diff --git a/federationapi/consumers/keychange.go b/federationapi/consumers/keychange.go index 95c9a7fdd..6d3cf0e46 100644 --- a/federationapi/consumers/keychange.go +++ b/federationapi/consumers/keychange.go @@ -120,7 +120,7 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool { logger.WithError(err).Error("failed to calculate joined rooms for user") return true } - logrus.Infof("DEBUG: %v joined rooms for user %v", queryRes.RoomIDs, m.UserID) + // 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 { @@ -129,9 +129,6 @@ func (t *KeyChangeConsumer) onDeviceKeyMessage(m api.DeviceMessage) bool { } if len(destinations) == 0 { - logger.WithField("num_rooms", len(queryRes.RoomIDs)).Debug("user is in no federated rooms") - destinations, err = t.db.GetJoinedHostsForRooms(t.ctx, queryRes.RoomIDs, false) - logrus.Infof("GetJoinedHostsForRooms exclude self=false -> %v %v", destinations, err) return true } // Pack the EDU and marshal it diff --git a/federationapi/consumers/roomserver.go b/federationapi/consumers/roomserver.go index 7a0816ff2..e50ec66ad 100644 --- a/federationapi/consumers/roomserver.go +++ b/federationapi/consumers/roomserver.go @@ -21,7 +21,6 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/nats-io/nats.go" - "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus" "github.com/matrix-org/dendrite/federationapi/queue" @@ -166,7 +165,6 @@ func (s *OutputRoomEventConsumer) processMessage(ore api.OutputNewRoomEvent, rew // expressed as a delta against the current state. // TODO(#290): handle EventIDMismatchError and recover the current state by // talking to the roomserver - logrus.Infof("room %s adds joined hosts: %v removes %v", ore.Event.RoomID(), addsJoinedHosts, ore.RemovesStateEventIDs) oldJoinedHosts, err := s.db.UpdateRoom( s.ctx, ore.Event.RoomID(), diff --git a/federationapi/storage/postgres/joined_hosts_table.go b/federationapi/storage/postgres/joined_hosts_table.go index bb6f6bfa3..5c95b72a8 100644 --- a/federationapi/storage/postgres/joined_hosts_table.go +++ b/federationapi/storage/postgres/joined_hosts_table.go @@ -24,7 +24,6 @@ import ( "github.com/matrix-org/dendrite/internal" "github.com/matrix-org/dendrite/internal/sqlutil" "github.com/matrix-org/gomatrixserverlib" - "github.com/sirupsen/logrus" ) const joinedHostsSchema = ` @@ -112,7 +111,6 @@ func (s *joinedHostsStatements) InsertJoinedHosts( roomID, eventID string, serverName gomatrixserverlib.ServerName, ) error { - logrus.Debugf("FederationJoinedHosts: INSERT %v %v %v", roomID, eventID, serverName) stmt := sqlutil.TxStmt(txn, s.insertJoinedHostsStmt) _, err := stmt.ExecContext(ctx, roomID, eventID, serverName) return err @@ -121,7 +119,6 @@ func (s *joinedHostsStatements) InsertJoinedHosts( func (s *joinedHostsStatements) DeleteJoinedHosts( ctx context.Context, txn *sql.Tx, eventIDs []string, ) error { - logrus.Debugf("FederationJoinedHosts: DELETE WITH EVENTS %v", eventIDs) stmt := sqlutil.TxStmt(txn, s.deleteJoinedHostsStmt) _, err := stmt.ExecContext(ctx, pq.StringArray(eventIDs)) return err @@ -130,7 +127,6 @@ func (s *joinedHostsStatements) DeleteJoinedHosts( func (s *joinedHostsStatements) DeleteJoinedHostsForRoom( ctx context.Context, txn *sql.Tx, roomID string, ) error { - logrus.Debugf("FederationJoinedHosts: DELETE ALL IN ROOM %v", roomID) stmt := sqlutil.TxStmt(txn, s.deleteJoinedHostsForRoomStmt) _, err := stmt.ExecContext(ctx, roomID) return err @@ -211,7 +207,6 @@ func joinedHostsFromStmt( ServerName: gomatrixserverlib.ServerName(serverName), }) } - logrus.Debugf("FederationJoinedHosts: SELECT %v => %+v", roomID, result) return result, rows.Err() } From b3162755a9053bbb30a83f00928ff0a0852ad32e Mon Sep 17 00:00:00 2001 From: kegsay Date: Tue, 17 May 2022 15:53:08 +0100 Subject: [PATCH 101/103] bugfix: fix race condition when updating presence via /sync (#2470) * bugfix: fix race condition when updating presence via /sync Previously when presence is updated via /sync, we would send the presence update asyncly via NATS. This created a race condition: - If the presence update is processed quickly, the /sync which triggered the presence update would see an online presence. - If the presence update was processed slowly, the /sync which triggered the presence update would see an offline presence. This is the root cause behind the flakey sytest: 'User sees their own presence in a sync'. The fix is to ensure we update the database/advance the stream position synchronously for local users. * Bugfix for test --- syncapi/consumers/presence.go | 25 +++++++++------ syncapi/sync/requestpool.go | 15 ++++++++- syncapi/sync/requestpool_test.go | 8 +++++ syncapi/syncapi.go | 20 ++++++------ syncapi/syncapi_test.go | 55 ++++++++++++++++++++++++++++++++ 5 files changed, 103 insertions(+), 20 deletions(-) diff --git a/syncapi/consumers/presence.go b/syncapi/consumers/presence.go index 388c08ff4..bfd72d604 100644 --- a/syncapi/consumers/presence.go +++ b/syncapi/consumers/presence.go @@ -138,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 @@ -151,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/sync/requestpool.go b/syncapi/sync/requestpool.go index fdf46cdde..ad151f70b 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -53,19 +53,24 @@ type RequestPool struct { 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.SyncUserAPI, keyAPI keyapi.SyncKeyAPI, rsAPI roomserverAPI.SyncRoomserverAPI, streams *streams.Streams, notifier *notifier.Notifier, - producer PresencePublisher, enableMetrics bool, + producer PresencePublisher, consumer PresenceConsumer, enableMetrics bool, ) *RequestPool { if enableMetrics { prometheus.MustRegister( @@ -83,6 +88,7 @@ func NewRequestPool( streams: streams, Notifier: notifier, producer: producer, + consumer: consumer, } go rp.cleanLastSeen() go rp.cleanPresence(db, time.Minute*5) @@ -160,6 +166,13 @@ func (rp *RequestPool) updatePresence(db storage.Presence, presence string, user 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) { diff --git a/syncapi/sync/requestpool_test.go b/syncapi/sync/requestpool_test.go index 5e52bc7c9..0c7209521 100644 --- a/syncapi/sync/requestpool_test.go +++ b/syncapi/sync/requestpool_test.go @@ -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 d8bacb2da..92db18d56 100644 --- a/syncapi/syncapi.go +++ b/syncapi/syncapi.go @@ -64,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, base.EnableMetrics) + 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, @@ -131,15 +140,6 @@ func AddPublicRoutes( logrus.WithError(err).Panicf("failed to start receipts consumer") } - presenceConsumer := consumers.NewPresenceConsumer( - base.ProcessContext, cfg, js, natsClient, syncDB, - notifier, streams.PresenceStreamProvider, - userAPI, - ) - if err = presenceConsumer.Start(); err != nil { - logrus.WithError(err).Panicf("failed to start presence consumer") - } - routing.Setup( base.PublicClientAPIMux, requestPool, syncDB, userAPI, rsAPI, cfg, base.Caches, diff --git a/syncapi/syncapi_test.go b/syncapi/syncapi_test.go index 5ecfd8772..3ce7c64b7 100644 --- a/syncapi/syncapi_test.go +++ b/syncapi/syncapi_test.go @@ -19,6 +19,7 @@ import ( 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 { @@ -256,6 +257,60 @@ func testSyncAPICreateRoomSyncEarly(t *testing.T, dbType test.DBType) { } } +// 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 { From f321a7d55ea75e6a5276cd88eddcbbc82ceeaaeb Mon Sep 17 00:00:00 2001 From: Till <2353100+S7evinK@users.noreply.github.com> Date: Wed, 18 May 2022 15:17:23 +0200 Subject: [PATCH 102/103] Really SKIP_NODB (#2472) * Really SKIP_NODB * Use fatalError in createLocalDB * Check if createdb exists * Revert change * Remove !Quiet --- test/db.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/db.go b/test/db.go index a1754cd08..c7cb919f6 100644 --- a/test/db.go +++ b/test/db.go @@ -44,8 +44,9 @@ func fatalError(t *testing.T, format string, args ...interface{}) { } func createLocalDB(t *testing.T, dbName string) { - if !Quiet { - t.Log("Note: tests require a postgres install accessible to the current user") + 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 { @@ -63,6 +64,9 @@ func createRemoteDB(t *testing.T, dbName, user, connStr string) { if err != nil { 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 { pqErr, ok := err.(*pq.Error) From 21dd5a7176e52d018b91854db273424e4430af7b Mon Sep 17 00:00:00 2001 From: kegsay Date: Thu, 19 May 2022 09:00:56 +0100 Subject: [PATCH 103/103] syncapi: don't return early for no-op incremental syncs (#2473) * syncapi: don't return early for no-op incremental syncs Comments explain why, but basically it's an inefficient use of bandwidth and some sytests rely on /sync to block. * Honour timeouts * Actually return a response with timeout=0 --- syncapi/sync/requestpool.go | 246 ++++++++++++++++++++---------------- syncapi/types/types.go | 13 ++ 2 files changed, 149 insertions(+), 110 deletions(-) diff --git a/syncapi/sync/requestpool.go b/syncapi/sync/requestpool.go index ad151f70b..7b9526b53 100644 --- a/syncapi/sync/requestpool.go +++ b/syncapi/sync/requestpool.go @@ -251,125 +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.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") + 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, } } - 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/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{}