From a443d1e5f3796942f68067741f4bdd482548bfd7 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 9 May 2022 16:25:22 +0100 Subject: [PATCH 01/30] 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 02/30] 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 03/30] 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 04/30] =?UTF-8?q?=F0=9F=8F=97=EF=B8=8F=20mediaapi/thumbnai?= =?UTF-8?q?ler:=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 05/30] 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 06/30] 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 07/30] 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 08/30] 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 09/30] 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 10/30] 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 11/30] 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 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

{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 12/30] 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 13/30] 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 14/30] 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 15/30] 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 16/30] 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 17/30] 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 18/30] 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 19/30] 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 20/30] 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 21/30] 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 22/30] 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 23/30] 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 24/30] 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 25/30] 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 26/30] 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 27/30] 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 28/30] 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 29/30] 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 30/30] 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{}