diff --git a/federation/server.go b/federation/server.go index ba7478de..3e0ed3cf 100644 --- a/federation/server.go +++ b/federation/server.go @@ -180,18 +180,35 @@ func (s *Server) MustMakeRoom(t ct.TestLike, roomVer gomatrixserverlib.RoomVersi if !s.listening { ct.Fatalf(s.t, "MustMakeRoom() called before Listen() - this is not supported because Listen() chooses a high-numbered port and thus changes the server name and thus changes the room ID. Ensure you Listen() first!") } + // Generate a unique room ID, prefixed with an incrementing counter. // This ensures that room IDs are not re-used across tests, even if a Complement server happens // to re-use the same port as a previous one, which // * reduces noise when searching through logs and // * prevents homeservers from getting confused when multiple test cases re-use the same homeserver deployment. + // This value is temporary for domainless room IDs and will be replaced with the create event ID. roomID := fmt.Sprintf("!%d-%s:%s", len(s.rooms), util.RandomString(18), s.serverName) - t.Logf("Creating room %s with version %s", roomID, roomVer) room := NewServerRoom(roomVer, roomID) for _, opt := range opts { + // let the caller replace the room impl before we try to create events opt(room) } + iRoomVer := gomatrixserverlib.MustGetRoomVersion(roomVer) + if iRoomVer.DomainlessRoomIDs() { + if len(events) == 0 || events[0].Type != spec.MRoomCreate { + ct.Fatalf(s.t, "MustMakeRoom: room version %s requires the create event as an initial event but it wasn't found", roomVer) + } + room.RoomID = "" + // build and sign the create event to work out the room ID + createEvent := s.MustCreateEvent(t, room, events[0]) + events = events[1:] + room.RoomID = "!" + createEvent.EventID()[1:] + room.AddEvent(createEvent) + } + + t.Logf("Creating room %s with version %s", room.RoomID, roomVer) + // sign all these events for _, ev := range events { signedEvent := s.MustCreateEvent(t, room, ev) diff --git a/federation/server_room.go b/federation/server_room.go index 5401a234..61c40ed3 100644 --- a/federation/server_room.go +++ b/federation/server_room.go @@ -10,6 +10,7 @@ import ( "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" "github.com/matrix-org/complement/b" "github.com/matrix-org/complement/ct" @@ -302,7 +303,7 @@ func (r *ServerRoom) GetEventInTimeline(eventID string) (gomatrixserverlib.PDU, return nil, false } -func initialPowerLevelsContent(roomCreator string) (c gomatrixserverlib.PowerLevelContent) { +func initialPowerLevelsContent(ver gomatrixserverlib.IRoomVersion, roomCreator string) (c gomatrixserverlib.PowerLevelContent) { c.Defaults() c.Events = map[string]int64{ "m.room.name": 50, @@ -312,14 +313,18 @@ func initialPowerLevelsContent(roomCreator string) (c gomatrixserverlib.PowerLev "m.room.avatar": 50, "m.room.aliases": 0, // anyone can publish aliases by default. Has to be 0 else state_default is used. } - c.Users = map[string]int64{roomCreator: 100} + if ver.PrivilegedCreators() { + c.Users = map[string]int64{} + } else { + c.Users = map[string]int64{roomCreator: 100} + } return c } // InitialRoomEvents returns the initial set of events that get created when making a room. func InitialRoomEvents(roomVer gomatrixserverlib.RoomVersion, creator string) []Event { // need to serialise/deserialise to get map[string]interface{} annoyingly - plContent := initialPowerLevelsContent(creator) + plContent := initialPowerLevelsContent(gomatrixserverlib.MustGetRoomVersion(roomVer), creator) plBytes, _ := json.Marshal(plContent) var plContentMap map[string]interface{} json.Unmarshal(plBytes, &plContentMap) @@ -331,6 +336,11 @@ func InitialRoomEvents(roomVer gomatrixserverlib.RoomVersion, creator string) [] Content: map[string]interface{}{ "creator": creator, "room_version": roomVer, + // We have to add randomness to the create event, else if you create 2x v12+ rooms in the same millisecond + // they will get the same room ID, clobbering internal data structures and causing extremely confusing + // behaviour. By adding this entropy, we ensure that even if rooms are created in the same millisecond, their + // hashes will not be the same. + "complement_entropy": util.RandomString(18), }, }, { @@ -441,12 +451,15 @@ func (i *ServerRoomImplDefault) ProtoEventCreator(room *ServerRoom, ev Event) (* PrevEvents: prevEvents, AuthEvents: ev.AuthEvents, Redacts: ev.Redacts, + Version: gomatrixserverlib.MustGetRoomVersion(room.Version), } if err := proto.SetContent(ev.Content); err != nil { return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) } - if err := proto.SetUnsigned(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + if len(ev.Unsigned) > 0 { + if err := proto.SetUnsigned(ev.Unsigned); err != nil { + return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) + } } if proto.AuthEvents == nil { var stateNeeded gomatrixserverlib.StateNeeded @@ -454,6 +467,9 @@ func (i *ServerRoomImplDefault) ProtoEventCreator(room *ServerRoom, ev Event) (* if err != nil { return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) } + if proto.Version.DomainlessRoomIDs() { + stateNeeded.Create = false + } proto.AuthEvents = room.AuthEvents(stateNeeded) } return &proto, nil diff --git a/tests/csapi/media_async_uploads_test.go b/tests/csapi/media_async_uploads_test.go index 1dc8b42f..49ca8252 100644 --- a/tests/csapi/media_async_uploads_test.go +++ b/tests/csapi/media_async_uploads_test.go @@ -8,6 +8,7 @@ import ( "github.com/matrix-org/complement" "github.com/matrix-org/complement/client" + "github.com/matrix-org/complement/ct" "github.com/matrix-org/complement/helpers" "github.com/matrix-org/complement/internal/data" "github.com/matrix-org/complement/match" @@ -15,6 +16,8 @@ import ( "github.com/matrix-org/complement/runtime" ) +const pngContentType = "image/png" + func TestAsyncUpload(t *testing.T) { runtime.SkipIf(t, runtime.Dendrite) // Dendrite doesn't support async uploads @@ -23,61 +26,91 @@ func TestAsyncUpload(t *testing.T) { alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) - var mxcURI, mediaID string t.Run("Create media", func(t *testing.T) { - mxcURI = alice.CreateMedia(t) - parts := strings.Split(mxcURI, "/") - mediaID = parts[len(parts)-1] + alice.CreateMedia(t) }) - origin, mediaID := client.SplitMxc(mxcURI) - t.Run("Not yet uploaded", func(t *testing.T) { + mxcURI := alice.CreateMedia(t) + parts := strings.Split(mxcURI, "/") + mediaID := parts[len(parts)-1] + origin, mediaID := client.SplitMxc(mxcURI) + // Check that the media is not yet uploaded res := alice.Do(t, "GET", []string{"_matrix", "media", "v3", "download", origin, mediaID}) must.MatchResponse(t, res, match.HTTPResponse{ StatusCode: http.StatusGatewayTimeout, JSON: []match.JSON{ match.JSONKeyEqual("errcode", "M_NOT_YET_UPLOADED"), - match.JSONKeyEqual("error", "Media has not been uploaded yet"), + match.JSONKeyPresent("error"), }, }) }) - wantContentType := "image/png" - t.Run("Upload media", func(t *testing.T) { - alice.UploadMediaAsync(t, origin, mediaID, data.MatrixPng, "test.png", wantContentType) + _ = asyncUploadMedia(t, alice) }) t.Run("Cannot upload to a media ID that has already been uploaded to", func(t *testing.T) { + // First upload some media that we can conflict with + mxcURI := asyncUploadMedia(t, alice) + parts := strings.Split(mxcURI, "/") + mediaID := parts[len(parts)-1] + origin, mediaID := client.SplitMxc(mxcURI) + + // Then try upload again using the same `mediaID` res := alice.Do(t, "PUT", []string{"_matrix", "media", "v3", "upload", origin, mediaID}) must.MatchResponse(t, res, match.HTTPResponse{ StatusCode: http.StatusConflict, JSON: []match.JSON{ match.JSONKeyEqual("errcode", "M_CANNOT_OVERWRITE_MEDIA"), - match.JSONKeyEqual("error", "Media ID already has content"), + match.JSONKeyPresent("error"), }, }) }) + // TODO: This is the same as the test below (both use authenticated media). Previously + // this was testing unauthenticated vs authenticated media. We should resolve this by + // removing one of these tests or ideally, keeping both authenticated and + // unauthenticated tests and just gate it behind some homeserver check for + // unauthenticated media support (see + // https://github.com/matrix-org/complement/pull/746#discussion_r2718904066) t.Run("Download media", func(t *testing.T) { + mxcURI := asyncUploadMedia(t, alice) + content, contentType := alice.DownloadContentAuthenticated(t, mxcURI) if !bytes.Equal(data.MatrixPng, content) { t.Fatalf("uploaded and downloaded content doesn't match: want %v\ngot\n%v", data.MatrixPng, content) } - if contentType != wantContentType { - t.Fatalf("expected contentType to be %s, got %s", wantContentType, contentType) + if contentType != pngContentType { + t.Fatalf("expected contentType to be %s, got %s", pngContentType, contentType) } }) t.Run("Download media over _matrix/client/v1/media/download", func(t *testing.T) { + mxcURI := asyncUploadMedia(t, alice) + content, contentType := alice.DownloadContentAuthenticated(t, mxcURI) if !bytes.Equal(data.MatrixPng, content) { t.Fatalf("uploaded and downloaded content doesn't match: want %v\ngot\n%v", data.MatrixPng, content) } - if contentType != wantContentType { - t.Fatalf("expected contentType to be %s, got %s", wantContentType, contentType) + if contentType != pngContentType { + t.Fatalf("expected contentType to be %s, got %s", pngContentType, contentType) } }) } + +func asyncUploadMedia( + t ct.TestLike, + matrixClient *client.CSAPI, +) string { + t.Helper() + + mxcURI := matrixClient.CreateMedia(t) + parts := strings.Split(mxcURI, "/") + mediaID := parts[len(parts)-1] + origin, mediaID := client.SplitMxc(mxcURI) + matrixClient.UploadMediaAsync(t, origin, mediaID, data.MatrixPng, "test.png", pngContentType) + + return mxcURI +} diff --git a/tests/v12_test.go b/tests/v12_test.go index 70c86f53..6139bc6f 100644 --- a/tests/v12_test.go +++ b/tests/v12_test.go @@ -29,55 +29,6 @@ var maxCanonicalJSONInt = math.Pow(2, 53) - 1 const roomVersion12 = "12" -var V12ServerRoom = federation.ServerRoomImplCustom{ - ProtoEventCreatorFn: Protov12EventCreator, -} - -// Override how Complement makes proto events so we can conditionally disable/enable the inclusion of the create event -// depending on whether we're running in combined mode or not. -// Complement also doesn't set the room version correctly on the ProtoEvent as this was a new addition to GMSL. -func Protov12EventCreator(def federation.ServerRoomImpl, room *federation.ServerRoom, ev federation.Event) (*gomatrixserverlib.ProtoEvent, error) { - var prevEvents interface{} - if ev.PrevEvents != nil { - // We deliberately want to set the prev events. - prevEvents = ev.PrevEvents - } else { - // No other prev events were supplied so we'll just - // use the forward extremities of the room, which is - // the usual behaviour. - prevEvents = room.ForwardExtremities - } - proto := gomatrixserverlib.ProtoEvent{ - SenderID: ev.Sender, - Depth: int64(room.Depth + 1), // depth starts at 1 - Type: ev.Type, - StateKey: ev.StateKey, - RoomID: room.RoomID, - PrevEvents: prevEvents, - AuthEvents: ev.AuthEvents, - Redacts: ev.Redacts, - Version: gomatrixserverlib.MustGetRoomVersion(room.Version), - } - if err := proto.SetContent(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event content: %s - %+v", err, ev.Content) - } - if err := proto.SetUnsigned(ev.Content); err != nil { - return nil, fmt.Errorf("EventCreator: failed to marshal event unsigned: %s - %+v", err, ev.Unsigned) - } - if proto.AuthEvents == nil { - var stateNeeded gomatrixserverlib.StateNeeded - // this does the right thing for v12 - stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto) - if err != nil { - return nil, fmt.Errorf("EventCreator: failed to work out auth_events : %s", err) - } - // we never include the create event if the HS supports MSC4291 - stateNeeded.Create = false - proto.AuthEvents = room.AuthEvents(stateNeeded) - } - return &proto, nil -} - // Test that the creator can kick an admin created both via // trusted_private_chat and by explicit promotion, including beyond PL100. // Also checks the creator isn't in the PL event. @@ -246,7 +197,7 @@ func TestMSC4289PrivilegedRoomCreators(t *testing.T) { "room_version": roomVersion12, "preset": "public_chat", }) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob) plEventID := alice.SendEventSynced(t, roomID, b.Event{ Type: spec.MRoomPowerLevels, StateKey: b.Ptr(""), @@ -663,6 +614,36 @@ func TestMSC4291RoomIDAsHashOfCreateEvent(t *testing.T) { assertCreateEventIsRoomID(t, alice, roomID) } +func TestComplementCanCreateValidV12Rooms(t *testing.T) { + deployment := complement.Deploy(t, 1) + defer deployment.Destroy(t) + alice := deployment.Register(t, "hs1", helpers.RegistrationOpts{}) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + federation.HandleMakeSendJoinRequests(), + federation.HandleTransactionRequests(nil, nil), + federation.HandleEventRequests(), + ) + srv.UnexpectedRequestsAreErrors = false + cancel := srv.Listen() + defer cancel() + bob := srv.UserID("bob") + srvRoom := srv.MustMakeRoom(t, roomVersion12, federation.InitialRoomEvents(roomVersion12, bob)) + alice.MustJoinRoom(t, srvRoom.RoomID, []spec.ServerName{srv.ServerName()}) + + msg := srv.MustCreateEvent(t, srvRoom, federation.Event{ + Type: "m.room.message", + Sender: bob, + Content: map[string]interface{}{ + "msgtype": "m.text", + "body": "Hello world", + }, + }) + srvRoom.AddEvent(msg) + srv.MustSendTransaction(t, deployment, "hs1", []json.RawMessage{msg.JSON()}, nil) + alice.MustSyncUntil(t, client.SyncReq{}, client.SyncTimelineHasEventID(srvRoom.RoomID, msg.EventID())) +} + func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing.T) { deployment := complement.Deploy(t, 1) defer deployment.Destroy(t) @@ -683,7 +664,7 @@ func TestMSC4291RoomIDAsHashOfCreateEvent_AuthEventsOmitsCreateEvent(t *testing. defer cancel() bob := srv.UserID("bob") - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, bob) createEvent := room.CurrentState(spec.MRoomCreate, "") if createEvent == nil { @@ -954,7 +935,7 @@ func TestMSC4297StateResolutionV2_1_starts_from_empty_set(t *testing.T) { "preset": "public_chat", }) bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie) joinRulePublic := room.CurrentState(spec.MRoomJoinRules, "") aliceJoin := room.CurrentState(spec.MRoomMember, alice.UserID) synchronisationEventID := bob.SendEventSynced(t, room.RoomID, b.Event{ @@ -1150,7 +1131,7 @@ func TestMSC4297StateResolutionV2_1_includes_conflicted_subgraph(t *testing.T) { }) alice.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) bob.MustJoinRoom(t, roomID, []spec.ServerName{"hs1"}) - room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie, federation.WithRoomOpts(federation.WithImpl(&V12ServerRoom))) + room := srv.MustJoinRoom(t, deployment, "hs1", roomID, charlie) firstPowerLevelEvent := room.CurrentState(spec.MRoomPowerLevels, "") alice.SendEventSynced(t, roomID, b.Event{ Type: spec.MRoomPowerLevels,