1647213fac
As outlined in https://github.com/matrix-org/gomatrixserverlib/pull/368 The main change Dendrite side is that `RoomVersion` no longer has any methods on it. Instead, you need to bounce via `gmsl.GetRoomVersion`. It's very interesting to see where exactly Dendrite cares about this. For some places it's creating events (fine) but others are way more specific. Those areas will need to migrate to GMSL at some point.
535 lines
18 KiB
Go
535 lines
18 KiB
Go
// Copyright 2017 New Vector 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 (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
"github.com/matrix-org/gomatrixserverlib/fclient"
|
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
"github.com/matrix-org/util"
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
"github.com/matrix-org/dendrite/roomserver/api"
|
|
"github.com/matrix-org/dendrite/setup/config"
|
|
)
|
|
|
|
// MakeJoin implements the /make_join API
|
|
func MakeJoin(
|
|
httpReq *http.Request,
|
|
request *fclient.FederationRequest,
|
|
cfg *config.FederationAPI,
|
|
rsAPI api.FederationRoomserverAPI,
|
|
roomID, userID string,
|
|
remoteVersions []gomatrixserverlib.RoomVersion,
|
|
) 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(),
|
|
}
|
|
}
|
|
|
|
// Check that the room that the remote side is trying to join is actually
|
|
// one of the room versions that they listed in their supported ?ver= in
|
|
// the make_join URL.
|
|
// https://matrix.org/docs/spec/server_server/r0.1.3#get-matrix-federation-v1-make-join-roomid-userid
|
|
remoteSupportsVersion := false
|
|
for _, v := range remoteVersions {
|
|
if v == verRes.RoomVersion {
|
|
remoteSupportsVersion = true
|
|
break
|
|
}
|
|
}
|
|
// If it isn't, stop trying to join the room.
|
|
if !remoteSupportsVersion {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.IncompatibleRoomVersion(verRes.RoomVersion),
|
|
}
|
|
}
|
|
|
|
_, 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 join must be sent by the server of the user"),
|
|
}
|
|
}
|
|
|
|
// Check if we think we are still joined to the room
|
|
inRoomReq := &api.QueryServerJoinedToRoomRequest{
|
|
ServerName: cfg.Matrix.ServerName,
|
|
RoomID: roomID,
|
|
}
|
|
inRoomRes := &api.QueryServerJoinedToRoomResponse{}
|
|
if err = rsAPI.QueryServerJoinedToRoom(httpReq.Context(), inRoomReq, inRoomRes); err != nil {
|
|
util.GetLogger(httpReq.Context()).WithError(err).Error("rsAPI.QueryServerJoinedToRoom failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
if !inRoomRes.RoomExists {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(fmt.Sprintf("Room ID %q was not found on this server", roomID)),
|
|
}
|
|
}
|
|
if !inRoomRes.IsInRoom {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(fmt.Sprintf("Room ID %q has no remaining users on this server", roomID)),
|
|
}
|
|
}
|
|
|
|
// Check if the restricted join is allowed. If the room doesn't
|
|
// support restricted joins then this is effectively a no-op.
|
|
res, authorisedVia, err := checkRestrictedJoin(httpReq, rsAPI, verRes.RoomVersion, roomID, userID)
|
|
if err != nil {
|
|
util.GetLogger(httpReq.Context()).WithError(err).Error("checkRestrictedJoin failed")
|
|
return jsonerror.InternalServerError()
|
|
} else if res != nil {
|
|
return *res
|
|
}
|
|
|
|
// Try building an event for the server
|
|
builder := gomatrixserverlib.EventBuilder{
|
|
Sender: userID,
|
|
RoomID: roomID,
|
|
Type: "m.room.member",
|
|
StateKey: &userID,
|
|
}
|
|
content := gomatrixserverlib.MemberContent{
|
|
Membership: spec.Join,
|
|
AuthorisedVia: authorisedVia,
|
|
}
|
|
if err = builder.SetContent(content); err != nil {
|
|
util.GetLogger(httpReq.Context()).WithError(err).Error("builder.SetContent failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
identity, err := cfg.Matrix.SigningIdentityFor(request.Destination())
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound(
|
|
fmt.Sprintf("Server name %q does not exist", request.Destination()),
|
|
),
|
|
}
|
|
}
|
|
|
|
queryRes := api.QueryLatestEventsAndStateResponse{
|
|
RoomVersion: verRes.RoomVersion,
|
|
}
|
|
event, err := eventutil.QueryAndBuildEvent(httpReq.Context(), &builder, cfg.Matrix, identity, 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()
|
|
}
|
|
|
|
// Check that the join 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{}{
|
|
"event": builder,
|
|
"room_version": verRes.RoomVersion,
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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:
|
|
// nolint:gocyclo
|
|
func SendJoin(
|
|
httpReq *http.Request,
|
|
request *fclient.FederationRequest,
|
|
cfg *config.FederationAPI,
|
|
rsAPI api.FederationRoomserverAPI,
|
|
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 {
|
|
util.GetLogger(httpReq.Context()).WithError(err).Error("rsAPI.QueryRoomVersionForRoom failed")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: jsonerror.InternalServerError(),
|
|
}
|
|
}
|
|
verImpl, err := gomatrixserverlib.GetRoomVersion(verRes.RoomVersion)
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: jsonerror.UnsupportedRoomVersion(
|
|
fmt.Sprintf("QueryRoomVersionForRoom returned unknown room version: %s", verRes.RoomVersion),
|
|
),
|
|
}
|
|
}
|
|
|
|
event, err := verImpl.NewEventFromUntrustedJSON(request.Content())
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON: " + err.Error()),
|
|
}
|
|
}
|
|
|
|
// Check that a state key is provided.
|
|
if event.StateKey() == nil || event.StateKeyEquals("") {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("No state key was provided in the join event."),
|
|
}
|
|
}
|
|
if !event.StateKeyEquals(event.Sender()) {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("Event state key must match the event sender."),
|
|
}
|
|
}
|
|
|
|
// Check that the sender belongs to the server that is sending us
|
|
// the request. By this point we've already asserted that the sender
|
|
// and the state key are equal so we don't need to check both.
|
|
var serverName spec.ServerName
|
|
if _, serverName, err = gomatrixserverlib.SplitID('@', event.Sender()); err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("The sender of the join is invalid"),
|
|
}
|
|
} else if serverName != request.Origin() {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("The sender does not match the server that originated the request"),
|
|
}
|
|
}
|
|
|
|
// Check that the room ID is correct.
|
|
if event.RoomID() != roomID {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(
|
|
fmt.Sprintf(
|
|
"The room ID in the request path (%q) must match the room ID in the join event JSON (%q)",
|
|
roomID, event.RoomID(),
|
|
),
|
|
),
|
|
}
|
|
}
|
|
|
|
// Check that the event ID is correct.
|
|
if event.EventID() != eventID {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(
|
|
fmt.Sprintf(
|
|
"The event ID in the request path (%q) must match the event ID in the join event JSON (%q)",
|
|
eventID, event.EventID(),
|
|
),
|
|
),
|
|
}
|
|
}
|
|
|
|
// Check that this is in fact a join event
|
|
membership, err := event.Membership()
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("missing content.membership key"),
|
|
}
|
|
}
|
|
if membership != spec.Join {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("membership must be 'join'"),
|
|
}
|
|
}
|
|
|
|
// Check that the event is signed by the server sending the request.
|
|
redacted, err := verImpl.RedactEventJSON(event.JSON())
|
|
if err != nil {
|
|
logrus.WithError(err).Errorf("XXX: join.go")
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON("The event JSON could not be redacted"),
|
|
}
|
|
}
|
|
verifyRequests := []gomatrixserverlib.VerifyJSONRequest{{
|
|
ServerName: serverName,
|
|
Message: redacted,
|
|
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("Signature check failed: " + verifyResults[0].Error.Error()),
|
|
}
|
|
}
|
|
|
|
// Fetch the state and auth chain. We do this before we send the events
|
|
// on, in case this fails.
|
|
var stateAndAuthChainResponse api.QueryStateAndAuthChainResponse
|
|
err = rsAPI.QueryStateAndAuthChain(httpReq.Context(), &api.QueryStateAndAuthChainRequest{
|
|
PrevEventIDs: event.PrevEventIDs(),
|
|
AuthEventIDs: event.AuthEventIDs(),
|
|
RoomID: roomID,
|
|
ResolveState: true,
|
|
}, &stateAndAuthChainResponse)
|
|
if err != nil {
|
|
util.GetLogger(httpReq.Context()).WithError(err).Error("rsAPI.QueryStateAndAuthChain failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
if !stateAndAuthChainResponse.RoomExists {
|
|
return util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound("Room does not exist"),
|
|
}
|
|
}
|
|
if !stateAndAuthChainResponse.StateKnown {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("State not known"),
|
|
}
|
|
}
|
|
|
|
// Check if the user is already in the room. If they're already in then
|
|
// there isn't much point in sending another join event into the room.
|
|
// Also check to see if they are banned: if they are then we reject them.
|
|
alreadyJoined := false
|
|
isBanned := false
|
|
for _, se := range stateAndAuthChainResponse.StateEvents {
|
|
if !se.StateKeyEquals(*event.StateKey()) {
|
|
continue
|
|
}
|
|
if membership, merr := se.Membership(); merr == nil {
|
|
alreadyJoined = (membership == spec.Join)
|
|
isBanned = (membership == spec.Ban)
|
|
break
|
|
}
|
|
}
|
|
|
|
if isBanned {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("user is banned"),
|
|
}
|
|
}
|
|
|
|
// If the membership content contains a user ID for a server that is not
|
|
// ours then we should kick it back.
|
|
var memberContent gomatrixserverlib.MemberContent
|
|
if err := json.Unmarshal(event.Content(), &memberContent); err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(err.Error()),
|
|
}
|
|
}
|
|
if memberContent.AuthorisedVia != "" {
|
|
_, domain, err := gomatrixserverlib.SplitID('@', memberContent.AuthorisedVia)
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(fmt.Sprintf("The authorising username %q is invalid.", memberContent.AuthorisedVia)),
|
|
}
|
|
}
|
|
if domain != cfg.Matrix.ServerName {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.BadJSON(fmt.Sprintf("The authorising username %q does not belong to this server.", memberContent.AuthorisedVia)),
|
|
}
|
|
}
|
|
}
|
|
|
|
// Sign the membership event. This is required for restricted joins to work
|
|
// in the case that the authorised via user is one of our own users. It also
|
|
// doesn't hurt to do it even if it isn't a restricted join.
|
|
signed := event.Sign(
|
|
string(cfg.Matrix.ServerName),
|
|
cfg.Matrix.KeyID,
|
|
cfg.Matrix.PrivateKey,
|
|
)
|
|
|
|
// Send the events to the room server.
|
|
// We are responsible for notifying other servers that the user has joined
|
|
// the room, so set SendAsServer to cfg.Matrix.ServerName
|
|
if !alreadyJoined {
|
|
var response api.InputRoomEventsResponse
|
|
if err := rsAPI.InputRoomEvents(httpReq.Context(), &api.InputRoomEventsRequest{
|
|
InputRoomEvents: []api.InputRoomEvent{
|
|
{
|
|
Kind: api.KindNew,
|
|
Event: signed.Headered(stateAndAuthChainResponse.RoomVersion),
|
|
SendAsServer: string(cfg.Matrix.ServerName),
|
|
TransactionID: nil,
|
|
},
|
|
},
|
|
}, &response); err != nil {
|
|
return jsonerror.InternalAPIError(httpReq.Context(), err)
|
|
}
|
|
if response.ErrMsg != "" {
|
|
util.GetLogger(httpReq.Context()).WithField(logrus.ErrorKey, response.ErrMsg).Error("SendEvents failed")
|
|
if response.NotAllowed {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.Forbidden(response.ErrMsg),
|
|
}
|
|
}
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
}
|
|
|
|
// sort events deterministically by depth (lower is earlier)
|
|
// We also do this because sytest's basic federation server isn't good at using the correct
|
|
// state if these lists are randomised, resulting in flakey tests. :(
|
|
sort.Sort(eventsByDepth(stateAndAuthChainResponse.StateEvents))
|
|
sort.Sort(eventsByDepth(stateAndAuthChainResponse.AuthChainEvents))
|
|
|
|
// https://matrix.org/docs/spec/server_server/latest#put-matrix-federation-v1-send-join-roomid-eventid
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: fclient.RespSendJoin{
|
|
StateEvents: gomatrixserverlib.NewEventJSONsFromHeaderedEvents(stateAndAuthChainResponse.StateEvents),
|
|
AuthEvents: gomatrixserverlib.NewEventJSONsFromHeaderedEvents(stateAndAuthChainResponse.AuthChainEvents),
|
|
Origin: cfg.Matrix.ServerName,
|
|
Event: signed.JSON(),
|
|
},
|
|
}
|
|
}
|
|
|
|
// checkRestrictedJoin finds out whether or not we can assist in processing
|
|
// a restricted room join. If the room version does not support restricted
|
|
// joins then this function returns with no side effects. This returns three
|
|
// values:
|
|
// - an optional JSON response body (i.e. M_UNABLE_TO_AUTHORISE_JOIN) which
|
|
// should always be sent back to the client if one is specified
|
|
// - a user ID of an authorising user, typically a user that has power to
|
|
// issue invites in the room, if one has been found
|
|
// - an error if there was a problem finding out if this was allowable,
|
|
// like if the room version isn't known or a problem happened talking to
|
|
// the roomserver
|
|
func checkRestrictedJoin(
|
|
httpReq *http.Request,
|
|
rsAPI api.FederationRoomserverAPI,
|
|
roomVersion gomatrixserverlib.RoomVersion,
|
|
roomID, userID string,
|
|
) (*util.JSONResponse, string, error) {
|
|
verImpl, err := gomatrixserverlib.GetRoomVersion(roomVersion)
|
|
if err != nil {
|
|
return nil, "", err
|
|
}
|
|
if !verImpl.MayAllowRestrictedJoinsInEventAuth() {
|
|
return nil, "", nil
|
|
}
|
|
req := &api.QueryRestrictedJoinAllowedRequest{
|
|
RoomID: roomID,
|
|
UserID: userID,
|
|
}
|
|
res := &api.QueryRestrictedJoinAllowedResponse{}
|
|
if err := rsAPI.QueryRestrictedJoinAllowed(httpReq.Context(), req, res); err != nil {
|
|
return nil, "", err
|
|
}
|
|
|
|
switch {
|
|
case !res.Restricted:
|
|
// The join rules for the room don't restrict membership.
|
|
return nil, "", nil
|
|
|
|
case !res.Resident:
|
|
// The join rules restrict membership but our server isn't currently
|
|
// joined to all of the allowed rooms, so we can't actually decide
|
|
// whether or not to allow the user to join. This error code should
|
|
// tell the joining server to try joining via another resident server
|
|
// instead.
|
|
return &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: jsonerror.UnableToAuthoriseJoin("This server cannot authorise the join."),
|
|
}, "", nil
|
|
|
|
case !res.Allowed:
|
|
// The join rules restrict membership, our server is in the relevant
|
|
// rooms and the user wasn't joined to join any of the allowed rooms
|
|
// and therefore can't join this room.
|
|
return &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: jsonerror.Forbidden("You are not joined to any matching rooms."),
|
|
}, "", nil
|
|
|
|
default:
|
|
// The join rules restrict membership, our server is in the relevant
|
|
// rooms and the user was allowed to join because they belong to one
|
|
// of the allowed rooms. We now need to pick one of our own local users
|
|
// from within the room to use as the authorising user ID, so that it
|
|
// can be referred to from within the membership content.
|
|
return nil, res.AuthorisedVia, nil
|
|
}
|
|
}
|
|
|
|
type eventsByDepth []*gomatrixserverlib.HeaderedEvent
|
|
|
|
func (e eventsByDepth) Len() int {
|
|
return len(e)
|
|
}
|
|
func (e eventsByDepth) Swap(i, j int) {
|
|
e[i], e[j] = e[j], e[i]
|
|
}
|
|
func (e eventsByDepth) Less(i, j int) bool {
|
|
return e[i].Depth() < e[j].Depth()
|
|
}
|