diff --git a/clientapi/routing/upgrade_room.go b/clientapi/routing/upgrade_room.go index 8d29b360a..eea51733a 100644 --- a/clientapi/routing/upgrade_room.go +++ b/clientapi/routing/upgrade_room.go @@ -15,22 +15,17 @@ package routing import ( - "encoding/json" - "fmt" "net/http" - "time" appserviceAPI "github.com/matrix-org/dendrite/appservice/api" "github.com/matrix-org/dendrite/clientapi/httputil" "github.com/matrix-org/dendrite/clientapi/jsonerror" - "github.com/matrix-org/dendrite/internal/eventutil" roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/version" "github.com/matrix-org/dendrite/setup/config" userapi "github.com/matrix-org/dendrite/userapi/api" "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" - "github.com/sirupsen/logrus" ) type upgradeRoomRequest struct { @@ -42,7 +37,6 @@ type upgradeRoomResponse struct { } // UpgradeRoom implements /upgrade -// nolint: gocyclo func UpgradeRoom( req *http.Request, device *userapi.Device, cfg *config.ClientAPI, @@ -50,699 +44,49 @@ func UpgradeRoom( rsAPI roomserverAPI.RoomserverInternalAPI, asAPI appserviceAPI.AppServiceQueryAPI, ) util.JSONResponse { - evTime, err := httputil.ParseTSParam(req) - if err != nil { - return util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.InvalidArgumentValue(err.Error()), - } - } - var r upgradeRoomRequest if rErr := httputil.UnmarshalJSONRequest(req, &r); rErr != nil { return *rErr } // Validate that the room version is supported - if _, err = version.SupportedRoomVersion(gomatrixserverlib.RoomVersion(r.NewVersion)); err != nil { + if _, err := version.SupportedRoomVersion(gomatrixserverlib.RoomVersion(r.NewVersion)); err != nil { return util.JSONResponse{ Code: http.StatusBadRequest, JSON: jsonerror.UnsupportedRoomVersion("This server does not support that room version"), } } - // Return an immediate error if the room does not exist - verReq := roomserverAPI.QueryRoomVersionForRoomRequest{RoomID: roomID} - verRes := roomserverAPI.QueryRoomVersionForRoomResponse{} - if err = rsAPI.QueryRoomVersionForRoom(req.Context(), &verReq, &verRes); err != nil { - return util.JSONResponse{ - Code: http.StatusNotFound, - JSON: jsonerror.NotFound("Room does not exist"), - } + upgradeReq := roomserverAPI.PerformRoomUpgradeRequest{ + UserID: device.UserID, + RoomID: roomID, + RoomVersion: gomatrixserverlib.RoomVersion(r.NewVersion), } + upgradeResp := roomserverAPI.PerformRoomUpgradeResponse{} - // 1. Check if the user is authorized to actually perform the upgrade (can send m.room.tombstone) - if rErr := userIsAuthorized(req, device, roomID, rsAPI); rErr != nil { - return *rErr - } + rsAPI.PerformRoomUpgrade(req.Context(), &upgradeReq, &upgradeResp) - oldCreateEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomCreate, - StateKey: "", - }) - oldPowerLevelsEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomPowerLevels, - StateKey: "", - }) - oldJoinRulesEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomJoinRules, - StateKey: "", - }) - oldHistoryVisibilityEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomHistoryVisibility, - StateKey: "", - }) - oldNameEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomName, - StateKey: "", - }) - oldTopicEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomTopic, - StateKey: "", - }) - oldGuestAccessEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomGuestAccess, - StateKey: "", - }) - oldAvatarEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomAvatar, - StateKey: "", - }) - oldEncryptionEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomEncryption, - StateKey: "", - }) - oldServerAclEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: "m.room.server_acl", - StateKey: "", - }) - // Not in the spec, but needed for sytest compatablility - oldRelatedGroupsEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: "m.room.related_groups", - StateKey: "", - }) - oldCanonicalAliasEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomCanonicalAlias, - StateKey: "", - }) - - // TODO (#267): Check room ID doesn't clash with an existing one, and we - // probably shouldn't be using pseudo-random strings, maybe GUIDs? - newRoomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.Matrix.ServerName) - userID := device.UserID - profile, err := appserviceAPI.RetrieveUserProfile(req.Context(), userID, asAPI, profileAPI) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed") - return jsonerror.InternalServerError() - } - - // Make the tombstone event - tombstoneEvent, resErr := makeTombstoneEvent(req, device, cfg, evTime, roomID, newRoomID, rsAPI) - if err != nil { - return *resErr - } - - newCreateContent := map[string]interface{}{ - "creator": userID, - "room_version": r.NewVersion, // TODO: change struct to single var? - "predecessor": gomatrixserverlib.PreviousRoom{ - EventID: tombstoneEvent.EventID(), - RoomID: roomID, - }, - } - oldCreateContent := unmarshal(oldCreateEvent.Content()) - if federate, ok := oldCreateContent["m.federate"].(bool); ok { - newCreateContent["m.federate"] = federate - } - - newCreateEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomCreate, - Content: newCreateContent, - } - - membershipEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomMember, - StateKey: userID, - Content: gomatrixserverlib.MemberContent{ - Membership: gomatrixserverlib.Join, - DisplayName: profile.DisplayName, - AvatarURL: profile.AvatarURL, - }, - } - - powerLevelContent, err := oldPowerLevelsEvent.PowerLevels() - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("powerLevel event was not actually a power level event") - return jsonerror.InternalServerError() - } - - newPowerLevelsEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomPowerLevels, - Content: powerLevelContent, - } - - //create temporary power level event that elevates upgrading user's prvileges to create every copied state event - tempPowerLevelsEvent := createTemporaryPowerLevels(powerLevelContent, userID) - - joinRulesContent, err := oldJoinRulesEvent.JoinRule() - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("Join rules event had bad content") - return jsonerror.InternalServerError() - } - newJoinRulesEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomJoinRules, - Content: map[string]interface{}{ - "join_rule": joinRulesContent, - }, - } - - historyVisibilityContent, err := oldHistoryVisibilityEvent.HistoryVisibility() - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("History visibility event had bad content") - return jsonerror.InternalServerError() - } - newHistoryVisibilityEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomHistoryVisibility, - Content: map[string]interface{}{ - "history_visibility": historyVisibilityContent, - }, - } - - var newNameEvent fledglingEvent - var newTopicEvent fledglingEvent - var newGuestAccessEvent fledglingEvent - var newAvatarEvent fledglingEvent - var newEncryptionEvent fledglingEvent - var newServerACLEvent fledglingEvent - var newRelatedGroupsEvent fledglingEvent - var newCanonicalAliasEvent fledglingEvent - - if oldNameEvent != nil { - newNameEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomName, - Content: unmarshal(oldNameEvent.Content()), - } - } - if oldTopicEvent != nil { - newTopicEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomTopic, - Content: unmarshal(oldTopicEvent.Content()), - } - } - if oldGuestAccessEvent != nil { - newGuestAccessEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomGuestAccess, - Content: unmarshal(oldGuestAccessEvent.Content()), - } - } - if oldAvatarEvent != nil { - newAvatarEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomAvatar, - Content: unmarshal(oldAvatarEvent.Content()), - } - } - if oldEncryptionEvent != nil { - newEncryptionEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomEncryption, - Content: unmarshal(oldEncryptionEvent.Content()), - } - } - if oldServerAclEvent != nil { - newServerACLEvent = fledglingEvent{ - Type: "m.room.server_acl", - Content: unmarshal(oldServerAclEvent.Content()), - } - } - if oldRelatedGroupsEvent != nil { - newRelatedGroupsEvent = fledglingEvent{ - Type: "m.room.related_groups", - Content: unmarshal(oldRelatedGroupsEvent.Content()), - } - } - if oldCanonicalAliasEvent != nil { - newCanonicalAliasEvent = fledglingEvent{ - Type: gomatrixserverlib.MRoomCanonicalAlias, - Content: unmarshal(oldCanonicalAliasEvent.Content()), - } - } - - // 3. Replicate transferable state events - // send events into the room in order of: - // 1- m.room.create - // 2- m.room.power_levels (temporary, to allow the upgrading user to send everything) - // 3- m.room.join_rules - // 4- m.room.history_visibility - // 5- m.room.guest_access - // 6- m.room.name - // 7- m.room.avatar - // 8- m.room.topic - // 9- m.room.encryption - // 10-m.room.server_acl - // 11-m.room.related_groups - // 12-m.room.canonical_alias - // 13-All ban events from the old room - // 14-The original room power levels - eventsToMake := []fledglingEvent{ - newCreateEvent, membershipEvent, tempPowerLevelsEvent, newJoinRulesEvent, newHistoryVisibilityEvent, - } - if oldGuestAccessEvent != nil { - eventsToMake = append(eventsToMake, newGuestAccessEvent) - } else { // Always create this with the default value to appease sytests - eventsToMake = append(eventsToMake, fledglingEvent{ - Type: gomatrixserverlib.MRoomGuestAccess, - Content: map[string]interface{}{"guest_access": "forbidden"}, - }) - } - if oldNameEvent != nil { - eventsToMake = append(eventsToMake, newNameEvent) - } - if oldAvatarEvent != nil { - eventsToMake = append(eventsToMake, newAvatarEvent) - } - if oldTopicEvent != nil { - eventsToMake = append(eventsToMake, newTopicEvent) - } - if oldEncryptionEvent != nil { - eventsToMake = append(eventsToMake, newEncryptionEvent) - } - if oldServerAclEvent != nil { - eventsToMake = append(eventsToMake, newServerACLEvent) - } - if oldRelatedGroupsEvent != nil { - eventsToMake = append(eventsToMake, newRelatedGroupsEvent) - } - if oldCanonicalAliasEvent != nil { - eventsToMake = append(eventsToMake, newCanonicalAliasEvent) - } - banEvents, err := getBanEvents(req, roomID, rsAPI) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("QueryCurrentState failed") - return jsonerror.InternalServerError() - } else { - eventsToMake = append(eventsToMake, banEvents...) - } - eventsToMake = append(eventsToMake, newPowerLevelsEvent) - - // 5. Send the tombstone event to the old room (must do this before we set the new canonical_alias) - resErr = sendHeaderedEvent(req, cfg, rsAPI, tombstoneEvent) - if resErr != nil { - return *resErr - } - - var builtEvents []*gomatrixserverlib.HeaderedEvent - authEvents := gomatrixserverlib.NewAuthEvents(nil) - for i, e := range eventsToMake { - depth := i + 1 // depth starts at 1 - - builder := gomatrixserverlib.EventBuilder{ - Sender: userID, - RoomID: newRoomID, - Type: e.Type, - StateKey: &e.StateKey, - Depth: int64(depth), - } - err = builder.SetContent(e.Content) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed") - return jsonerror.InternalServerError() - } - if i > 0 { - builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()} - } - var event *gomatrixserverlib.Event - event, err = buildEvent(&builder, &authEvents, cfg, evTime, gomatrixserverlib.RoomVersion(r.NewVersion)) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("buildEvent failed") - return jsonerror.InternalServerError() - } - - if err = gomatrixserverlib.Allowed(event, &authEvents); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.Allowed failed") - return jsonerror.InternalServerError() - } - - // Add the event to the list of auth events - builtEvents = append(builtEvents, event.Headered(gomatrixserverlib.RoomVersion(r.NewVersion))) - err = authEvents.AddEvent(event) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("authEvents.AddEvent failed") - return jsonerror.InternalServerError() - } - } - - inputs := make([]roomserverAPI.InputRoomEvent, 0, len(builtEvents)) - for _, event := range builtEvents { - inputs = append(inputs, roomserverAPI.InputRoomEvent{ - Kind: roomserverAPI.KindNew, - Event: event, - Origin: cfg.Matrix.ServerName, - SendAsServer: roomserverAPI.DoNotSendToOtherServers, - }) - } - if err = roomserverAPI.SendInputRoomEvents(req.Context(), rsAPI, inputs, false); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed") - return jsonerror.InternalServerError() - } - - // check if the old room was published - var pubQueryRes roomserverAPI.QueryPublishedRoomsResponse - err = rsAPI.QueryPublishedRooms(req.Context(), &roomserverAPI.QueryPublishedRoomsRequest{ - RoomID: roomID, - }, &pubQueryRes) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("QueryPublishedRooms failed") - return jsonerror.InternalServerError() - } - - // if the old room is published (was public), publish the new room - if len(pubQueryRes.RoomIDs) == 1 { - publishNewRoom(req, rsAPI, roomID, newRoomID) - } - - // Clear the old canonical alias event in the old room - emptyCanonicalAliasEvent, resErr := makeHeaderedEvent(req, device, cfg, evTime, roomID, rsAPI, fledglingEvent{ - Type: gomatrixserverlib.MRoomCanonicalAlias, - Content: map[string]interface{}{}, - }) - if resErr != nil { - if resErr.Code == http.StatusForbidden { - util.GetLogger(req.Context()).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not set empty canonical alias event in old room") + if upgradeResp.Error != nil { + if upgradeResp.Error.Code == roomserverAPI.PerformErrorNoRoom { + return util.JSONResponse{ + Code: http.StatusNotFound, + JSON: jsonerror.NotFound("Room does not exist"), + } + } else if upgradeResp.Error.Code == roomserverAPI.PerformErrorNotAllowed { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: jsonerror.Forbidden(upgradeResp.Error.Msg), + } } else { - return *resErr + return jsonerror.InternalServerError() } - } else { - if resErr = sendHeaderedEvent(req, cfg, rsAPI, emptyCanonicalAliasEvent); resErr != nil { - return *resErr - } - } - // 4. Move local aliases to the new room - if resErr = moveLocalAliases(req, roomID, newRoomID, userID, rsAPI); resErr != nil { - return *resErr - } - - // 6. Restrict power levels in the old room - if resErr = restrictOldRoomPowerLevels(req, device, cfg, evTime, roomID, rsAPI, *powerLevelContent); resErr != nil { - return *resErr } return util.JSONResponse{ - Code: 200, + Code: http.StatusOK, JSON: upgradeRoomResponse{ - ReplacementRoom: newRoomID, + ReplacementRoom: upgradeResp.NewRoomID, }, } } - -func publishNewRoom( - req *http.Request, - rsAPI roomserverAPI.RoomserverInternalAPI, - oldRoomID, newRoomID string, -) { - - // expose this room in the published room list - var pubNewRoomRes roomserverAPI.PerformPublishResponse - rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{ - RoomID: newRoomID, - Visibility: "public", - }, &pubNewRoomRes) - if pubNewRoomRes.Error != nil { - // treat as non-fatal since the room is already made by this point - util.GetLogger(req.Context()).WithError(pubNewRoomRes.Error).Error("failed to visibility:public") - } - - var unpubOldRoomRes roomserverAPI.PerformPublishResponse - // remove the old room from the published room list - rsAPI.PerformPublish(req.Context(), &roomserverAPI.PerformPublishRequest{ - RoomID: oldRoomID, - Visibility: "private", - }, &unpubOldRoomRes) - if unpubOldRoomRes.Error != nil { - // treat as non-fatal since the room is already made by this point - util.GetLogger(req.Context()).WithError(unpubOldRoomRes.Error).Error("failed to visibility:private") - } -} - -func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelContent, userID string) fledglingEvent { - eventPowerLevels := powerLevelContent.Events - stateDefaultPowerLevel := powerLevelContent.StateDefault - neededPowerLevel := stateDefaultPowerLevel - for _, powerLevel := range eventPowerLevels { - if powerLevel > neededPowerLevel { - neededPowerLevel = powerLevel - } - } - - tempPowerLevelContent := &gomatrixserverlib.PowerLevelContent{} - *tempPowerLevelContent = *powerLevelContent - newUserPowerLevels := make(map[string]int64) - for key, value := range powerLevelContent.Users { - newUserPowerLevels[key] = value - } - tempPowerLevelContent.Users = newUserPowerLevels - - if val, ok := tempPowerLevelContent.Users[userID]; ok { - if val < neededPowerLevel { - tempPowerLevelContent.Users[userID] = neededPowerLevel - } - } else { - if tempPowerLevelContent.UsersDefault < val { - tempPowerLevelContent.UsersDefault = neededPowerLevel - } - } - tempPowerLevelsEvent := fledglingEvent{ - Type: gomatrixserverlib.MRoomPowerLevels, - Content: tempPowerLevelContent, - } - return tempPowerLevelsEvent -} - -func getBanEvents(req *http.Request, roomID string, rsAPI roomserverAPI.RoomserverInternalAPI) ([]fledglingEvent, error) { - var err error - banEvents := []fledglingEvent{} - - roomMemberReq := roomserverAPI.QueryCurrentStateRequest{RoomID: roomID, AllowWildcards: true, StateTuples: []gomatrixserverlib.StateKeyTuple{ - {EventType: gomatrixserverlib.MRoomMember, StateKey: "*"}, - }} - roomMemberRes := roomserverAPI.QueryCurrentStateResponse{} - if err = rsAPI.QueryCurrentState(req.Context(), &roomMemberReq, &roomMemberRes); err != nil { - return nil, err - } - for _, event := range roomMemberRes.StateEvents { - if event == nil { - continue - } - memberContent, err := gomatrixserverlib.NewMemberContentFromEvent(event.Event) - if err != nil || memberContent.Membership != gomatrixserverlib.Ban { - continue - } - banEvents = append(banEvents, fledglingEvent{Type: gomatrixserverlib.MRoomMember, StateKey: *event.StateKey(), Content: memberContent}) - } - return banEvents, nil -} - -func moveLocalAliases(req *http.Request, - roomID, newRoomID, userID string, - rsAPI roomserverAPI.RoomserverInternalAPI) *util.JSONResponse { - var err error - internalServerError := jsonerror.InternalServerError() - - aliasReq := roomserverAPI.GetAliasesForRoomIDRequest{RoomID: roomID} - aliasRes := roomserverAPI.GetAliasesForRoomIDResponse{} - if err = rsAPI.GetAliasesForRoomID(req.Context(), &aliasReq, &aliasRes); err != nil { - return &internalServerError - } - - for _, alias := range aliasRes.Aliases { - removeAliasReq := roomserverAPI.RemoveRoomAliasRequest{UserID: userID, Alias: alias} - removeAliasRes := roomserverAPI.RemoveRoomAliasResponse{} - if err = rsAPI.RemoveRoomAlias(req.Context(), &removeAliasReq, &removeAliasRes); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.RemoveRoomAlias failed") - return &internalServerError - } - - setAliasReq := roomserverAPI.SetRoomAliasRequest{UserID: userID, Alias: alias, RoomID: newRoomID} - setAliasRes := roomserverAPI.SetRoomAliasResponse{} - if err = rsAPI.SetRoomAlias(req.Context(), &setAliasReq, &setAliasRes); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SetRoomAlias failed") - return &internalServerError - } - } - return nil -} - -func restrictOldRoomPowerLevels(req *http.Request, device *userapi.Device, - cfg *config.ClientAPI, evTime time.Time, - roomID string, - rsAPI roomserverAPI.RoomserverInternalAPI, powerLevelContent gomatrixserverlib.PowerLevelContent) *util.JSONResponse { - restrictedPowerLevelContent := &gomatrixserverlib.PowerLevelContent{} - *restrictedPowerLevelContent = powerLevelContent - - restrictedDefaultPowerLevel := int64(50) - if restrictedPowerLevelContent.UsersDefault+1 > restrictedDefaultPowerLevel { - restrictedDefaultPowerLevel = restrictedPowerLevelContent.UsersDefault + 1 - } - restrictedPowerLevelContent.EventsDefault = restrictedDefaultPowerLevel - restrictedPowerLevelContent.Invite = restrictedDefaultPowerLevel - - restrictedPowerLevelsHeadered, resErr := makeHeaderedEvent(req, device, cfg, evTime, roomID, rsAPI, fledglingEvent{ - Type: gomatrixserverlib.MRoomPowerLevels, - Content: restrictedPowerLevelContent, - }) - if resErr != nil { - if resErr.Code == http.StatusForbidden { - util.GetLogger(req.Context()).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not restrict power levels in old room") - } else { - return resErr - } - } else { - if resErr = sendHeaderedEvent(req, cfg, rsAPI, restrictedPowerLevelsHeadered); resErr != nil { - return resErr - } - } - return nil -} - -func userIsAuthorized( - req *http.Request, device *userapi.Device, - roomID string, - rsAPI roomserverAPI.RoomserverInternalAPI, -) *util.JSONResponse { - plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{ - EventType: gomatrixserverlib.MRoomPowerLevels, - StateKey: "", - }) - if plEvent == nil { - return &util.JSONResponse{ - Code: http.StatusForbidden, - JSON: jsonerror.Forbidden("You don't have permission to upgrade this room, no power_levels event in this room."), - } - } - - pl, err := plEvent.PowerLevels() - if err != nil { - return &util.JSONResponse{ - Code: 403, - JSON: jsonerror.Forbidden("The power_levels event for this room is malformed so auth checks cannot be performed."), - } - } - - // Check for power level required to send tombstone event (marks the curren room as obsolete), - // if not found, use the StateDefault power level - plToUpgrade, ok := pl.Events["m.room.tombstone"] - if !ok { - plToUpgrade = pl.StateDefault - } - - allowedToUpgrade := pl.UserLevel(device.UserID) >= plToUpgrade - if !allowedToUpgrade { - return &util.JSONResponse{ - Code: 403, - JSON: jsonerror.Forbidden("You don't have permission to upgrade the room, power level too low."), - } - } - - return nil -} - -func makeHeaderedEvent(req *http.Request, device *userapi.Device, - cfg *config.ClientAPI, evTime time.Time, - roomID string, - rsAPI roomserverAPI.RoomserverInternalAPI, event fledglingEvent) (*gomatrixserverlib.HeaderedEvent, *util.JSONResponse) { - - builder := gomatrixserverlib.EventBuilder{ - Sender: device.UserID, - RoomID: roomID, - Type: event.Type, - StateKey: &event.StateKey, - } - err := builder.SetContent(event.Content) - if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("builder.SetContent failed") - resErr := jsonerror.InternalServerError() - return nil, &resErr - } - var queryRes roomserverAPI.QueryLatestEventsAndStateResponse - headeredEvent, err := eventutil.QueryAndBuildEvent(req.Context(), &builder, cfg.Matrix, evTime, rsAPI, &queryRes) - if err == eventutil.ErrRoomNoExists { - return nil, &util.JSONResponse{ - Code: http.StatusNotFound, - JSON: jsonerror.NotFound("Room does not exist"), - } - } else if e, ok := err.(gomatrixserverlib.BadJSONError); ok { - return nil, &util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.BadJSON(e.Error()), - } - } else if e, ok := err.(gomatrixserverlib.EventValidationError); ok { - if e.Code == gomatrixserverlib.EventValidationTooLarge { - return nil, &util.JSONResponse{ - Code: http.StatusRequestEntityTooLarge, - JSON: jsonerror.BadJSON(e.Error()), - } - } - return nil, &util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: jsonerror.BadJSON(e.Error()), - } - } else if err != nil { - util.GetLogger(req.Context()).WithError(err).Error("eventutil.BuildEvent failed") - resErr := jsonerror.InternalServerError() - return nil, &resErr - } - - // check to see if this user can perform this operation - stateEvents := make([]*gomatrixserverlib.Event, len(queryRes.StateEvents)) - for i := range queryRes.StateEvents { - stateEvents[i] = queryRes.StateEvents[i].Event - } - provider := gomatrixserverlib.NewAuthEvents(stateEvents) - if err = gomatrixserverlib.Allowed(headeredEvent.Event, &provider); err != nil { - return nil, &util.JSONResponse{ - Code: http.StatusForbidden, - JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client? - } - } - - return headeredEvent, nil - -} - -func makeTombstoneEvent( - req *http.Request, device *userapi.Device, - cfg *config.ClientAPI, evTime time.Time, - roomID, newRoomID string, - rsAPI roomserverAPI.RoomserverInternalAPI, -) (*gomatrixserverlib.HeaderedEvent, *util.JSONResponse) { - content := map[string]interface{}{ - "body": "This room has been replaced", - "replacement_room": newRoomID, - } - event := fledglingEvent{ - Type: "m.room.tombstone", - Content: content, - } - return makeHeaderedEvent(req, device, cfg, evTime, roomID, rsAPI, event) - -} - -func sendHeaderedEvent( - req *http.Request, - cfg *config.ClientAPI, - rsAPI roomserverAPI.RoomserverInternalAPI, - headeredEvent *gomatrixserverlib.HeaderedEvent, -) *util.JSONResponse { - var inputs []roomserverAPI.InputRoomEvent - inputs = append(inputs, roomserverAPI.InputRoomEvent{ - Kind: roomserverAPI.KindNew, - Event: headeredEvent, - Origin: cfg.Matrix.ServerName, - SendAsServer: roomserverAPI.DoNotSendToOtherServers, - }) - if err := roomserverAPI.SendInputRoomEvents(req.Context(), rsAPI, inputs, false); err != nil { - util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed") - resErr := jsonerror.InternalServerError() - return &resErr - } - - return nil -} - -func unmarshal(in []byte) map[string]interface{} { - ret := make(map[string]interface{}) - err := json.Unmarshal(in, &ret) - if err != nil { - logrus.Fatalf("One of our own state events is not valid JSON: %v", err) - } - return ret -} diff --git a/roomserver/api/api.go b/roomserver/api/api.go index bcbf0e4f9..fb77423f8 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -170,6 +170,9 @@ type RoomserverInternalAPI interface { // PerformForget forgets a rooms history for a specific user PerformForget(ctx context.Context, req *PerformForgetRequest, resp *PerformForgetResponse) error + // PerformRoomUpgrade upgrades a room to a newer version + PerformRoomUpgrade(ctx context.Context, req *PerformRoomUpgradeRequest, resp *PerformRoomUpgradeResponse) + // Asks for the default room version as preferred by the server. QueryRoomVersionCapabilities( ctx context.Context, diff --git a/roomserver/api/api_trace.go b/roomserver/api/api_trace.go index 88b372154..8f54799b2 100644 --- a/roomserver/api/api_trace.go +++ b/roomserver/api/api_trace.go @@ -67,6 +67,15 @@ func (t *RoomserverInternalAPITrace) PerformUnpeek( util.GetLogger(ctx).Infof("PerformUnpeek req=%+v res=%+v", js(req), js(res)) } +func (t *RoomserverInternalAPITrace) PerformRoomUpgrade( + ctx context.Context, + req *PerformRoomUpgradeRequest, + res *PerformRoomUpgradeResponse, +) { + t.Impl.PerformRoomUpgrade(ctx, req, res) + util.GetLogger(ctx).Infof("PerformUnpeek req=%+v res=%+v", js(req), js(res)) +} + func (t *RoomserverInternalAPITrace) PerformJoin( ctx context.Context, req *PerformJoinRequest, diff --git a/roomserver/api/perform.go b/roomserver/api/perform.go index d640858a6..cda4b3ee4 100644 --- a/roomserver/api/perform.go +++ b/roomserver/api/perform.go @@ -203,3 +203,14 @@ type PerformForgetRequest struct { } type PerformForgetResponse struct{} + +type PerformRoomUpgradeRequest struct { + RoomID string `json:"room_id"` + UserID string `json:"user_id"` + RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"` +} + +type PerformRoomUpgradeResponse struct { + NewRoomID string + Error *PerformError +} diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index f96cefcb3..59f485cf7 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -34,6 +34,7 @@ type RoomserverInternalAPI struct { *perform.Publisher *perform.Backfiller *perform.Forgetter + *perform.Upgrader ProcessContext *process.ProcessContext DB storage.Database Cfg *config.RoomServer @@ -159,6 +160,10 @@ func (r *RoomserverInternalAPI) SetFederationAPI(fsAPI fsAPI.FederationInternalA r.Forgetter = &perform.Forgetter{ DB: r.DB, } + r.Upgrader = &perform.Upgrader{ + Cfg: r.Cfg, + URSAPI: r, + } if err := r.Inputer.Start(); err != nil { logrus.WithError(err).Panic("failed to start roomserver input API") diff --git a/roomserver/internal/perform/perform_upgrade.go b/roomserver/internal/perform/perform_upgrade.go new file mode 100644 index 000000000..6d12207b4 --- /dev/null +++ b/roomserver/internal/perform/perform_upgrade.go @@ -0,0 +1,795 @@ +// Copyright 2022 New Vector 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 perform + +import ( + "context" + "encoding/json" + "fmt" + "time" + + "github.com/matrix-org/dendrite/internal/eventutil" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/util" + "github.com/sirupsen/logrus" +) + +type Upgrader struct { + Cfg *config.RoomServer + URSAPI api.RoomserverInternalAPI +} + +// fledglingEvent is a helper representation of an event used when creating many events in succession. +type fledglingEvent struct { + Type string `json:"type"` + StateKey string `json:"state_key"` + Content interface{} `json:"content"` +} + +// PerformRoomUpgrade upgrades a room from one version to another +func (r *Upgrader) PerformRoomUpgrade( + ctx context.Context, + req *api.PerformRoomUpgradeRequest, + res *api.PerformRoomUpgradeResponse, +) { + res.NewRoomID, res.Error = r.performRoomUpgrade(ctx, req) + if res.Error != nil { + res.NewRoomID = "" + } +} + +func (r *Upgrader) performRoomUpgrade( + ctx context.Context, + req *api.PerformRoomUpgradeRequest, +) (string, *api.PerformError) { + roomID := req.RoomID + userID := req.UserID + evTime := time.Now() + + // Return an immediate error if the room does not exist + if err := r.validateRoomExists(ctx, roomID); err != nil { + return "", &api.PerformError{ + Code: api.PerformErrorNoRoom, + Msg: "Error validating that the room exists", + } + } + + // 1. Check if the user is authorized to actually perform the upgrade (can send m.room.tombstone) + if !r.userIsAuthorized(ctx, userID, roomID) { + return "", &api.PerformError{ + Code: api.PerformErrorNotAllowed, + Msg: "You don't have permission to upgrade the room, power level too low.", + } + + } + + // TODO (#267): Check room ID doesn't clash with an existing one, and we + // probably shouldn't be using pseudo-random strings, maybe GUIDs? + newRoomID := fmt.Sprintf("!%s:%s", util.RandomString(16), r.Cfg.Matrix.ServerName) + + // Make the tombstone event + tombstoneEvent, pErr := r.makeTombstoneEvent(ctx, evTime, userID, roomID, newRoomID) + if pErr != nil { + return "", pErr + } + + // Generate the initial events we need to send into the new room. This includes copied state events and bans + // as well as the power level events needed to set up the room + eventsToMake, pErr := r.generateInitialEvents(ctx, userID, roomID, newRoomID, string(req.RoomVersion), tombstoneEvent) + if pErr != nil { + return "", pErr + } + + // 5. Send the tombstone event to the old room (must do this before we set the new canonical_alias) + if pErr = r.sendHeaderedEvent(ctx, tombstoneEvent); pErr != nil { + return "", pErr + } + + // Send the setup events to the new room + if pErr = r.sendInitialEvents(ctx, evTime, userID, newRoomID, string(req.RoomVersion), eventsToMake); pErr != nil { + return "", pErr + } + + // If the old room was public, make sure the new one is too + if pErr = r.publishIfOldRoomWasPublic(ctx, roomID, newRoomID); pErr != nil { + return "", pErr + } + + // If the old room had a canonical alias event, it should be deleted in the old room + if pErr = r.clearOldCanonicalAliasEvent(ctx, evTime, userID, roomID); pErr != nil { + return "", pErr + } + + // 4. Move local aliases to the new room + if pErr = moveLocalAliases(ctx, roomID, newRoomID, userID, r.URSAPI); pErr != nil { + return "", pErr + } + + // 6. Restrict power levels in the old room + if pErr = r.restrictOldRoomPowerLevels(ctx, evTime, userID, roomID); pErr != nil { + return "", pErr + } + + return newRoomID, nil +} + +func (r *Upgrader) getRoomPowerLevels(ctx context.Context, roomID string) (*gomatrixserverlib.PowerLevelContent, *api.PerformError) { + oldPowerLevelsEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomPowerLevels, + StateKey: "", + }) + powerLevelContent, err := oldPowerLevelsEvent.PowerLevels() + if err != nil { + util.GetLogger(ctx).WithError(err).Error() + return nil, &api.PerformError{ + Msg: "powerLevel event was not actually a power level event", + } + } + return powerLevelContent, nil +} + +func (r *Upgrader) restrictOldRoomPowerLevels(ctx context.Context, evTime time.Time, userID, roomID string) *api.PerformError { + powerLevelContent, pErr := r.getRoomPowerLevels(ctx, roomID) + if pErr != nil { + return pErr + } + + restrictedPowerLevelContent := &gomatrixserverlib.PowerLevelContent{} + *restrictedPowerLevelContent = *powerLevelContent + + restrictedDefaultPowerLevel := int64(50) + if restrictedPowerLevelContent.UsersDefault+1 > restrictedDefaultPowerLevel { + restrictedDefaultPowerLevel = restrictedPowerLevelContent.UsersDefault + 1 + } + restrictedPowerLevelContent.EventsDefault = restrictedDefaultPowerLevel + restrictedPowerLevelContent.Invite = restrictedDefaultPowerLevel + + restrictedPowerLevelsHeadered, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{ + Type: gomatrixserverlib.MRoomPowerLevels, + Content: restrictedPowerLevelContent, + }) + if resErr != nil { + if resErr.Code == api.PerformErrorNotAllowed { + util.GetLogger(ctx).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not restrict power levels in old room") + } else { + return resErr + } + } else { + if resErr = r.sendHeaderedEvent(ctx, restrictedPowerLevelsHeadered); resErr != nil { + return resErr + } + } + return nil +} + +func moveLocalAliases(ctx context.Context, + roomID, newRoomID, userID string, + URSAPI api.RoomserverInternalAPI) *api.PerformError { + var err error + + aliasReq := api.GetAliasesForRoomIDRequest{RoomID: roomID} + aliasRes := api.GetAliasesForRoomIDResponse{} + if err = URSAPI.GetAliasesForRoomID(ctx, &aliasReq, &aliasRes); err != nil { + return &api.PerformError{ + Msg: "Could not get aliases for old room", + } + } + + for _, alias := range aliasRes.Aliases { + removeAliasReq := api.RemoveRoomAliasRequest{UserID: userID, Alias: alias} + removeAliasRes := api.RemoveRoomAliasResponse{} + if err = URSAPI.RemoveRoomAlias(ctx, &removeAliasReq, &removeAliasRes); err != nil { + return &api.PerformError{ + Msg: "api.RemoveRoomAlias failed", + } + } + + setAliasReq := api.SetRoomAliasRequest{UserID: userID, Alias: alias, RoomID: newRoomID} + setAliasRes := api.SetRoomAliasResponse{} + if err = URSAPI.SetRoomAlias(ctx, &setAliasReq, &setAliasRes); err != nil { + return &api.PerformError{ + Msg: "api.SetRoomAlias failed", + } + } + } + return nil +} + +func (r *Upgrader) clearOldCanonicalAliasEvent(ctx context.Context, evTime time.Time, userID, roomID string) *api.PerformError { + emptyCanonicalAliasEvent, resErr := r.makeHeaderedEvent(ctx, evTime, userID, roomID, fledglingEvent{ + Type: gomatrixserverlib.MRoomCanonicalAlias, + Content: map[string]interface{}{}, + }) + if resErr != nil { + if resErr.Code == api.PerformErrorNotAllowed { + util.GetLogger(ctx).WithField(logrus.ErrorKey, resErr).Warn("UpgradeRoom: Could not set empty canonical alias event in old room") + } else { + return resErr + } + } else { + if resErr = r.sendHeaderedEvent(ctx, emptyCanonicalAliasEvent); resErr != nil { + return resErr + } + } + return nil +} + +func (r *Upgrader) publishIfOldRoomWasPublic(ctx context.Context, roomID, newRoomID string) *api.PerformError { + // check if the old room was published + var pubQueryRes api.QueryPublishedRoomsResponse + err := r.URSAPI.QueryPublishedRooms(ctx, &api.QueryPublishedRoomsRequest{ + RoomID: roomID, + }, &pubQueryRes) + if err != nil { + return &api.PerformError{ + Msg: "QueryPublishedRooms failed", + } + } + + // if the old room is published (was public), publish the new room + if len(pubQueryRes.RoomIDs) == 1 { + publishNewRoom(ctx, r.URSAPI, roomID, newRoomID) + } + return nil +} + +func publishNewRoom( + ctx context.Context, + URSAPI api.RoomserverInternalAPI, + oldRoomID, newRoomID string, +) { + // expose this room in the published room list + var pubNewRoomRes api.PerformPublishResponse + URSAPI.PerformPublish(ctx, &api.PerformPublishRequest{ + RoomID: newRoomID, + Visibility: "public", + }, &pubNewRoomRes) + if pubNewRoomRes.Error != nil { + // treat as non-fatal since the room is already made by this point + util.GetLogger(ctx).WithError(pubNewRoomRes.Error).Error("failed to visibility:public") + } + + var unpubOldRoomRes api.PerformPublishResponse + // remove the old room from the published room list + URSAPI.PerformPublish(ctx, &api.PerformPublishRequest{ + RoomID: oldRoomID, + Visibility: "private", + }, &unpubOldRoomRes) + if unpubOldRoomRes.Error != nil { + // treat as non-fatal since the room is already made by this point + util.GetLogger(ctx).WithError(unpubOldRoomRes.Error).Error("failed to visibility:private") + } +} + +func (r *Upgrader) validateRoomExists(ctx context.Context, roomID string) error { + verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID} + verRes := api.QueryRoomVersionForRoomResponse{} + if err := r.URSAPI.QueryRoomVersionForRoom(ctx, &verReq, &verRes); err != nil { + return &api.PerformError{ + Code: api.PerformErrorNoRoom, + Msg: "Room does not exist", + } + } + return nil +} + +func (r *Upgrader) userIsAuthorized(ctx context.Context, userID, roomID string, +) bool { + plEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomPowerLevels, + StateKey: "", + }) + if plEvent == nil { + return false + } + pl, err := plEvent.PowerLevels() + if err != nil { + return false + } + // Check for power level required to send tombstone event (marks the curren room as obsolete), + // if not found, use the StateDefault power level + plToUpgrade, ok := pl.Events["m.room.tombstone"] + if !ok { + plToUpgrade = pl.StateDefault + } + return pl.UserLevel(userID) >= plToUpgrade +} + +func (r *Upgrader) generateInitialEvents(ctx context.Context, userID, roomID, newRoomID, newVersion string, tombstoneEvent *gomatrixserverlib.HeaderedEvent) ([]fledglingEvent, *api.PerformError) { + oldCreateEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomCreate, + StateKey: "", + }) + oldPowerLevelsEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomPowerLevels, + StateKey: "", + }) + oldJoinRulesEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomJoinRules, + StateKey: "", + }) + oldHistoryVisibilityEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomHistoryVisibility, + StateKey: "", + }) + oldNameEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomName, + StateKey: "", + }) + oldTopicEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomTopic, + StateKey: "", + }) + oldGuestAccessEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomGuestAccess, + StateKey: "", + }) + oldAvatarEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomAvatar, + StateKey: "", + }) + oldEncryptionEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomEncryption, + StateKey: "", + }) + oldServerAclEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: "m.room.server_acl", + StateKey: "", + }) + // Not in the spec, but needed for sytest compatablility + oldRelatedGroupsEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: "m.room.related_groups", + StateKey: "", + }) + oldCanonicalAliasEvent := api.GetStateEvent(ctx, r.URSAPI, roomID, gomatrixserverlib.StateKeyTuple{ + EventType: gomatrixserverlib.MRoomCanonicalAlias, + StateKey: "", + }) + + newCreateContent := map[string]interface{}{ + "creator": userID, + "room_version": newVersion, // TODO: change struct to single var? + "predecessor": gomatrixserverlib.PreviousRoom{ + EventID: tombstoneEvent.EventID(), + RoomID: roomID, + }, + } + oldCreateContent := unmarshal(oldCreateEvent.Content()) + if federate, ok := oldCreateContent["m.federate"].(bool); ok { + newCreateContent["m.federate"] = federate + } + + newCreateEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomCreate, + Content: newCreateContent, + } + + membershipEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomMember, + StateKey: userID, + Content: gomatrixserverlib.MemberContent{ + Membership: gomatrixserverlib.Join, + DisplayName: "", + AvatarURL: "", // TODO + }, + } + + powerLevelContent, err := oldPowerLevelsEvent.PowerLevels() + if err != nil { + util.GetLogger(ctx).WithError(err).Error() + return nil, &api.PerformError{ + Msg: "powerLevel event was not actually a power level event", + } + } + + newPowerLevelsEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomPowerLevels, + Content: powerLevelContent, + } + + //create temporary power level event that elevates upgrading user's prvileges to create every copied state event + tempPowerLevelsEvent := createTemporaryPowerLevels(powerLevelContent, userID) + + joinRulesContent, err := oldJoinRulesEvent.JoinRule() + if err != nil { + return nil, &api.PerformError{ + Msg: "Join rules event had bad content", + } + } + newJoinRulesEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomJoinRules, + Content: map[string]interface{}{ + "join_rule": joinRulesContent, + }, + } + + historyVisibilityContent, err := oldHistoryVisibilityEvent.HistoryVisibility() + if err != nil { + return nil, &api.PerformError{ + Msg: "History visibility event had bad content", + } + } + newHistoryVisibilityEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomHistoryVisibility, + Content: map[string]interface{}{ + "history_visibility": historyVisibilityContent, + }, + } + + var newNameEvent fledglingEvent + var newTopicEvent fledglingEvent + var newGuestAccessEvent fledglingEvent + var newAvatarEvent fledglingEvent + var newEncryptionEvent fledglingEvent + var newServerACLEvent fledglingEvent + var newRelatedGroupsEvent fledglingEvent + var newCanonicalAliasEvent fledglingEvent + + if oldNameEvent != nil { + newNameEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomName, + Content: unmarshal(oldNameEvent.Content()), + } + } + if oldTopicEvent != nil { + newTopicEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomTopic, + Content: unmarshal(oldTopicEvent.Content()), + } + } + if oldGuestAccessEvent != nil { + newGuestAccessEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomGuestAccess, + Content: unmarshal(oldGuestAccessEvent.Content()), + } + } + if oldAvatarEvent != nil { + newAvatarEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomAvatar, + Content: unmarshal(oldAvatarEvent.Content()), + } + } + if oldEncryptionEvent != nil { + newEncryptionEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomEncryption, + Content: unmarshal(oldEncryptionEvent.Content()), + } + } + if oldServerAclEvent != nil { + newServerACLEvent = fledglingEvent{ + Type: "m.room.server_acl", + Content: unmarshal(oldServerAclEvent.Content()), + } + } + if oldRelatedGroupsEvent != nil { + newRelatedGroupsEvent = fledglingEvent{ + Type: "m.room.related_groups", + Content: unmarshal(oldRelatedGroupsEvent.Content()), + } + } + if oldCanonicalAliasEvent != nil { + newCanonicalAliasEvent = fledglingEvent{ + Type: gomatrixserverlib.MRoomCanonicalAlias, + Content: unmarshal(oldCanonicalAliasEvent.Content()), + } + } + + // 3. Replicate transferable state events + // send events into the room in order of: + // 1- m.room.create + // 2- m.room.power_levels (temporary, to allow the upgrading user to send everything) + // 3- m.room.join_rules + // 4- m.room.history_visibility + // 5- m.room.guest_access + // 6- m.room.name + // 7- m.room.avatar + // 8- m.room.topic + // 9- m.room.encryption + // 10-m.room.server_acl + // 11-m.room.related_groups + // 12-m.room.canonical_alias + // 13-All ban events from the old room + // 14-The original room power levels + eventsToMake := []fledglingEvent{ + newCreateEvent, membershipEvent, tempPowerLevelsEvent, newJoinRulesEvent, newHistoryVisibilityEvent, + } + if oldGuestAccessEvent != nil { + eventsToMake = append(eventsToMake, newGuestAccessEvent) + } else { // Always create this with the default value to appease sytests + eventsToMake = append(eventsToMake, fledglingEvent{ + Type: gomatrixserverlib.MRoomGuestAccess, + Content: map[string]interface{}{"guest_access": "forbidden"}, + }) + } + if oldNameEvent != nil { + eventsToMake = append(eventsToMake, newNameEvent) + } + if oldAvatarEvent != nil { + eventsToMake = append(eventsToMake, newAvatarEvent) + } + if oldTopicEvent != nil { + eventsToMake = append(eventsToMake, newTopicEvent) + } + if oldEncryptionEvent != nil { + eventsToMake = append(eventsToMake, newEncryptionEvent) + } + if oldServerAclEvent != nil { + eventsToMake = append(eventsToMake, newServerACLEvent) + } + if oldRelatedGroupsEvent != nil { + eventsToMake = append(eventsToMake, newRelatedGroupsEvent) + } + if oldCanonicalAliasEvent != nil { + eventsToMake = append(eventsToMake, newCanonicalAliasEvent) + } + banEvents, err := getBanEvents(ctx, roomID, r.URSAPI) + if err != nil { + return nil, &api.PerformError{ + Msg: err.Error(), + } + } else { + eventsToMake = append(eventsToMake, banEvents...) + } + eventsToMake = append(eventsToMake, newPowerLevelsEvent) + return eventsToMake, nil +} + +func (r *Upgrader) sendInitialEvents(ctx context.Context, evTime time.Time, userID, newRoomID, newVersion string, eventsToMake []fledglingEvent) *api.PerformError { + var err error + var builtEvents []*gomatrixserverlib.HeaderedEvent + authEvents := gomatrixserverlib.NewAuthEvents(nil) + for i, e := range eventsToMake { + depth := i + 1 // depth starts at 1 + + builder := gomatrixserverlib.EventBuilder{ + Sender: userID, + RoomID: newRoomID, + Type: e.Type, + StateKey: &e.StateKey, + Depth: int64(depth), + } + err = builder.SetContent(e.Content) + if err != nil { + return &api.PerformError{ + Msg: "builder.SetContent failed", + } + } + if i > 0 { + builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()} + } + var event *gomatrixserverlib.Event + event, err = r.buildEvent(&builder, &authEvents, evTime, gomatrixserverlib.RoomVersion(newVersion)) + if err != nil { + return &api.PerformError{ + Msg: "buildEvent failed", + } + } + + if err = gomatrixserverlib.Allowed(event, &authEvents); err != nil { + return &api.PerformError{ + Msg: "gomatrixserverlib.Allowed failed", + } + } + + // Add the event to the list of auth events + builtEvents = append(builtEvents, event.Headered(gomatrixserverlib.RoomVersion(newVersion))) + err = authEvents.AddEvent(event) + if err != nil { + return &api.PerformError{ + Msg: "authEvents.AddEvent failed", + } + } + } + + inputs := make([]api.InputRoomEvent, 0, len(builtEvents)) + for _, event := range builtEvents { + inputs = append(inputs, api.InputRoomEvent{ + Kind: api.KindNew, + Event: event, + Origin: r.Cfg.Matrix.ServerName, + SendAsServer: api.DoNotSendToOtherServers, + }) + } + if err = api.SendInputRoomEvents(ctx, r.URSAPI, inputs, false); err != nil { + return &api.PerformError{ + Msg: "api.SendInputRoomEvents failed", + } + } + return nil +} + +func (r *Upgrader) makeTombstoneEvent( + ctx context.Context, + evTime time.Time, + userID, roomID, newRoomID string, +) (*gomatrixserverlib.HeaderedEvent, *api.PerformError) { + content := map[string]interface{}{ + "body": "This room has been replaced", + "replacement_room": newRoomID, + } + event := fledglingEvent{ + Type: "m.room.tombstone", + Content: content, + } + return r.makeHeaderedEvent(ctx, evTime, userID, roomID, event) +} + +func (r *Upgrader) makeHeaderedEvent(ctx context.Context, evTime time.Time, userID, roomID string, event fledglingEvent) (*gomatrixserverlib.HeaderedEvent, *api.PerformError) { + builder := gomatrixserverlib.EventBuilder{ + Sender: userID, + RoomID: roomID, + Type: event.Type, + StateKey: &event.StateKey, + } + err := builder.SetContent(event.Content) + if err != nil { + return nil, &api.PerformError{ + Msg: "builder.SetContent failed", + } + } + var queryRes api.QueryLatestEventsAndStateResponse + headeredEvent, err := eventutil.QueryAndBuildEvent(ctx, &builder, r.Cfg.Matrix, evTime, r.URSAPI, &queryRes) + if err == eventutil.ErrRoomNoExists { + return nil, &api.PerformError{ + Code: api.PerformErrorNoRoom, + Msg: "Room does not exist", + } + } else if e, ok := err.(gomatrixserverlib.BadJSONError); ok { + return nil, &api.PerformError{ + Msg: e.Error(), + } + } else if e, ok := err.(gomatrixserverlib.EventValidationError); ok { + if e.Code == gomatrixserverlib.EventValidationTooLarge { + return nil, &api.PerformError{ + Msg: e.Error(), + } + } + return nil, &api.PerformError{ + Msg: e.Error(), + } + } else if err != nil { + return nil, &api.PerformError{ + Msg: "eventutil.BuildEvent failed", + } + } + // check to see if this user can perform this operation + stateEvents := make([]*gomatrixserverlib.Event, len(queryRes.StateEvents)) + for i := range queryRes.StateEvents { + stateEvents[i] = queryRes.StateEvents[i].Event + } + provider := gomatrixserverlib.NewAuthEvents(stateEvents) + if err = gomatrixserverlib.Allowed(headeredEvent.Event, &provider); err != nil { + return nil, &api.PerformError{ + Code: api.PerformErrorNotAllowed, + Msg: err.Error(), // TODO: Is this error string comprehensible to the client? + } + } + + return headeredEvent, nil +} + +func getBanEvents(ctx context.Context, roomID string, URSAPI api.RoomserverInternalAPI) ([]fledglingEvent, error) { + var err error + banEvents := []fledglingEvent{} + + roomMemberReq := api.QueryCurrentStateRequest{RoomID: roomID, AllowWildcards: true, StateTuples: []gomatrixserverlib.StateKeyTuple{ + {EventType: gomatrixserverlib.MRoomMember, StateKey: "*"}, + }} + roomMemberRes := api.QueryCurrentStateResponse{} + if err = URSAPI.QueryCurrentState(ctx, &roomMemberReq, &roomMemberRes); err != nil { + return nil, err + } + for _, event := range roomMemberRes.StateEvents { + if event == nil { + continue + } + memberContent, err := gomatrixserverlib.NewMemberContentFromEvent(event.Event) + if err != nil || memberContent.Membership != gomatrixserverlib.Ban { + continue + } + banEvents = append(banEvents, fledglingEvent{Type: gomatrixserverlib.MRoomMember, StateKey: *event.StateKey(), Content: memberContent}) + } + return banEvents, nil +} + +func createTemporaryPowerLevels(powerLevelContent *gomatrixserverlib.PowerLevelContent, userID string) fledglingEvent { + eventPowerLevels := powerLevelContent.Events + stateDefaultPowerLevel := powerLevelContent.StateDefault + neededPowerLevel := stateDefaultPowerLevel + for _, powerLevel := range eventPowerLevels { + if powerLevel > neededPowerLevel { + neededPowerLevel = powerLevel + } + } + + tempPowerLevelContent := &gomatrixserverlib.PowerLevelContent{} + *tempPowerLevelContent = *powerLevelContent + newUserPowerLevels := make(map[string]int64) + for key, value := range powerLevelContent.Users { + newUserPowerLevels[key] = value + } + tempPowerLevelContent.Users = newUserPowerLevels + + if val, ok := tempPowerLevelContent.Users[userID]; ok { + if val < neededPowerLevel { + tempPowerLevelContent.Users[userID] = neededPowerLevel + } + } else { + if tempPowerLevelContent.UsersDefault < val { + tempPowerLevelContent.UsersDefault = neededPowerLevel + } + } + tempPowerLevelsEvent := fledglingEvent{ + Type: gomatrixserverlib.MRoomPowerLevels, + Content: tempPowerLevelContent, + } + return tempPowerLevelsEvent +} + +func (r *Upgrader) sendHeaderedEvent( + ctx context.Context, + headeredEvent *gomatrixserverlib.HeaderedEvent, +) *api.PerformError { + var inputs []api.InputRoomEvent + inputs = append(inputs, api.InputRoomEvent{ + Kind: api.KindNew, + Event: headeredEvent, + Origin: r.Cfg.Matrix.ServerName, + SendAsServer: api.DoNotSendToOtherServers, + }) + if err := api.SendInputRoomEvents(ctx, r.URSAPI, inputs, false); err != nil { + return &api.PerformError{ + Msg: "api.SendInputRoomEvents failed", + } + } + + return nil +} + +func (r *Upgrader) buildEvent( + builder *gomatrixserverlib.EventBuilder, + provider gomatrixserverlib.AuthEventProvider, + evTime time.Time, + roomVersion gomatrixserverlib.RoomVersion, +) (*gomatrixserverlib.Event, error) { + eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) + if err != nil { + return nil, err + } + refs, err := eventsNeeded.AuthEventReferences(provider) + if err != nil { + return nil, err + } + builder.AuthEvents = refs + event, err := builder.Build( + evTime, r.Cfg.Matrix.ServerName, r.Cfg.Matrix.KeyID, + r.Cfg.Matrix.PrivateKey, roomVersion, + ) + if err != nil { + return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %w", builder.Type, err) + } + return event, nil +} + +func unmarshal(in []byte) map[string]interface{} { + ret := make(map[string]interface{}) + err := json.Unmarshal(in, &ret) + if err != nil { + logrus.Fatalf("One of our own state events is not valid JSON: %v", err) + } + return ret +} diff --git a/roomserver/inthttp/client.go b/roomserver/inthttp/client.go index 99c596606..d55805a91 100644 --- a/roomserver/inthttp/client.go +++ b/roomserver/inthttp/client.go @@ -32,6 +32,7 @@ const ( RoomserverPerformInvitePath = "/roomserver/performInvite" RoomserverPerformPeekPath = "/roomserver/performPeek" RoomserverPerformUnpeekPath = "/roomserver/performUnpeek" + RoomserverPerformRoomUpgradePath = "/roomserver/performRoomUpgrade" RoomserverPerformJoinPath = "/roomserver/performJoin" RoomserverPerformLeavePath = "/roomserver/performLeave" RoomserverPerformBackfillPath = "/roomserver/performBackfill" @@ -252,6 +253,23 @@ func (h *httpRoomserverInternalAPI) PerformUnpeek( } } +func (h *httpRoomserverInternalAPI) PerformRoomUpgrade( + ctx context.Context, + request *api.PerformRoomUpgradeRequest, + response *api.PerformRoomUpgradeResponse, +) { + span, ctx := opentracing.StartSpanFromContext(ctx, "PerformRoomUpgrade") + defer span.Finish() + + apiURL := h.roomserverURL + RoomserverPerformRoomUpgradePath + err := httputil.PostJSON(ctx, span, h.httpClient, apiURL, request, response) + if err != nil { + response.Error = &api.PerformError{ + Msg: fmt.Sprintf("failed to communicate with roomserver: %s", err), + } + } +} + func (h *httpRoomserverInternalAPI) PerformLeave( ctx context.Context, request *api.PerformLeaveRequest, diff --git a/roomserver/inthttp/server.go b/roomserver/inthttp/server.go index 691a45830..64c091c49 100644 --- a/roomserver/inthttp/server.go +++ b/roomserver/inthttp/server.go @@ -96,6 +96,17 @@ func AddRoutes(r api.RoomserverInternalAPI, internalAPIMux *mux.Router) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + internalAPIMux.Handle(RoomserverPerformPeekPath, + httputil.MakeInternalAPI("performRoomUpgrade", func(req *http.Request) util.JSONResponse { + var request api.PerformRoomUpgradeRequest + var response api.PerformRoomUpgradeResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + r.PerformRoomUpgrade(req.Context(), &request, &response) + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) internalAPIMux.Handle(RoomserverPerformPublishPath, httputil.MakeInternalAPI("performPublish", func(req *http.Request) util.JSONResponse { var request api.PerformPublishRequest