mirror of
https://github.com/matrix-org/dendrite
synced 2024-11-19 16:20:51 +01:00
537 lines
16 KiB
Go
537 lines
16 KiB
Go
// 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 (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/getsentry/sentry-go"
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
"github.com/matrix-org/gomatrixserverlib/spec"
|
|
|
|
appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
|
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
"github.com/matrix-org/dendrite/clientapi/httputil"
|
|
"github.com/matrix-org/dendrite/clientapi/threepid"
|
|
"github.com/matrix-org/dendrite/internal/eventutil"
|
|
"github.com/matrix-org/dendrite/roomserver/api"
|
|
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
|
|
"github.com/matrix-org/dendrite/roomserver/types"
|
|
"github.com/matrix-org/dendrite/setup/config"
|
|
userapi "github.com/matrix-org/dendrite/userapi/api"
|
|
|
|
"github.com/matrix-org/util"
|
|
)
|
|
|
|
func SendBan(
|
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, reqErr := extractRequestData(req)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
allowedToBan := pl.UserLevel(device.UserID) >= pl.Ban
|
|
if !allowedToBan {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("You don't have permission to ban this user, power level too low."),
|
|
}
|
|
}
|
|
|
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Ban, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
|
}
|
|
|
|
func sendMembership(ctx context.Context, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
roomID, membership, reason string, cfg *config.ClientAPI, targetUserID string, evTime time.Time,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI) util.JSONResponse {
|
|
|
|
event, err := buildMembershipEvent(
|
|
ctx, targetUserID, reason, profileAPI, device, membership,
|
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
|
)
|
|
if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
|
|
serverName := device.UserDomain()
|
|
if err = roomserverAPI.SendEvents(
|
|
ctx, rsAPI,
|
|
roomserverAPI.KindNew,
|
|
[]*types.HeaderedEvent{event},
|
|
device.UserDomain(),
|
|
serverName,
|
|
serverName,
|
|
nil,
|
|
false,
|
|
); err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("SendEvents failed")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|
|
|
|
func SendKick(
|
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, reqErr := extractRequestData(req)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
pl, errRes := getPowerlevels(req, rsAPI, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
allowedToKick := pl.UserLevel(device.UserID) >= pl.Kick
|
|
if !allowedToKick {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("You don't have permission to kick this user, power level too low."),
|
|
}
|
|
}
|
|
|
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: body.UserID,
|
|
}, &queryRes)
|
|
if err != nil {
|
|
return util.ErrorResponse(err)
|
|
}
|
|
// kick is only valid if the user is not currently banned or left (that is, they are joined or invited)
|
|
if queryRes.Membership != spec.Join && queryRes.Membership != spec.Invite {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Unknown("cannot /kick banned or left users"),
|
|
}
|
|
}
|
|
// TODO: should we be using SendLeave instead?
|
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
|
}
|
|
|
|
func SendUnban(
|
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, reqErr := extractRequestData(req)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
var queryRes roomserverAPI.QueryMembershipForUserResponse
|
|
err := rsAPI.QueryMembershipForUser(req.Context(), &roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: body.UserID,
|
|
}, &queryRes)
|
|
if err != nil {
|
|
return util.ErrorResponse(err)
|
|
}
|
|
|
|
// unban is only valid if the user is currently banned
|
|
if queryRes.Membership != spec.Ban {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.Unknown("can only /unban users that are banned"),
|
|
}
|
|
}
|
|
// TODO: should we be using SendLeave instead?
|
|
return sendMembership(req.Context(), profileAPI, device, roomID, spec.Leave, body.Reason, cfg, body.UserID, evTime, rsAPI, asAPI)
|
|
}
|
|
|
|
func SendInvite(
|
|
req *http.Request, profileAPI userapi.ClientUserAPI, device *userapi.Device,
|
|
roomID string, cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) util.JSONResponse {
|
|
body, evTime, reqErr := extractRequestData(req)
|
|
if reqErr != nil {
|
|
return *reqErr
|
|
}
|
|
|
|
inviteStored, jsonErrResp := checkAndProcessThreepid(
|
|
req, device, body, cfg, rsAPI, profileAPI, roomID, evTime,
|
|
)
|
|
if jsonErrResp != nil {
|
|
return *jsonErrResp
|
|
}
|
|
|
|
// If an invite has been stored on an identity server, it means that a
|
|
// m.room.third_party_invite event has been emitted and that we shouldn't
|
|
// emit a m.room.member one.
|
|
if inviteStored {
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|
|
|
|
if body.UserID == "" {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON("missing user_id"),
|
|
}
|
|
}
|
|
|
|
errRes := checkMemberInRoom(req.Context(), rsAPI, device.UserID, roomID)
|
|
if errRes != nil {
|
|
return *errRes
|
|
}
|
|
|
|
// We already received the return value, so no need to check for an error here.
|
|
response, _ := sendInvite(req.Context(), profileAPI, device, roomID, body.UserID, body.Reason, cfg, rsAPI, asAPI, evTime)
|
|
return response
|
|
}
|
|
|
|
// sendInvite sends an invitation to a user. Returns a JSONResponse and an error
|
|
func sendInvite(
|
|
ctx context.Context,
|
|
profileAPI userapi.ClientUserAPI,
|
|
device *userapi.Device,
|
|
roomID, userID, reason string,
|
|
cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
asAPI appserviceAPI.AppServiceInternalAPI, evTime time.Time,
|
|
) (util.JSONResponse, error) {
|
|
event, err := buildMembershipEvent(
|
|
ctx, userID, reason, profileAPI, device, spec.Invite,
|
|
roomID, false, cfg, evTime, rsAPI, asAPI,
|
|
)
|
|
if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("buildMembershipEvent failed")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}, err
|
|
}
|
|
|
|
err = rsAPI.PerformInvite(ctx, &api.PerformInviteRequest{
|
|
Event: event,
|
|
InviteRoomState: nil, // ask the roomserver to draw up invite room state for us
|
|
RoomVersion: event.Version(),
|
|
SendAsServer: string(device.UserDomain()),
|
|
})
|
|
|
|
switch e := err.(type) {
|
|
case roomserverAPI.ErrInvalidID:
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.Unknown(e.Error()),
|
|
}, e
|
|
case roomserverAPI.ErrNotAllowed:
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden(e.Error()),
|
|
}, e
|
|
case nil:
|
|
default:
|
|
util.GetLogger(ctx).WithError(err).Error("PerformInvite failed")
|
|
sentry.CaptureException(err)
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}, err
|
|
}
|
|
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}, nil
|
|
}
|
|
|
|
func buildMembershipEvent(
|
|
ctx context.Context,
|
|
targetUserID, reason string, profileAPI userapi.ClientUserAPI,
|
|
device *userapi.Device,
|
|
membership, roomID string, isDirect bool,
|
|
cfg *config.ClientAPI, evTime time.Time,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI, asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) (*types.HeaderedEvent, error) {
|
|
profile, err := loadProfile(ctx, targetUserID, cfg, profileAPI, asAPI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
proto := gomatrixserverlib.ProtoEvent{
|
|
Sender: device.UserID,
|
|
RoomID: roomID,
|
|
Type: "m.room.member",
|
|
StateKey: &targetUserID,
|
|
}
|
|
|
|
content := gomatrixserverlib.MemberContent{
|
|
Membership: membership,
|
|
DisplayName: profile.DisplayName,
|
|
AvatarURL: profile.AvatarURL,
|
|
Reason: reason,
|
|
IsDirect: isDirect,
|
|
}
|
|
|
|
if err = proto.SetContent(content); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
identity, err := cfg.Matrix.SigningIdentityFor(device.UserDomain())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return eventutil.QueryAndBuildEvent(ctx, &proto, cfg.Matrix, identity, evTime, rsAPI, nil)
|
|
}
|
|
|
|
// loadProfile lookups the profile of a given user from the database and returns
|
|
// it if the user is local to this server, or returns an empty profile if not.
|
|
// Returns an error if the retrieval failed or if the first parameter isn't a
|
|
// valid Matrix ID.
|
|
func loadProfile(
|
|
ctx context.Context,
|
|
userID string,
|
|
cfg *config.ClientAPI,
|
|
profileAPI userapi.ClientUserAPI,
|
|
asAPI appserviceAPI.AppServiceInternalAPI,
|
|
) (*authtypes.Profile, error) {
|
|
_, serverName, err := gomatrixserverlib.SplitID('@', userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var profile *authtypes.Profile
|
|
if cfg.Matrix.IsLocalServerName(serverName) {
|
|
profile, err = appserviceAPI.RetrieveUserProfile(ctx, userID, asAPI, profileAPI)
|
|
} else {
|
|
profile = &authtypes.Profile{}
|
|
}
|
|
|
|
return profile, err
|
|
}
|
|
|
|
func extractRequestData(req *http.Request) (body *threepid.MembershipRequest, evTime time.Time, resErr *util.JSONResponse) {
|
|
|
|
if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
|
|
resErr = reqErr
|
|
return
|
|
}
|
|
|
|
evTime, err := httputil.ParseTSParam(req)
|
|
if err != nil {
|
|
resErr = &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.InvalidParam(err.Error()),
|
|
}
|
|
return
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkAndProcessThreepid(
|
|
req *http.Request,
|
|
device *userapi.Device,
|
|
body *threepid.MembershipRequest,
|
|
cfg *config.ClientAPI,
|
|
rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
profileAPI userapi.ClientUserAPI,
|
|
roomID string,
|
|
evTime time.Time,
|
|
) (inviteStored bool, errRes *util.JSONResponse) {
|
|
|
|
inviteStored, err := threepid.CheckAndProcessInvite(
|
|
req.Context(), device, body, cfg, rsAPI, profileAPI,
|
|
roomID, evTime,
|
|
)
|
|
switch e := err.(type) {
|
|
case nil:
|
|
case threepid.ErrMissingParameter:
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON(err.Error()),
|
|
}
|
|
case threepid.ErrNotTrusted:
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.NotTrusted(body.IDServer),
|
|
}
|
|
case eventutil.ErrRoomNoExists:
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: spec.NotFound(err.Error()),
|
|
}
|
|
case gomatrixserverlib.BadJSONError:
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.BadJSON(e.Error()),
|
|
}
|
|
default:
|
|
util.GetLogger(req.Context()).WithError(err).Error("threepid.CheckAndProcessInvite failed")
|
|
return inviteStored, &util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func checkMemberInRoom(ctx context.Context, rsAPI roomserverAPI.ClientRoomserverAPI, userID, roomID string) *util.JSONResponse {
|
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
|
err := rsAPI.QueryMembershipForUser(ctx, &roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: userID,
|
|
}, &membershipRes)
|
|
if err != nil {
|
|
util.GetLogger(ctx).WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
|
return &util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
if !membershipRes.IsInRoom {
|
|
return &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("user does not belong to room"),
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SendForget(
|
|
req *http.Request, device *userapi.Device,
|
|
roomID string, rsAPI roomserverAPI.ClientRoomserverAPI,
|
|
) util.JSONResponse {
|
|
ctx := req.Context()
|
|
logger := util.GetLogger(ctx).WithField("roomID", roomID).WithField("userID", device.UserID)
|
|
var membershipRes roomserverAPI.QueryMembershipForUserResponse
|
|
membershipReq := roomserverAPI.QueryMembershipForUserRequest{
|
|
RoomID: roomID,
|
|
UserID: device.UserID,
|
|
}
|
|
err := rsAPI.QueryMembershipForUser(ctx, &membershipReq, &membershipRes)
|
|
if err != nil {
|
|
logger.WithError(err).Error("QueryMembershipForUser: could not query membership for user")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
if !membershipRes.RoomExists {
|
|
return util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("room does not exist"),
|
|
}
|
|
}
|
|
if membershipRes.IsInRoom {
|
|
return util.JSONResponse{
|
|
Code: http.StatusBadRequest,
|
|
JSON: spec.Unknown(fmt.Sprintf("User %s is in room %s", device.UserID, roomID)),
|
|
}
|
|
}
|
|
|
|
request := roomserverAPI.PerformForgetRequest{
|
|
RoomID: roomID,
|
|
UserID: device.UserID,
|
|
}
|
|
response := roomserverAPI.PerformForgetResponse{}
|
|
if err := rsAPI.PerformForget(ctx, &request, &response); err != nil {
|
|
logger.WithError(err).Error("PerformForget: unable to forget room")
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: spec.InternalServerError{},
|
|
}
|
|
}
|
|
return util.JSONResponse{
|
|
Code: http.StatusOK,
|
|
JSON: struct{}{},
|
|
}
|
|
}
|
|
|
|
func getPowerlevels(req *http.Request, rsAPI roomserverAPI.ClientRoomserverAPI, roomID string) (*gomatrixserverlib.PowerLevelContent, *util.JSONResponse) {
|
|
plEvent := roomserverAPI.GetStateEvent(req.Context(), rsAPI, roomID, gomatrixserverlib.StateKeyTuple{
|
|
EventType: spec.MRoomPowerLevels,
|
|
StateKey: "",
|
|
})
|
|
if plEvent == nil {
|
|
return nil, &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("You don't have permission to perform this action, no power_levels event in this room."),
|
|
}
|
|
}
|
|
pl, err := plEvent.PowerLevels()
|
|
if err != nil {
|
|
return nil, &util.JSONResponse{
|
|
Code: http.StatusForbidden,
|
|
JSON: spec.Forbidden("You don't have permission to perform this action, the power_levels event for this room is malformed so auth checks cannot be performed."),
|
|
}
|
|
}
|
|
return pl, nil
|
|
}
|