From 0e6416b68e12aaf863e86cc25ae231a142a5d377 Mon Sep 17 00:00:00 2001 From: Neil Alexander Date: Mon, 4 May 2020 15:45:36 +0100 Subject: [PATCH] First pass at PerformLeave --- clientapi/routing/leaveroom.go | 51 +++++++ clientapi/routing/routing.go | 14 +- federationsender/api/perform.go | 4 +- federationsender/internal/perform.go | 79 +++++++++- roomserver/api/api.go | 6 + roomserver/internal/api.go | 13 ++ roomserver/internal/perform_join.go | 2 +- roomserver/internal/perform_leave.go | 206 +++++++++++++++++++++++++++ 8 files changed, 370 insertions(+), 5 deletions(-) create mode 100644 clientapi/routing/leaveroom.go create mode 100644 roomserver/internal/perform_leave.go diff --git a/clientapi/routing/leaveroom.go b/clientapi/routing/leaveroom.go new file mode 100644 index 000000000..bd7696181 --- /dev/null +++ b/clientapi/routing/leaveroom.go @@ -0,0 +1,51 @@ +// Copyright 2017 Vector Creations Ltd +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package routing + +import ( + "net/http" + + "github.com/matrix-org/dendrite/clientapi/auth/authtypes" + "github.com/matrix-org/dendrite/clientapi/jsonerror" + roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/util" +) + +func LeaveRoomByID( + req *http.Request, + device *authtypes.Device, + rsAPI roomserverAPI.RoomserverInternalAPI, + roomID string, +) util.JSONResponse { + // Prepare to ask the roomserver to perform the room join. + leaveReq := roomserverAPI.PerformLeaveRequest{ + RoomID: roomID, + UserID: device.UserID, + } + leaveRes := roomserverAPI.PerformLeaveResponse{} + + // Ask the roomserver to perform the leave. + if err := rsAPI.PerformLeave(req.Context(), &leaveReq, &leaveRes); err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: jsonerror.Unknown(err.Error()), + } + } + + return util.JSONResponse{ + Code: http.StatusOK, + JSON: struct{}{}, + } +} diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index 3ceefa078..ead8a4c85 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -109,8 +109,18 @@ func Setup( return GetJoinedRooms(req, device, accountDB) }), ).Methods(http.MethodGet, http.MethodOptions) - - r0mux.Handle("/rooms/{roomID}/{membership:(?:join|kick|ban|unban|leave|invite)}", + r0mux.Handle("/rooms/{roomID}/leave", + common.MakeAuthAPI("membership", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse { + vars, err := common.URLDecodeMapValues(mux.Vars(req)) + if err != nil { + return util.ErrorResponse(err) + } + return LeaveRoomByID( + req, device, rsAPI, vars["roomID"], + ) + }), + ).Methods(http.MethodPost, http.MethodOptions) + r0mux.Handle("/rooms/{roomID}/{membership:(?:join|kick|ban|unban|invite)}", common.MakeAuthAPI("membership", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse { vars, err := common.URLDecodeMapValues(mux.Vars(req)) if err != nil { diff --git a/federationsender/api/perform.go b/federationsender/api/perform.go index a7b12adc7..2f643e5cf 100644 --- a/federationsender/api/perform.go +++ b/federationsender/api/perform.go @@ -67,7 +67,9 @@ func (h *httpFederationSenderInternalAPI) PerformJoin( } type PerformLeaveRequest struct { - RoomID string `json:"room_id"` + RoomID string `json:"room_id"` + UserID string `json:"user_id"` + ServerNames types.ServerNames `json:"server_names"` } type PerformLeaveResponse struct { diff --git a/federationsender/internal/perform.go b/federationsender/internal/perform.go index 161b689e1..5d998b8d3 100644 --- a/federationsender/internal/perform.go +++ b/federationsender/internal/perform.go @@ -153,5 +153,82 @@ func (r *FederationSenderInternalAPI) PerformLeave( request *api.PerformLeaveRequest, response *api.PerformLeaveResponse, ) (err error) { - return nil + // Deduplicate the server names we were provided. + util.Unique(request.ServerNames) + + // Try each server that we were provided until we land on one that + // successfully completes the make-leave send-leave dance. + for _, serverName := range request.ServerNames { + // Try to perform a make_leave using the information supplied in the + // request. + respMakeLeave, err := r.federation.MakeLeave( + ctx, + serverName, + request.RoomID, + request.UserID, + ) + if err != nil { + // TODO: Check if the user was not allowed to leave the room. + return fmt.Errorf("r.federation.MakeLeave: %w", err) + } + + // Set all the fields to be what they should be, this should be a no-op + // but it's possible that the remote server returned us something "odd" + respMakeLeave.LeaveEvent.Type = gomatrixserverlib.MRoomMember + respMakeLeave.LeaveEvent.Sender = request.UserID + respMakeLeave.LeaveEvent.StateKey = &request.UserID + respMakeLeave.LeaveEvent.RoomID = request.RoomID + respMakeLeave.LeaveEvent.Redacts = "" + if respMakeLeave.LeaveEvent.Content == nil { + content := map[string]interface{}{ + "membership": "leave", + } + if err = respMakeLeave.LeaveEvent.SetContent(content); err != nil { + return fmt.Errorf("respMakeLeave.LeaveEvent.SetContent: %w", err) + } + } + if err = respMakeLeave.LeaveEvent.SetUnsigned(struct{}{}); err != nil { + return fmt.Errorf("respMakeLeave.LeaveEvent.SetUnsigned: %w", err) + } + + // Work out if we support the room version that has been supplied in + // the make_leave response. + if respMakeLeave.RoomVersion == "" { + respMakeLeave.RoomVersion = gomatrixserverlib.RoomVersionV1 + } + if _, err = respMakeLeave.RoomVersion.EventFormat(); err != nil { + return fmt.Errorf("respMakeLeave.RoomVersion.EventFormat: %w", err) + } + + // Build the leave event. + event, err := respMakeLeave.LeaveEvent.Build( + time.Now(), + r.cfg.Matrix.ServerName, + r.cfg.Matrix.KeyID, + r.cfg.Matrix.PrivateKey, + respMakeLeave.RoomVersion, + ) + if err != nil { + return fmt.Errorf("respMakeLeave.LeaveEvent.Build: %w", err) + } + + // Try to perform a send_leave using the newly built event. + err = r.federation.SendLeave( + ctx, + serverName, + event, + ) + if err != nil { + logrus.WithError(err).Warnf("r.federation.SendLeave failed") + continue + } + + return nil + } + + // If we reach here then we didn't complete a leave for some reason. + return fmt.Errorf( + "failed to leave user %q from room %q through %d server(s)", + request.UserID, request.RoomID, len(request.ServerNames), + ) } diff --git a/roomserver/api/api.go b/roomserver/api/api.go index ae4beab21..aefe55bcd 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -24,6 +24,12 @@ type RoomserverInternalAPI interface { res *PerformJoinResponse, ) error + PerformLeave( + ctx context.Context, + req *PerformLeaveRequest, + res *PerformLeaveResponse, + ) error + // Query the latest events and state for a room from the room server. QueryLatestEventsAndState( ctx context.Context, diff --git a/roomserver/internal/api.go b/roomserver/internal/api.go index 1dc985efa..62389c237 100644 --- a/roomserver/internal/api.go +++ b/roomserver/internal/api.go @@ -59,6 +59,19 @@ func (r *RoomserverInternalAPI) SetupHTTP(servMux *http.ServeMux) { return util.JSONResponse{Code: http.StatusOK, JSON: &response} }), ) + servMux.Handle(api.RoomserverPerformLeavePath, + common.MakeInternalAPI("performLeave", func(req *http.Request) util.JSONResponse { + var request api.PerformLeaveRequest + var response api.PerformLeaveResponse + if err := json.NewDecoder(req.Body).Decode(&request); err != nil { + return util.MessageResponse(http.StatusBadRequest, err.Error()) + } + if err := r.PerformLeave(req.Context(), &request, &response); err != nil { + return util.ErrorResponse(err) + } + return util.JSONResponse{Code: http.StatusOK, JSON: &response} + }), + ) servMux.Handle( api.RoomserverQueryLatestEventsAndStatePath, common.MakeInternalAPI("queryLatestEventsAndState", func(req *http.Request) util.JSONResponse { diff --git a/roomserver/internal/perform_join.go b/roomserver/internal/perform_join.go index 3dfa118fd..502fe3833 100644 --- a/roomserver/internal/perform_join.go +++ b/roomserver/internal/perform_join.go @@ -156,7 +156,7 @@ func (r *RoomserverInternalAPI) performJoinRoomByID( if !alreadyJoined { inputReq := api.InputRoomEventsRequest{ InputRoomEvents: []api.InputRoomEvent{ - api.InputRoomEvent{ + { Kind: api.KindNew, Event: event.Headered(buildRes.RoomVersion), AuthEventIDs: event.AuthEventIDs(), diff --git a/roomserver/internal/perform_leave.go b/roomserver/internal/perform_leave.go new file mode 100644 index 000000000..65e429bb2 --- /dev/null +++ b/roomserver/internal/perform_leave.go @@ -0,0 +1,206 @@ +package internal + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/matrix-org/dendrite/common" + fsAPI "github.com/matrix-org/dendrite/federationsender/api" + "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/gomatrixserverlib" +) + +// WriteOutputEvents implements OutputRoomEventWriter +func (r *RoomserverInternalAPI) PerformLeave( + ctx context.Context, + req *api.PerformLeaveRequest, + res *api.PerformLeaveResponse, +) error { + _, domain, err := gomatrixserverlib.SplitID('@', req.UserID) + if err != nil { + return fmt.Errorf("Supplied user ID %q in incorrect format", req.UserID) + } + if domain != r.Cfg.Matrix.ServerName { + return fmt.Errorf("User %q does not belong to this homeserver", req.UserID) + } + if strings.HasPrefix(req.RoomID, "!") { + return r.performLeaveRoomByID(ctx, req, res) + } + return fmt.Errorf("Room ID %q is invalid", req.RoomID) +} + +func (r *RoomserverInternalAPI) performLeaveRoomByID( + ctx context.Context, + req *api.PerformLeaveRequest, + res *api.PerformLeaveResponse, // nolint:unparam +) error { + // If there's an invite outstanding for the room then respond to + // that. + senderUser, err := r.isInvitePending(ctx, req, res) + if err == nil { + fmt.Println("Responding to invite") + return r.performRejectInvite(ctx, req, res, senderUser) + } else { + fmt.Println("Not responding to invite:", err) + } + + // First of all we want to find out if the room exists and if the + // user is actually in it. + latestReq := api.QueryLatestEventsAndStateRequest{ + RoomID: req.RoomID, + StateToFetch: []gomatrixserverlib.StateKeyTuple{ + { + EventType: gomatrixserverlib.MRoomMember, + StateKey: req.UserID, + }, + }, + } + latestRes := api.QueryLatestEventsAndStateResponse{} + if err = r.QueryLatestEventsAndState(ctx, &latestReq, &latestRes); err != nil { + return err + } + if !latestRes.RoomExists { + return fmt.Errorf("room %q does not exist", req.RoomID) + } + + // Now let's see if the user is in the room. + if len(latestRes.StateEvents) == 0 { + return fmt.Errorf("user %q is not a member of room %q", req.UserID, req.RoomID) + } + membership, err := latestRes.StateEvents[0].Membership() + if err != nil { + return fmt.Errorf("error getting membership: %w", err) + } + if membership != "join" { + // TODO: should be able to handle "invite" in this case too, if + // it's a case of kicking or banning or such + return fmt.Errorf("user %q is not joined to the room (membership is %q)", req.UserID, membership) + } + + // Prepare the template for the leave event. + userID := req.UserID + eb := gomatrixserverlib.EventBuilder{ + Type: gomatrixserverlib.MRoomMember, + Sender: userID, + StateKey: &userID, + RoomID: req.RoomID, + Redacts: "", + } + if err = eb.SetContent(map[string]interface{}{"membership": "leave"}); err != nil { + return fmt.Errorf("eb.SetContent: %w", err) + } + if err = eb.SetUnsigned(struct{}{}); err != nil { + return fmt.Errorf("eb.SetUnsigned: %w", err) + } + + // We know that the user is in the room at this point so let's build + // a leave event. + // TODO: Check what happens if the room exists on the server + // but everyone has since left. I suspect it does the wrong thing. + buildRes := api.QueryLatestEventsAndStateResponse{} + event, err := common.BuildEvent( + ctx, // the request context + &eb, // the template join event + r.Cfg, // the server configuration + time.Now(), // the event timestamp to use + r, // the roomserver API to use + &buildRes, // the query response + ) + if err != nil { + return fmt.Errorf("common.BuildEvent: %w", err) + } + + // Give our leave event to the roomserver input stream. The + // roomserver will process the membership change and notify + // downstream automatically. + inputReq := api.InputRoomEventsRequest{ + InputRoomEvents: []api.InputRoomEvent{ + { + Kind: api.KindNew, + Event: event.Headered(buildRes.RoomVersion), + AuthEventIDs: event.AuthEventIDs(), + SendAsServer: string(r.Cfg.Matrix.ServerName), + }, + }, + } + inputRes := api.InputRoomEventsResponse{} + if err = r.InputRoomEvents(ctx, &inputReq, &inputRes); err != nil { + return fmt.Errorf("r.InputRoomEvents: %w", err) + } + + return nil +} + +func (r *RoomserverInternalAPI) performRejectInvite( + ctx context.Context, + req *api.PerformLeaveRequest, + res *api.PerformLeaveResponse, // nolint:unparam + senderUser string, +) error { + _, domain, err := gomatrixserverlib.SplitID('@', senderUser) + if err != nil { + return fmt.Errorf("user ID %q invalid: %w", senderUser, err) + } + + // Ask the federation sender to perform a federated leave for us. + leaveReq := fsAPI.PerformLeaveRequest{ + RoomID: req.RoomID, + UserID: req.UserID, + ServerNames: []gomatrixserverlib.ServerName{domain}, + } + leaveRes := fsAPI.PerformLeaveResponse{} + if err := r.fsAPI.PerformLeave(ctx, &leaveReq, &leaveRes); err != nil { + return fmt.Errorf("fsAPI.PerformLeave: %w", err) + } + + // If this succeeded then we can clean up the invite. + fmt.Println("REMOVE THE INVITE!") + + return nil +} + +func (r *RoomserverInternalAPI) isInvitePending( + ctx context.Context, + req *api.PerformLeaveRequest, + res *api.PerformLeaveResponse, // nolint:unparam +) (string, error) { + // Look up the room NID for the supplied room ID. + roomNID, err := r.DB.RoomNID(ctx, req.RoomID) + if err != nil { + return "", fmt.Errorf("r.DB.RoomNID: %w", err) + } + + // Look up the state key NID for the supplied user ID. + targetUserNIDs, err := r.DB.EventStateKeyNIDs(ctx, []string{req.UserID}) + if err != nil { + return "", fmt.Errorf("r.DB.EventStateKeyNIDs: %w", err) + } + targetUserNID, targetUserFound := targetUserNIDs[req.UserID] + if !targetUserFound { + return "", fmt.Errorf("missing NID for user %q (%+v)", req.UserID, targetUserNIDs) + } + + // Let's see if we have an event active for the user in the room. If + // we do then it will contain a server name that we can direct the + // send_leave to. + senderUserNIDs, err := r.DB.GetInvitesForUser(ctx, roomNID, targetUserNID) + if err != nil { + return "", fmt.Errorf("r.DB.GetInvitesForUser: %w", err) + } + fmt.Println("Sender user NIDs:", senderUserNIDs) + + // Look up the user ID from the NID. + senderUsers, err := r.DB.EventStateKeys(ctx, senderUserNIDs) + if err != nil { + return "", fmt.Errorf("r.DB.EventStateKeys: %w", err) + } + fmt.Println("Sender users:", senderUsers) + senderUser, senderUserFound := senderUsers[senderUserNIDs[0]] + if !senderUserFound { + return "", fmt.Errorf("missing user for NID %d (%+v)", senderUserNIDs[0], senderUsers) + } + + return senderUser, nil +}