dendrite/federationapi/routing/leave.go
Neil Alexander 6cb1a65809
Synchronous invites (#1273)
* Refactor invites to be synchronous

* Fix synchronous invites

* Fix client API return type for send invite error

* Linter

* Restore PerformError on rsAPI.PerformInvite

* Update sytest-whitelist

* Don't override PerformError with normal errors

* Fix error passing

* Un-whitelist a couple of tests

* Update sytest-whitelist

* Try to handle multiple invite rejections better

* nolint

* Update gomatrixserverlib

* Fix /v1/invite test

* Remove replace from go.mod
2020-08-17 11:40:49 +01:00

266 lines
8.3 KiB
Go

// 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"
"time"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/internal/config"
"github.com/matrix-org/dendrite/internal/eventutil"
"github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
// MakeLeave implements the /make_leave API
// nolint:gocyclo
func MakeLeave(
httpReq *http.Request,
request *gomatrixserverlib.FederationRequest,
cfg *config.FederationAPI,
rsAPI api.RoomserverInternalAPI,
roomID, userID string,
) util.JSONResponse {
_, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("Invalid UserID"),
}
}
if domain != request.Origin() {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("The leave must be sent by the server of the user"),
}
}
// Try building an event for the server
builder := gomatrixserverlib.EventBuilder{
Sender: userID,
RoomID: roomID,
Type: "m.room.member",
StateKey: &userID,
}
err = builder.SetContent(map[string]interface{}{"membership": gomatrixserverlib.Leave})
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("builder.SetContent failed")
return jsonerror.InternalServerError()
}
var queryRes api.QueryLatestEventsAndStateResponse
event, err := eventutil.BuildEvent(httpReq.Context(), &builder, cfg.Matrix, time.Now(), rsAPI, &queryRes)
if err == eventutil.ErrRoomNoExists {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Room does not exist"),
}
} else if e, ok := err.(gomatrixserverlib.BadJSONError); ok {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON(e.Error()),
}
} else if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("eventutil.BuildEvent failed")
return jsonerror.InternalServerError()
}
// If the user has already left then just return their last leave
// event. This means that /send_leave will be a no-op, which helps
// to reject invites multiple times - hopefully.
for _, state := range queryRes.StateEvents {
if state.Type() == gomatrixserverlib.MRoomMember && state.StateKeyEquals(userID) {
if mem, merr := state.Membership(); merr == nil && mem == gomatrixserverlib.Leave {
return util.JSONResponse{
Code: http.StatusOK,
JSON: map[string]interface{}{
"room_version": event.RoomVersion,
"event": state,
},
}
}
}
}
// Check that the leave is allowed or not
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(event.Event, &provider); err != nil {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden(err.Error()),
}
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: map[string]interface{}{
"room_version": event.RoomVersion,
"event": builder,
},
}
}
// SendLeave implements the /send_leave API
// nolint:gocyclo
func SendLeave(
httpReq *http.Request,
request *gomatrixserverlib.FederationRequest,
cfg *config.FederationAPI,
rsAPI api.RoomserverInternalAPI,
keys gomatrixserverlib.JSONVerifier,
roomID, eventID 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.StatusBadRequest,
JSON: jsonerror.UnsupportedRoomVersion(err.Error()),
}
}
// Decode the event JSON from the request.
event, err := gomatrixserverlib.NewEventFromUntrustedJSON(request.Content(), verRes.RoomVersion)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.NotJSON("The request body could not be decoded into valid JSON. " + err.Error()),
}
}
// Check that the room ID is correct.
if event.RoomID() != roomID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("The room ID in the request path must match the room ID in the leave event JSON"),
}
}
// Check that the event ID is correct.
if event.EventID() != eventID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("The event ID in the request path must match the event ID in the leave event JSON"),
}
}
// Check that the event is from the server sending the request.
if event.Origin() != request.Origin() {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("The leave must be sent by the server it originated on"),
}
}
// Check if the user has already left. If so, no-op!
queryReq := &api.QueryLatestEventsAndStateRequest{
RoomID: roomID,
StateToFetch: []gomatrixserverlib.StateKeyTuple{
{
EventType: gomatrixserverlib.MRoomMember,
StateKey: *event.StateKey(),
},
},
}
queryRes := &api.QueryLatestEventsAndStateResponse{}
err = rsAPI.QueryLatestEventsAndState(httpReq.Context(), queryReq, queryRes)
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("rsAPI.QueryLatestEventsAndState failed")
return jsonerror.InternalServerError()
}
// The room doesn't exist or we weren't ever joined to it. Might as well
// no-op here.
if !queryRes.RoomExists || len(queryRes.StateEvents) == 0 {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
// Check if we're recycling a previous leave event.
if event.EventID() == queryRes.StateEvents[0].EventID() {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
// We are/were joined/invited/banned or something. Check if
// we can no-op here.
if len(queryRes.StateEvents) == 1 {
if mem, merr := queryRes.StateEvents[0].Membership(); merr == nil && mem == gomatrixserverlib.Leave {
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}
}
// Check that the event is signed by the server sending the request.
redacted := event.Redact()
verifyRequests := []gomatrixserverlib.VerifyJSONRequest{{
ServerName: event.Origin(),
Message: redacted.JSON(),
AtTS: event.OriginServerTS(),
StrictValidityChecking: true,
}}
verifyResults, err := keys.VerifyJSONs(httpReq.Context(), verifyRequests)
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("keys.VerifyJSONs failed")
return jsonerror.InternalServerError()
}
if verifyResults[0].Error != nil {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: jsonerror.Forbidden("The leave must be signed by the server it originated on"),
}
}
// check membership is set to leave
mem, err := event.Membership()
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("event.Membership failed")
return jsonerror.InternalServerError()
}
if mem != gomatrixserverlib.Leave {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("The membership in the event content must be set to leave"),
}
}
// Send the events to the room server.
// We are responsible for notifying other servers that the user has left
// the room, so set SendAsServer to cfg.Matrix.ServerName
_, err = api.SendEvents(
httpReq.Context(), rsAPI,
[]gomatrixserverlib.HeaderedEvent{
event.Headered(verRes.RoomVersion),
},
cfg.Matrix.ServerName,
nil,
)
if err != nil {
util.GetLogger(httpReq.Context()).WithError(err).Error("producer.SendEvents failed")
return jsonerror.InternalServerError()
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct{}{},
}
}