diff --git a/federationapi/routing/invite.go b/federationapi/routing/invite.go index 4d80b3939..31661d0d6 100644 --- a/federationapi/routing/invite.go +++ b/federationapi/routing/invite.go @@ -20,7 +20,6 @@ import ( "fmt" "net/http" - "github.com/getsentry/sentry-go" "github.com/matrix-org/dendrite/roomserver/api" "github.com/matrix-org/dendrite/roomserver/types" "github.com/matrix-org/dendrite/setup/config" @@ -28,6 +27,7 @@ import ( "github.com/matrix-org/gomatrixserverlib/fclient" "github.com/matrix-org/gomatrixserverlib/spec" "github.com/matrix-org/util" + "github.com/sirupsen/logrus" ) // InviteV2 implements /_matrix/federation/v2/invite/{roomID}/{eventID} @@ -200,11 +200,9 @@ func processInvite( string(domain), cfg.Matrix.KeyID, cfg.Matrix.PrivateKey, ) - // TODO: Split out this logic! - - // Add the invite event to the roomserver. inviteEvent := &types.HeaderedEvent{PDU: signedEvent} - if err = rsAPI.HandleInvite(ctx, inviteEvent, strippedState); err != nil { + err = handleInvite(ctx, cfg, inviteEvent, strippedState, rsAPI) + if err != nil { util.GetLogger(ctx).WithError(err).Error("HandleInvite failed") return util.JSONResponse{ Code: http.StatusInternalServerError, @@ -212,21 +210,9 @@ func processInvite( } } - switch e := err.(type) { - case api.ErrInvalidID: - return util.JSONResponse{ - Code: http.StatusBadRequest, - JSON: spec.Unknown(e.Error()), - } - case api.ErrNotAllowed: - return util.JSONResponse{ - Code: http.StatusForbidden, - JSON: spec.Forbidden(e.Error()), - } - case nil: - default: + // Add the invite event to the roomserver. + if err = rsAPI.HandleInvite(ctx, inviteEvent); err != nil { util.GetLogger(ctx).WithError(err).Error("HandleInvite failed") - sentry.CaptureException(err) return util.JSONResponse{ Code: http.StatusInternalServerError, JSON: spec.InternalServerError{}, @@ -247,3 +233,141 @@ func processInvite( } } } + +// TODO: Define interface instead of passing in FedRoomserverAPI +// TODO: Migrate to GMSL +// TODO: Clean up logic/naming of stuff + +func handleInvite( + ctx context.Context, + cfg *config.FederationAPI, + inviteEvent *types.HeaderedEvent, + inviteRoomState []fclient.InviteV2StrippedState, + rsAPI api.FederationRoomserverAPI, +) error { + if inviteEvent.StateKey() == nil { + return fmt.Errorf("invite must be a state event") + } + + roomID := inviteEvent.RoomID() + targetUserID := *inviteEvent.StateKey() + _, domain, err := gomatrixserverlib.SplitID('@', targetUserID) + if err != nil { + return api.ErrInvalidID{Err: fmt.Errorf("the user ID %s is invalid", targetUserID)} + } + isTargetLocal := cfg.Matrix.IsLocalServerName(domain) + if !isTargetLocal { + return api.ErrInvalidID{Err: fmt.Errorf("the invite must be to a local user")} + } + + validRoomID, err := spec.NewRoomID(roomID) + if err != nil { + return err + } + isKnownRoom, err := rsAPI.IsKnownRoom(ctx, *validRoomID) + if err != nil { + return err + } + + inviteState := inviteRoomState + if len(inviteState) == 0 { + // "If they are set on the room, at least the state for m.room.avatar, m.room.canonical_alias, m.room.join_rules, and m.room.name SHOULD be included." + // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-member + stateWanted := []gomatrixserverlib.StateKeyTuple{} + for _, t := range []string{ + spec.MRoomName, spec.MRoomCanonicalAlias, + spec.MRoomJoinRules, spec.MRoomAvatar, + spec.MRoomEncryption, spec.MRoomCreate, + } { + stateWanted = append(stateWanted, gomatrixserverlib.StateKeyTuple{ + EventType: t, + StateKey: "", + }) + } + if is, err := rsAPI.GenerateInviteStrippedStateV2(ctx, *validRoomID, stateWanted, inviteEvent); err == nil { + inviteState = is + } else { + return err + } + } + + logger := util.GetLogger(ctx).WithFields(map[string]interface{}{ + "inviter": inviteEvent.Sender(), + "invitee": *inviteEvent.StateKey(), + "room_id": roomID, + "event_id": inviteEvent.EventID(), + }) + logger.WithFields(logrus.Fields{ + "room_version": inviteEvent.Version(), + "room_info_exists": isKnownRoom, + "target_local": isTargetLocal, + }).Debug("processing incoming federation invite event") + + if len(inviteState) == 0 { + if err = inviteEvent.SetUnsignedField("invite_room_state", struct{}{}); err != nil { + return fmt.Errorf("event.SetUnsignedField: %w", err) + } + } else { + if err = inviteEvent.SetUnsignedField("invite_room_state", inviteState); err != nil { + return fmt.Errorf("event.SetUnsignedField: %w", err) + } + } + + if !isKnownRoom && isTargetLocal { + // The invite came in over federation for a room that we don't know about + // yet. We need to handle this a bit differently to most invites because + // we don't know the room state, therefore the roomserver can't process + // an input event. Instead we will update the membership table with the + // new invite and generate an output event. + return nil + } + + req := api.QueryMembershipForUserRequest{ + RoomID: roomID, + UserID: targetUserID, + } + res := api.QueryMembershipForUserResponse{} + err = rsAPI.QueryMembershipForUser(ctx, &req, &res) + if err != nil { + return fmt.Errorf("r.QueryMembershipForUser: %w", err) + } + isAlreadyJoined := (res.Membership == spec.Join) + + //_, isAlreadyJoined, _, err = r.DB.GetMembership(ctx, info.RoomNID, *inviteEvent.StateKey()) + //if err != nil { + // return nil, fmt.Errorf("r.DB.GetMembership: %w", err) + //} + if isAlreadyJoined { + // If the user is joined to the room then that takes precedence over this + // invite event. It makes little sense to move a user that is already + // joined to the room into the invite state. + // This could plausibly happen if an invite request raced with a join + // request for a user. For example if a user was invited to a public + // room and they joined the room at the same time as the invite was sent. + // The other way this could plausibly happen is if an invite raced with + // a kick. For example if a user was kicked from a room in error and in + // response someone else in the room re-invited them then it is possible + // for the invite request to race with the leave event so that the + // target receives invite before it learns that it has been kicked. + // There are a few ways this could be plausibly handled in the roomserver. + // 1) Store the invite, but mark it as retired. That will result in the + // permanent rejection of that invite event. So even if the target + // user leaves the room and the invite is retransmitted it will be + // ignored. However a new invite with a new event ID would still be + // accepted. + // 2) Silently discard the invite event. This means that if the event + // was retransmitted at a later date after the target user had left + // the room we would accept the invite. However since we hadn't told + // the sending server that the invite had been discarded it would + // have no reason to attempt to retry. + // 3) Signal the sending server that the user is already joined to the + // room. + // For now we will implement option 2. Since in the abesence of a retry + // mechanism it will be equivalent to option 1, and we don't have a + // signalling mechanism to implement option 3. + logger.Debugf("user already joined") + return api.ErrNotAllowed{Err: fmt.Errorf("user is already joined to room")} + } + + return nil +} diff --git a/roomserver/api/api.go b/roomserver/api/api.go index 69cecd7e4..a452f1d06 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -224,7 +224,8 @@ type FederationRoomserverAPI interface { QueryRoomsForUser(ctx context.Context, req *QueryRoomsForUserRequest, res *QueryRoomsForUserResponse) error QueryRestrictedJoinAllowed(ctx context.Context, req *QueryRestrictedJoinAllowedRequest, res *QueryRestrictedJoinAllowedResponse) error PerformInboundPeek(ctx context.Context, req *PerformInboundPeekRequest, res *PerformInboundPeekResponse) error - HandleInvite(ctx context.Context, event *types.HeaderedEvent, inviteRoomState []fclient.InviteV2StrippedState) error + HandleInvite(ctx context.Context, event *types.HeaderedEvent) error + PerformInvite(ctx context.Context, req *PerformInviteRequest) error // Query a given amount (or less) of events prior to a given set of events. PerformBackfill(ctx context.Context, req *PerformBackfillRequest, res *PerformBackfillResponse) error @@ -234,6 +235,9 @@ type FederationRoomserverAPI interface { QueryRoomInfo(ctx context.Context, roomID spec.RoomID) (*types.RoomInfo, error) UserJoinedToRoom(ctx context.Context, roomID types.RoomNID, userID spec.UserID) (bool, error) LocallyJoinedUsers(ctx context.Context, roomVersion gomatrixserverlib.RoomVersion, roomNID types.RoomNID) ([]gomatrixserverlib.PDU, error) + + IsKnownRoom(ctx context.Context, roomID spec.RoomID) (bool, error) + GenerateInviteStrippedStateV2(ctx context.Context, roomID spec.RoomID, stateWanted []gomatrixserverlib.StateKeyTuple, inviteEvent *types.HeaderedEvent) ([]fclient.InviteV2StrippedState, error) } type KeyserverRoomserverAPI interface { diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index 90155fbe6..1a833976f 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -209,16 +209,24 @@ func (r *RoomserverInternalAPI) SetAppserviceAPI(asAPI asAPI.AppServiceInternalA r.asAPI = asAPI } +func (r *RoomserverInternalAPI) IsKnownRoom(ctx context.Context, roomID spec.RoomID) (bool, error) { + return r.Inviter.IsKnownRoom(ctx, roomID) +} + +func (r *RoomserverInternalAPI) GenerateInviteStrippedStateV2( + ctx context.Context, roomID spec.RoomID, stateWanted []gomatrixserverlib.StateKeyTuple, inviteEvent *types.HeaderedEvent, +) ([]fclient.InviteV2StrippedState, error) { + return r.Inviter.GenerateInviteStrippedStateV2(ctx, roomID, stateWanted, inviteEvent) +} + func (r *RoomserverInternalAPI) HandleInvite( - ctx context.Context, - event *types.HeaderedEvent, - inviteRoomState []fclient.InviteV2StrippedState, + ctx context.Context, inviteEvent *types.HeaderedEvent, ) error { - outputEvents, err := r.Inviter.HandleInvite(ctx, event, inviteRoomState) + outputEvents, err := r.Inviter.ProcessInviteMembership(ctx, inviteEvent) if err != nil { return err } - return r.OutputProducer.ProduceRoomEvents(event.RoomID(), outputEvents) + return r.OutputProducer.ProduceRoomEvents(inviteEvent.RoomID(), outputEvents) } func (r *RoomserverInternalAPI) PerformInvite( diff --git a/roomserver/internal/perform/perform_invite.go b/roomserver/internal/perform/perform_invite.go index 2ee31b297..0e449c6ea 100644 --- a/roomserver/internal/perform/perform_invite.go +++ b/roomserver/internal/perform/perform_invite.go @@ -42,6 +42,14 @@ type Inviter struct { Inputer *input.Inputer } +func (r *Inviter) IsKnownRoom(ctx context.Context, roomID spec.RoomID) (bool, error) { + info, err := r.DB.RoomInfo(ctx, roomID.String()) + if err != nil { + return false, fmt.Errorf("failed to load RoomInfo: %w", err) + } + return (info != nil && !info.IsStub()), nil +} + func (r *Inviter) generateInviteStrippedState( ctx context.Context, roomID spec.RoomID, inviteEvent *types.HeaderedEvent, inviteState []fclient.InviteV2StrippedState, ) (*types.RoomInfo, []fclient.InviteV2StrippedState, error) { @@ -60,6 +68,41 @@ func (r *Inviter) generateInviteStrippedState( return info, strippedState, nil } +func (r *Inviter) GenerateInviteStrippedStateV2( + ctx context.Context, roomID spec.RoomID, stateWanted []gomatrixserverlib.StateKeyTuple, inviteEvent *types.HeaderedEvent, +) ([]fclient.InviteV2StrippedState, error) { + info, err := r.DB.RoomInfo(ctx, roomID.String()) + if err != nil { + return nil, fmt.Errorf("failed to load RoomInfo: %w", err) + } + if info != nil { + roomState := state.NewStateResolution(r.DB, info) + stateEntries, err := roomState.LoadStateAtSnapshotForStringTuples( + ctx, info.StateSnapshotNID(), stateWanted, + ) + if err != nil { + return nil, nil + } + stateNIDs := []types.EventNID{} + for _, stateNID := range stateEntries { + stateNIDs = append(stateNIDs, stateNID.EventNID) + } + stateEvents, err := r.DB.Events(ctx, info.RoomVersion, stateNIDs) + if err != nil { + return nil, nil + } + inviteState := []fclient.InviteV2StrippedState{ + fclient.NewInviteV2StrippedState(inviteEvent.PDU), + } + stateEvents = append(stateEvents, types.Event{PDU: inviteEvent.PDU}) + for _, event := range stateEvents { + inviteState = append(inviteState, fclient.NewInviteV2StrippedState(event.PDU)) + } + return inviteState, nil + } + return nil, nil +} + func (r *Inviter) generateInviteStrippedStateNoNID( ctx context.Context, roomID spec.RoomID, inviteEvent *types.HeaderedEvent, inviteState []fclient.InviteV2StrippedState, ) (bool, []fclient.InviteV2StrippedState, error) { @@ -78,7 +121,7 @@ func (r *Inviter) generateInviteStrippedStateNoNID( return (info != nil && !info.IsStub()), strippedState, nil } -func (r *Inviter) processInviteMembership( +func (r *Inviter) ProcessInviteMembership( ctx context.Context, inviteEvent *types.HeaderedEvent, ) ([]api.OutputEvent, error) { var outputUpdates []api.OutputEvent @@ -115,7 +158,6 @@ func (r *Inviter) HandleInvite( roomID := inviteEvent.RoomID() targetUserID := *inviteEvent.StateKey() - _, domain, err := gomatrixserverlib.SplitID('@', targetUserID) if err != nil { return nil, api.ErrInvalidID{Err: fmt.Errorf("the user ID %s is invalid", targetUserID)} @@ -129,13 +171,36 @@ func (r *Inviter) HandleInvite( if err != nil { return nil, err } - // HACK: What to do with this interface? - // NOTE: ??? - isKnownRoom, inviteState, err := r.generateInviteStrippedStateNoNID(ctx, *validRoomID, inviteEvent, inviteRoomState) + // HACK: Easy to inject this interface + isKnownRoom, err := r.IsKnownRoom(ctx, *validRoomID) if err != nil { return nil, err } + inviteState := inviteRoomState + if len(inviteState) == 0 { + // "If they are set on the room, at least the state for m.room.avatar, m.room.canonical_alias, m.room.join_rules, and m.room.name SHOULD be included." + // https://matrix.org/docs/spec/client_server/r0.6.0#m-room-member + stateWanted := []gomatrixserverlib.StateKeyTuple{} + for _, t := range []string{ + spec.MRoomName, spec.MRoomCanonicalAlias, + spec.MRoomJoinRules, spec.MRoomAvatar, + spec.MRoomEncryption, spec.MRoomCreate, + } { + stateWanted = append(stateWanted, gomatrixserverlib.StateKeyTuple{ + EventType: t, + StateKey: "", + }) + } + // HACK: Mostly easy to inject this interface? + // NOTE: Only hard bit is the fclient.InviteV2StrippedState return type + if is, err := r.GenerateInviteStrippedStateV2(ctx, *validRoomID, stateWanted, inviteEvent); err == nil { + inviteState = is + } else { + return nil, err + } + } + logger := util.GetLogger(ctx).WithFields(map[string]interface{}{ "inviter": inviteEvent.Sender(), "invitee": *inviteEvent.StateKey(), @@ -165,7 +230,7 @@ func (r *Inviter) HandleInvite( // an input event. Instead we will update the membership table with the // new invite and generate an output event. // HACK: Easy to inject this interface - return r.processInviteMembership(ctx, inviteEvent) + return r.ProcessInviteMembership(ctx, inviteEvent) } // HACK: Easy to inject this interface @@ -217,7 +282,7 @@ func (r *Inviter) HandleInvite( } // HACK: Easy to inject this interface - return r.processInviteMembership(ctx, inviteEvent) + return r.ProcessInviteMembership(ctx, inviteEvent) } // nolint:gocyclo @@ -326,7 +391,7 @@ func (r *Inviter) PerformInvite( _, err = helpers.CheckAuthEvents(ctx, r.DB, info, event, event.AuthEventIDs()) if err != nil { logger.WithError(err).WithField("event_id", event.EventID()).WithField("auth_event_ids", event.AuthEventIDs()).Error( - "processInviteEvent.checkAuthEvents failed for event", + "ProcessInviteEvent.checkAuthEvents failed for event", ) return nil, api.ErrNotAllowed{Err: err} }