Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion federation/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
26 changes: 21 additions & 5 deletions federation/server_room.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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),
},
},
{
Expand Down Expand Up @@ -441,19 +451,25 @@ 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
stateNeeded, err := gomatrixserverlib.StateNeededForProtoEvent(&proto)
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
Expand Down
63 changes: 48 additions & 15 deletions tests/csapi/media_async_uploads_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@ 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"
"github.com/matrix-org/complement/must"
"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

Expand All @@ -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
}
87 changes: 34 additions & 53 deletions tests/v12_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(""),
Expand Down Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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,
Expand Down
Loading