diff --git a/clientapi/routing/leaveroom.go b/clientapi/routing/leaveroom.go new file mode 100644 index 00000000..bd769618 --- /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 3ceefa07..ead8a4c8 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/federationapi/routing/leave.go b/federationapi/routing/leave.go index 1124bfa2..bab3fd0b 100644 --- a/federationapi/routing/leave.go +++ b/federationapi/routing/leave.go @@ -33,6 +33,15 @@ func MakeLeave( rsAPI api.RoomserverInternalAPI, roomID, userID string, ) util.JSONResponse { + verReq := api.QueryRoomVersionForRoomRequest{RoomID: roomID} + verRes := api.QueryRoomVersionForRoomResponse{} + if err := rsAPI.QueryRoomVersionForRoom(httpReq.Context(), &verReq, &verRes); err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: jsonerror.InternalServerError(), + } + } + _, domain, err := gomatrixserverlib.SplitID('@', userID) if err != nil { return util.JSONResponse{ @@ -87,7 +96,10 @@ func MakeLeave( return util.JSONResponse{ Code: http.StatusOK, - JSON: map[string]interface{}{"event": builder}, + JSON: map[string]interface{}{ + "room_version": verRes.RoomVersion, + "event": builder, + }, } } diff --git a/federationsender/api/perform.go b/federationsender/api/perform.go index a7b12adc..2f643e5c 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 161b689e..ff7f821c 100644 --- a/federationsender/internal/perform.go +++ b/federationsender/internal/perform.go @@ -153,5 +153,83 @@ 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. + logrus.WithError(err).Warnf("r.federation.MakeLeave failed") + continue + } + + // 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 { + logrus.WithError(err).Warnf("respMakeLeave.LeaveEvent.SetContent failed") + continue + } + } + if err = respMakeLeave.LeaveEvent.SetUnsigned(struct{}{}); err != nil { + logrus.WithError(err).Warnf("respMakeLeave.LeaveEvent.SetUnsigned failed") + continue + } + + // Work out if we support the room version that has been supplied in + // the make_leave response. + if _, err = respMakeLeave.RoomVersion.EventFormat(); err != nil { + return gomatrixserverlib.UnsupportedRoomVersionError{} + } + + // 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 { + logrus.WithError(err).Warnf("respMakeLeave.LeaveEvent.Build failed") + continue + } + + // 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 room %q through %d server(s)", + request.RoomID, len(request.ServerNames), + ) } diff --git a/go.mod b/go.mod index 019edbf3..5188a7f5 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/matrix-org/go-http-js-libp2p v0.0.0-20200318135427-31631a9ef51f github.com/matrix-org/go-sqlite3-js v0.0.0-20200325174927-327088cdef10 github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26 - github.com/matrix-org/gomatrixserverlib v0.0.0-20200504142819-073764319c0f + github.com/matrix-org/gomatrixserverlib v0.0.0-20200504153202-7542702abea6 github.com/matrix-org/naffka v0.0.0-20200422140631-181f1ee7401f github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7 github.com/mattn/go-sqlite3 v2.0.2+incompatible diff --git a/go.sum b/go.sum index c9f89c6d..80839ead 100644 --- a/go.sum +++ b/go.sum @@ -367,8 +367,8 @@ github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26 h1:Hr3zjRsq2bh github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26/go.mod h1:3fxX6gUjWyI/2Bt7J1OLhpCzOfO/bB3AiX0cJtEKud0= github.com/matrix-org/gomatrixserverlib v0.0.0-20200124100636-0c2ec91d1df5 h1:kmRjpmFOenVpOaV/DRlo9p6z/IbOKlUC+hhKsAAh8Qg= github.com/matrix-org/gomatrixserverlib v0.0.0-20200124100636-0c2ec91d1df5/go.mod h1:FsKa2pWE/bpQql9H7U4boOPXFoJX/QcqaZZ6ijLkaZI= -github.com/matrix-org/gomatrixserverlib v0.0.0-20200504142819-073764319c0f h1:RiQ+YLu/S5Oi2Tm2QpBfO3bNxinhFtpZiar13kswLmY= -github.com/matrix-org/gomatrixserverlib v0.0.0-20200504142819-073764319c0f/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU= +github.com/matrix-org/gomatrixserverlib v0.0.0-20200504153202-7542702abea6 h1:CnU+0kV1xzpvzEkFr1tX7c9BTWCTFOIlBPM1XD9I++c= +github.com/matrix-org/gomatrixserverlib v0.0.0-20200504153202-7542702abea6/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU= github.com/matrix-org/naffka v0.0.0-20200127221512-0716baaabaf1 h1:osLoFdOy+ChQqVUn2PeTDETFftVkl4w9t/OW18g3lnk= github.com/matrix-org/naffka v0.0.0-20200127221512-0716baaabaf1/go.mod h1:cXoYQIENbdWIQHt1SyCo6Bl3C3raHwJ0wgVrXHSqf+A= github.com/matrix-org/naffka v0.0.0-20200422140631-181f1ee7401f h1:pRz4VTiRCO4zPlEMc3ESdUOcW4PXHH4Kj+YDz1XyE+Y= diff --git a/roomserver/api/api.go b/roomserver/api/api.go index ae4beab2..aefe55bc 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 1dc985ef..62389c23 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 3dfa118f..99e10d97 100644 --- a/roomserver/internal/perform_join.go +++ b/roomserver/internal/perform_join.go @@ -116,7 +116,7 @@ func (r *RoomserverInternalAPI) performJoinRoomByID( if req.Content == nil { req.Content = map[string]interface{}{} } - req.Content["membership"] = "join" + req.Content["membership"] = gomatrixserverlib.Join if err = eb.SetContent(req.Content); err != nil { return fmt.Errorf("eb.SetContent: %w", err) } @@ -145,7 +145,7 @@ func (r *RoomserverInternalAPI) performJoinRoomByID( for _, se := range buildRes.StateEvents { if membership, merr := se.Membership(); merr == nil { if se.StateKey() != nil && *se.StateKey() == *event.StateKey() { - alreadyJoined = (membership == "join") + alreadyJoined = (membership == gomatrixserverlib.Join) break } } @@ -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 00000000..422748e6 --- /dev/null +++ b/roomserver/internal/perform_leave.go @@ -0,0 +1,208 @@ +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. + isInvitePending, senderUser, err := r.isInvitePending(ctx, req, res) + if err == nil && isInvitePending { + return r.performRejectInvite(ctx, req, res, senderUser) + } + + // There's no invite pending, so 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 != gomatrixserverlib.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 leave 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 err + } + + // TODO: Withdraw the invite, so that the sync API etc are + // notified that we rejected it. + + return nil +} + +func (r *RoomserverInternalAPI) isInvitePending( + ctx context.Context, + req *api.PerformLeaveRequest, + res *api.PerformLeaveResponse, // nolint:unparam +) (bool, string, error) { + // Look up the room NID for the supplied room ID. + roomNID, err := r.DB.RoomNID(ctx, req.RoomID) + if err != nil { + return false, "", 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 false, "", fmt.Errorf("r.DB.EventStateKeyNIDs: %w", err) + } + targetUserNID, targetUserFound := targetUserNIDs[req.UserID] + if !targetUserFound { + return false, "", 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 false, "", fmt.Errorf("r.DB.GetInvitesForUser: %w", err) + } + if len(senderUserNIDs) == 0 { + return false, "", nil + } + + // Look up the user ID from the NID. + senderUsers, err := r.DB.EventStateKeys(ctx, senderUserNIDs) + if err != nil { + return false, "", fmt.Errorf("r.DB.EventStateKeys: %w", err) + } + if len(senderUsers) == 0 { + return false, "", fmt.Errorf("no senderUsers") + } + + senderUser, senderUserFound := senderUsers[senderUserNIDs[0]] + if !senderUserFound { + return false, "", fmt.Errorf("missing user for NID %d (%+v)", senderUserNIDs[0], senderUsers) + } + + return true, senderUser, nil +} diff --git a/roomserver/storage/sqlite3/event_state_keys_table.go b/roomserver/storage/sqlite3/event_state_keys_table.go index fa8fc57e..204b4eb6 100644 --- a/roomserver/storage/sqlite3/event_state_keys_table.go +++ b/roomserver/storage/sqlite3/event_state_keys_table.go @@ -47,14 +47,14 @@ const selectEventStateKeyNIDSQL = ` // Bulk lookup from string state key to numeric ID for that state key. // Takes an array of strings as the query parameter. -const bulkSelectEventStateKeyNIDSQL = ` +const bulkSelectEventStateKeySQL = ` SELECT event_state_key, event_state_key_nid FROM roomserver_event_state_keys WHERE event_state_key IN ($1) ` // Bulk lookup from numeric ID to string state key for that state key. // Takes an array of strings as the query parameter. -const bulkSelectEventStateKeySQL = ` +const bulkSelectEventStateKeyNIDSQL = ` SELECT event_state_key, event_state_key_nid FROM roomserver_event_state_keys WHERE event_state_key_nid IN ($1) ` @@ -110,7 +110,7 @@ func (s *eventStateKeyStatements) bulkSelectEventStateKeyNID( for k, v := range eventStateKeys { iEventStateKeys[k] = v } - selectOrig := strings.Replace(bulkSelectEventStateKeyNIDSQL, "($1)", common.QueryVariadic(len(eventStateKeys)), 1) + selectOrig := strings.Replace(bulkSelectEventStateKeySQL, "($1)", common.QueryVariadic(len(eventStateKeys)), 1) rows, err := txn.QueryContext(ctx, selectOrig, iEventStateKeys...) if err != nil {