Move Invite logic to GMSL (#3086)

This is both the federation receiving & sending side logic (which were
previously entangeld in a single function)
This commit is contained in:
devonh 2023-05-31 16:33:49 +00:00 committed by GitHub
parent cbdc601f1b
commit ea6b368ad4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 359 additions and 442 deletions

View file

@ -62,7 +62,7 @@ type RoomserverFederationAPI interface {
// Handle an instruction to make_leave & send_leave with a remote server.
PerformLeave(ctx context.Context, request *PerformLeaveRequest, response *PerformLeaveResponse) error
// Handle sending an invite to a remote server.
PerformInvite(ctx context.Context, request *PerformInviteRequest, response *PerformInviteResponse) error
SendInvite(ctx context.Context, event gomatrixserverlib.PDU, strippedState []gomatrixserverlib.InviteStrippedState) (gomatrixserverlib.PDU, error)
// Handle an instruction to peek a room on a remote server.
PerformOutboundPeek(ctx context.Context, request *PerformOutboundPeekRequest, response *PerformOutboundPeekResponse) error
// Query the server names of the joined hosts in a room.
@ -190,9 +190,9 @@ type PerformLeaveResponse struct {
}
type PerformInviteRequest struct {
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
Event *rstypes.HeaderedEvent `json:"event"`
InviteRoomState []fclient.InviteV2StrippedState `json:"invite_room_state"`
RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"`
Event *rstypes.HeaderedEvent `json:"event"`
InviteRoomState []gomatrixserverlib.InviteStrippedState `json:"invite_room_state"`
}
type PerformInviteResponse struct {

View file

@ -503,60 +503,59 @@ func (r *FederationInternalAPI) PerformLeave(
)
}
// PerformLeaveRequest implements api.FederationInternalAPI
func (r *FederationInternalAPI) PerformInvite(
// SendInvite implements api.FederationInternalAPI
func (r *FederationInternalAPI) SendInvite(
ctx context.Context,
request *api.PerformInviteRequest,
response *api.PerformInviteResponse,
) (err error) {
_, origin, err := r.cfg.Matrix.SplitLocalID('@', request.Event.Sender())
event gomatrixserverlib.PDU,
strippedState []gomatrixserverlib.InviteStrippedState,
) (gomatrixserverlib.PDU, error) {
_, origin, err := r.cfg.Matrix.SplitLocalID('@', event.Sender())
if err != nil {
return err
return nil, err
}
if request.Event.StateKey() == nil {
return errors.New("invite must be a state event")
if event.StateKey() == nil {
return nil, errors.New("invite must be a state event")
}
_, destination, err := gomatrixserverlib.SplitID('@', *request.Event.StateKey())
_, destination, err := gomatrixserverlib.SplitID('@', *event.StateKey())
if err != nil {
return fmt.Errorf("gomatrixserverlib.SplitID: %w", err)
return nil, fmt.Errorf("gomatrixserverlib.SplitID: %w", err)
}
// TODO (devon): This should be allowed via a relay. Currently only transactions
// can be sent to relays. Would need to extend relays to handle invites.
if !r.shouldAttemptDirectFederation(destination) {
return fmt.Errorf("relay servers have no meaningful response for invite.")
return nil, fmt.Errorf("relay servers have no meaningful response for invite.")
}
logrus.WithFields(logrus.Fields{
"event_id": request.Event.EventID(),
"user_id": *request.Event.StateKey(),
"room_id": request.Event.RoomID(),
"room_version": request.RoomVersion,
"event_id": event.EventID(),
"user_id": *event.StateKey(),
"room_id": event.RoomID(),
"room_version": event.Version(),
"destination": destination,
}).Info("Sending invite")
inviteReq, err := fclient.NewInviteV2Request(request.Event.PDU, request.InviteRoomState)
inviteReq, err := fclient.NewInviteV2Request(event, strippedState)
if err != nil {
return fmt.Errorf("gomatrixserverlib.NewInviteV2Request: %w", err)
return nil, fmt.Errorf("gomatrixserverlib.NewInviteV2Request: %w", err)
}
inviteRes, err := r.federation.SendInviteV2(ctx, origin, destination, inviteReq)
if err != nil {
return fmt.Errorf("r.federation.SendInviteV2: failed to send invite: %w", err)
return nil, fmt.Errorf("r.federation.SendInviteV2: failed to send invite: %w", err)
}
verImpl, err := gomatrixserverlib.GetRoomVersion(request.RoomVersion)
verImpl, err := gomatrixserverlib.GetRoomVersion(event.Version())
if err != nil {
return err
return nil, err
}
inviteEvent, err := verImpl.NewEventFromUntrustedJSON(inviteRes.Event)
if err != nil {
return fmt.Errorf("r.federation.SendInviteV2 failed to decode event response: %w", err)
return nil, fmt.Errorf("r.federation.SendInviteV2 failed to decode event response: %w", err)
}
response.Event = &types.HeaderedEvent{PDU: inviteEvent}
return nil
return inviteEvent, nil
}
// PerformServersAlive implements api.FederationInternalAPI

View file

@ -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"
@ -34,7 +33,7 @@ import (
func InviteV2(
httpReq *http.Request,
request *fclient.FederationRequest,
roomID string,
roomID spec.RoomID,
eventID string,
cfg *config.FederationAPI,
rsAPI api.FederationRoomserverAPI,
@ -56,9 +55,55 @@ func InviteV2(
JSON: spec.BadJSON(err.Error()),
}
case nil:
return processInvite(
httpReq.Context(), true, inviteReq.Event(), inviteReq.RoomVersion(), inviteReq.InviteRoomState(), roomID, eventID, cfg, rsAPI, keys,
)
if inviteReq.Event().StateKey() == nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The invite event has no state key"),
}
}
invitedUser, userErr := spec.NewUserID(*inviteReq.Event().StateKey(), true)
if userErr != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam("The user ID is invalid"),
}
}
if !cfg.Matrix.IsLocalServerName(invitedUser.Domain()) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam("The invited user domain does not belong to this server"),
}
}
if inviteReq.Event().EventID() != eventID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The event ID in the request path must match the event ID in the invite event JSON"),
}
}
input := gomatrixserverlib.HandleInviteInput{
RoomVersion: inviteReq.RoomVersion(),
RoomID: roomID,
InvitedUser: *invitedUser,
KeyID: cfg.Matrix.KeyID,
PrivateKey: cfg.Matrix.PrivateKey,
Verifier: keys,
RoomQuerier: rsAPI,
MembershipQuerier: &api.MembershipQuerier{Roomserver: rsAPI},
StateQuerier: rsAPI.StateQuerier(),
InviteEvent: inviteReq.Event(),
StrippedState: inviteReq.InviteRoomState(),
}
event, jsonErr := handleInvite(httpReq.Context(), input, rsAPI)
if jsonErr != nil {
return *jsonErr
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: fclient.RespInviteV2{Event: event.JSON()},
}
default:
return util.JSONResponse{
Code: http.StatusBadRequest,
@ -71,7 +116,7 @@ func InviteV2(
func InviteV1(
httpReq *http.Request,
request *fclient.FederationRequest,
roomID string,
roomID spec.RoomID,
eventID string,
cfg *config.FederationAPI,
rsAPI api.FederationRoomserverAPI,
@ -94,55 +139,11 @@ func InviteV1(
JSON: spec.NotJSON("The request body could not be decoded into an invite v1 request. " + err.Error()),
}
}
var strippedState []fclient.InviteV2StrippedState
if err := json.Unmarshal(event.Unsigned(), &strippedState); err != nil {
var strippedState []gomatrixserverlib.InviteStrippedState
if jsonErr := json.Unmarshal(event.Unsigned(), &strippedState); jsonErr != nil {
// just warn, they may not have added any.
util.GetLogger(httpReq.Context()).Warnf("failed to extract stripped state from invite event")
}
return processInvite(
httpReq.Context(), false, event, roomVer, strippedState, roomID, eventID, cfg, rsAPI, keys,
)
}
func processInvite(
ctx context.Context,
isInviteV2 bool,
event gomatrixserverlib.PDU,
roomVer gomatrixserverlib.RoomVersion,
strippedState []fclient.InviteV2StrippedState,
roomID string,
eventID string,
cfg *config.FederationAPI,
rsAPI api.FederationRoomserverAPI,
keys gomatrixserverlib.JSONVerifier,
) util.JSONResponse {
// Check that we can accept invites for this room version.
verImpl, err := gomatrixserverlib.GetRoomVersion(roomVer)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.UnsupportedRoomVersion(
fmt.Sprintf("Room version %q is not supported by this server.", roomVer),
),
}
}
// Check that the room ID is correct.
if event.RoomID() != roomID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The room ID in the request path must match the room ID in the invite event JSON"),
}
}
// Check that the event ID is correct.
if event.EventID() != eventID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The event ID in the request path must match the event ID in the invite event JSON"),
}
}
if event.StateKey() == nil {
return util.JSONResponse{
@ -151,105 +152,91 @@ func processInvite(
}
}
_, domain, err := cfg.Matrix.SplitLocalID('@', *event.StateKey())
invitedUser, err := spec.NewUserID(*event.StateKey(), true)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam(fmt.Sprintf("The user ID is invalid or domain %q does not belong to this server", domain)),
JSON: spec.InvalidParam("The user ID is invalid"),
}
}
// Check that the event is signed by the server sending the request.
redacted, err := verImpl.RedactEventJSON(event.JSON())
if err != nil {
if !cfg.Matrix.IsLocalServerName(invitedUser.Domain()) {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The event JSON could not be redacted"),
JSON: spec.InvalidParam("The invited user domain does not belong to this server"),
}
}
_, serverName, err := gomatrixserverlib.SplitID('@', event.Sender())
if err != nil {
if event.EventID() != eventID {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.BadJSON("The event JSON contains an invalid sender"),
}
}
verifyRequests := []gomatrixserverlib.VerifyJSONRequest{{
ServerName: serverName,
Message: redacted,
AtTS: event.OriginServerTS(),
StrictValidityChecking: true,
}}
verifyResults, err := keys.VerifyJSONs(ctx, verifyRequests)
if err != nil {
util.GetLogger(ctx).WithError(err).Error("keys.VerifyJSONs failed")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
}
if verifyResults[0].Error != nil {
return util.JSONResponse{
Code: http.StatusForbidden,
JSON: spec.Forbidden("The invite must be signed by the server it originated on"),
JSON: spec.BadJSON("The event ID in the request path must match the event ID in the invite event JSON"),
}
}
// Sign the event so that other servers will know that we have received the invite.
signedEvent := event.Sign(
string(domain), cfg.Matrix.KeyID, cfg.Matrix.PrivateKey,
)
// Add the invite event to the roomserver.
inviteEvent := &types.HeaderedEvent{PDU: signedEvent}
request := &api.PerformInviteRequest{
Event: inviteEvent,
InviteRoomState: strippedState,
RoomVersion: inviteEvent.Version(),
SendAsServer: string(api.DoNotSendToOtherServers),
TransactionID: nil,
input := gomatrixserverlib.HandleInviteInput{
RoomVersion: roomVer,
RoomID: roomID,
InvitedUser: *invitedUser,
KeyID: cfg.Matrix.KeyID,
PrivateKey: cfg.Matrix.PrivateKey,
Verifier: keys,
RoomQuerier: rsAPI,
MembershipQuerier: &api.MembershipQuerier{Roomserver: rsAPI},
StateQuerier: rsAPI.StateQuerier(),
InviteEvent: event,
StrippedState: strippedState,
}
if err = rsAPI.PerformInvite(ctx, request); err != nil {
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
event, jsonErr := handleInvite(httpReq.Context(), input, rsAPI)
if jsonErr != nil {
return *jsonErr
}
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:
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
sentry.CaptureException(err)
return util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
}
// Return the signed event to the originating server, it should then tell
// the other servers in the room that we have been invited.
if isInviteV2 {
return util.JSONResponse{
Code: http.StatusOK,
JSON: fclient.RespInviteV2{Event: signedEvent.JSON()},
}
} else {
return util.JSONResponse{
Code: http.StatusOK,
JSON: fclient.RespInvite{Event: signedEvent.JSON()},
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: fclient.RespInvite{Event: event.JSON()},
}
}
func handleInvite(ctx context.Context, input gomatrixserverlib.HandleInviteInput, rsAPI api.FederationRoomserverAPI) (gomatrixserverlib.PDU, *util.JSONResponse) {
inviteEvent, err := gomatrixserverlib.HandleInvite(ctx, input)
switch e := err.(type) {
case nil:
case spec.InternalServerError:
util.GetLogger(ctx).WithError(err)
return nil, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
case spec.MatrixError:
util.GetLogger(ctx).WithError(err)
code := http.StatusInternalServerError
switch e.ErrCode {
case spec.ErrorForbidden:
code = http.StatusForbidden
case spec.ErrorUnsupportedRoomVersion:
fallthrough // http.StatusBadRequest
case spec.ErrorBadJSON:
code = http.StatusBadRequest
}
return nil, &util.JSONResponse{
Code: code,
JSON: e,
}
default:
util.GetLogger(ctx).WithError(err)
return nil, &util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.Unknown("unknown error"),
}
}
headeredInvite := &types.HeaderedEvent{PDU: inviteEvent}
if err = rsAPI.HandleInvite(ctx, headeredInvite); err != nil {
util.GetLogger(ctx).WithError(err).Error("HandleInvite failed")
return nil, &util.JSONResponse{
Code: http.StatusInternalServerError,
JSON: spec.InternalServerError{},
}
}
return inviteEvent, nil
}

View file

@ -216,25 +216,6 @@ func MakeJoin(
}
}
type MembershipQuerier struct {
roomserver api.FederationRoomserverAPI
}
func (mq *MembershipQuerier) CurrentMembership(ctx context.Context, roomID spec.RoomID, userID spec.UserID) (string, error) {
req := api.QueryMembershipForUserRequest{
RoomID: roomID.String(),
UserID: userID.String(),
}
res := api.QueryMembershipForUserResponse{}
err := mq.roomserver.QueryMembershipForUser(ctx, &req, &res)
membership := ""
if err == nil {
membership = res.Membership
}
return membership, err
}
// SendJoin implements the /send_join API
// The make-join send-join dance makes much more sense as a single
// flow so the cyclomatic complexity is high:
@ -268,7 +249,7 @@ func SendJoin(
KeyID: cfg.Matrix.KeyID,
PrivateKey: cfg.Matrix.PrivateKey,
Verifier: keys,
MembershipQuerier: &MembershipQuerier{roomserver: rsAPI},
MembershipQuerier: &api.MembershipQuerier{Roomserver: rsAPI},
}
response, joinErr := gomatrixserverlib.HandleSendJoin(input)
switch e := joinErr.(type) {

View file

@ -152,8 +152,16 @@ func Setup(
JSON: spec.Forbidden("Forbidden by server ACLs"),
}
}
roomID, err := spec.NewRoomID(vars["roomID"])
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam("Invalid RoomID"),
}
}
return InviteV1(
httpReq, request, vars["roomID"], vars["eventID"],
httpReq, request, *roomID, vars["eventID"],
cfg, rsAPI, keys,
)
},
@ -168,8 +176,16 @@ func Setup(
JSON: spec.Forbidden("Forbidden by server ACLs"),
}
}
roomID, err := spec.NewRoomID(vars["roomID"])
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: spec.InvalidParam("Invalid RoomID"),
}
}
return InviteV2(
httpReq, request, vars["roomID"], vars["eventID"],
httpReq, request, *roomID, vars["eventID"],
cfg, rsAPI, keys,
)
},