Try roomserver perform join

This commit is contained in:
Neil Alexander 2020-05-01 16:02:42 +01:00
parent 38513fb4ed
commit f946de5f7c
5 changed files with 156 additions and 306 deletions

View file

@ -15,332 +15,49 @@
package routing
import (
"fmt"
"net/http"
"strings"
"time"
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
"github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
"github.com/matrix-org/dendrite/clientapi/httputil"
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/dendrite/clientapi/producers"
"github.com/matrix-org/dendrite/common"
"github.com/matrix-org/dendrite/common/config"
federationSenderAPI "github.com/matrix-org/dendrite/federationsender/api"
roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
"github.com/matrix-org/gomatrix"
"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/util"
)
// JoinRoomByIDOrAlias implements the "/join/{roomIDOrAlias}" API.
// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
func JoinRoomByIDOrAlias(
req *http.Request,
device *authtypes.Device,
roomIDOrAlias string,
cfg *config.Dendrite,
federation *gomatrixserverlib.FederationClient,
producer *producers.RoomserverProducer,
cfg *config.Dendrite, // nolint:unparam
federation *gomatrixserverlib.FederationClient, // nolint:unparam
producer *producers.RoomserverProducer, // nolint:unparam
rsAPI roomserverAPI.RoomserverInternalAPI,
fsAPI federationSenderAPI.FederationSenderInternalAPI,
keyRing gomatrixserverlib.KeyRing,
accountDB accounts.Database,
fsAPI federationSenderAPI.FederationSenderInternalAPI, // nolint:unparam
keyRing gomatrixserverlib.KeyRing, // nolint:unparam
accountDB accounts.Database, // nolint:unparam
) util.JSONResponse {
var content map[string]interface{} // must be a JSON object
if resErr := httputil.UnmarshalJSONRequest(req, &content); resErr != nil {
return *resErr
joinReq := roomserverAPI.PerformJoinRequest{
RoomIDOrAlias: roomIDOrAlias,
UserID: device.UserID,
Content: nil,
}
evTime, err := httputil.ParseTSParam(req)
if err != nil {
joinRes := roomserverAPI.PerformJoinResponse{}
if err := rsAPI.PerformJoin(req.Context(), &joinReq, &joinRes); err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.InvalidArgumentValue(err.Error()),
JSON: jsonerror.Unknown(err.Error()),
}
}
localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
return jsonerror.InternalServerError()
}
profile, err := accountDB.GetProfileByLocalpart(req.Context(), localpart)
if err != nil {
util.GetLogger(req.Context()).WithError(err).Error("accountDB.GetProfileByLocalpart failed")
return jsonerror.InternalServerError()
}
content["membership"] = gomatrixserverlib.Join
content["displayname"] = profile.DisplayName
content["avatar_url"] = profile.AvatarURL
r := joinRoomReq{
req, evTime, content, device.UserID, cfg, federation, producer,
rsAPI, fsAPI, keyRing,
}
if strings.HasPrefix(roomIDOrAlias, "!") {
return r.joinRoomByID(roomIDOrAlias)
}
if strings.HasPrefix(roomIDOrAlias, "#") {
return r.joinRoomByAlias(roomIDOrAlias)
}
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON(
fmt.Sprintf("Invalid first character '%s' for room ID or alias",
string([]rune(roomIDOrAlias)[0])), // Wrapping with []rune makes this call UTF-8 safe
),
}
}
type joinRoomReq struct {
req *http.Request
evTime time.Time
content map[string]interface{}
userID string
cfg *config.Dendrite
federation *gomatrixserverlib.FederationClient
producer *producers.RoomserverProducer
rsAPI roomserverAPI.RoomserverInternalAPI
fsAPI federationSenderAPI.FederationSenderInternalAPI
keyRing gomatrixserverlib.KeyRing
}
// joinRoomByID joins a room by room ID
func (r joinRoomReq) joinRoomByID(roomID string) util.JSONResponse {
// A client should only join a room by room ID when it has an invite
// to the room. If the server is already in the room then we can
// lookup the invite and process the request as a normal state event.
// If the server is not in the room the we will need to look up the
// remote server the invite came from in order to request a join event
// from that server.
queryReq := roomserverAPI.QueryInvitesForUserRequest{
RoomID: roomID, TargetUserID: r.userID,
}
var queryRes roomserverAPI.QueryInvitesForUserResponse
if err := r.rsAPI.QueryInvitesForUser(r.req.Context(), &queryReq, &queryRes); err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("r.queryAPI.QueryInvitesForUser failed")
return jsonerror.InternalServerError()
}
servers := []gomatrixserverlib.ServerName{}
seenInInviterIDs := map[gomatrixserverlib.ServerName]bool{}
for _, userID := range queryRes.InviteSenderUserIDs {
_, domain, err := gomatrixserverlib.SplitID('@', userID)
if err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
return jsonerror.InternalServerError()
}
if !seenInInviterIDs[domain] {
servers = append(servers, domain)
seenInInviterIDs[domain] = true
}
}
// Also add the domain extracted from the roomID as a last resort to join
// in case the client is erroneously trying to join by ID without an invite
// or all previous attempts at domains extracted from the inviter IDs fail
// Note: It's no guarantee we'll succeed because a room isn't bound to the domain in its ID
_, domain, err := gomatrixserverlib.SplitID('!', roomID)
if err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("gomatrixserverlib.SplitID failed")
return jsonerror.InternalServerError()
}
if domain != r.cfg.Matrix.ServerName && !seenInInviterIDs[domain] {
servers = append(servers, domain)
}
return r.joinRoomUsingServers(roomID, servers)
}
// joinRoomByAlias joins a room using a room alias.
func (r joinRoomReq) joinRoomByAlias(roomAlias string) util.JSONResponse {
_, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
if err != nil {
return util.JSONResponse{
Code: http.StatusBadRequest,
JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
}
}
if domain == r.cfg.Matrix.ServerName {
queryReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: roomAlias}
var queryRes roomserverAPI.GetRoomIDForAliasResponse
if err = r.rsAPI.GetRoomIDForAlias(r.req.Context(), &queryReq, &queryRes); err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("r.aliasAPI.GetRoomIDForAlias failed")
return jsonerror.InternalServerError()
}
if len(queryRes.RoomID) > 0 {
return r.joinRoomUsingServers(queryRes.RoomID, []gomatrixserverlib.ServerName{r.cfg.Matrix.ServerName})
}
// If the response doesn't contain a non-empty string, return an error
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Room alias " + roomAlias + " not found."),
}
}
// If the room isn't local, use federation to join
return r.joinRoomByRemoteAlias(domain, roomAlias)
}
func (r joinRoomReq) joinRoomByRemoteAlias(
domain gomatrixserverlib.ServerName, roomAlias string,
) util.JSONResponse {
resp, err := r.federation.LookupRoomAlias(r.req.Context(), domain, roomAlias)
if err != nil {
switch x := err.(type) {
case gomatrix.HTTPError:
if x.Code == http.StatusNotFound {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("Room alias not found"),
}
}
}
util.GetLogger(r.req.Context()).WithError(err).Error("r.federation.LookupRoomAlias failed")
return jsonerror.InternalServerError()
}
return r.joinRoomUsingServers(resp.RoomID, resp.Servers)
}
func (r joinRoomReq) writeToBuilder(eb *gomatrixserverlib.EventBuilder, roomID string) error {
eb.Type = "m.room.member"
err := eb.SetContent(r.content)
if err != nil {
return err
}
err = eb.SetUnsigned(struct{}{})
if err != nil {
return err
}
eb.Sender = r.userID
eb.StateKey = &r.userID
eb.RoomID = roomID
eb.Redacts = ""
return nil
}
func (r joinRoomReq) joinRoomUsingServers(
roomID string, servers []gomatrixserverlib.ServerName,
) util.JSONResponse {
var eb gomatrixserverlib.EventBuilder
err := r.writeToBuilder(&eb, roomID)
if err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("r.writeToBuilder failed")
return jsonerror.InternalServerError()
}
queryRes := roomserverAPI.QueryLatestEventsAndStateResponse{}
event, err := common.BuildEvent(r.req.Context(), &eb, r.cfg, r.evTime, r.rsAPI, &queryRes)
if err == nil {
// If we have successfully built an event at this point then we can
// assert that the room is a local room, as BuildEvent was able to
// add prev_events etc successfully.
if _, err = r.producer.SendEvents(
r.req.Context(),
[]gomatrixserverlib.HeaderedEvent{
(*event).Headered(queryRes.RoomVersion),
},
r.cfg.Matrix.ServerName,
nil,
); err != nil {
util.GetLogger(r.req.Context()).WithError(err).Error("r.producer.SendEvents failed")
return jsonerror.InternalServerError()
}
return util.JSONResponse{
Code: http.StatusOK,
JSON: struct {
RoomID string `json:"room_id"`
}{roomID},
}
}
// Otherwise, if we've reached here, then we haven't been able to populate
// prev_events etc for the room, therefore the room is probably federated.
// TODO: This needs to be re-thought, as in the case of an invite, the room
// will exist in the database in roomserver_rooms but won't have any state
// events, therefore this below check fails.
if err != common.ErrRoomNoExists {
util.GetLogger(r.req.Context()).WithError(err).Error("common.BuildEvent failed")
return jsonerror.InternalServerError()
}
if len(servers) == 0 {
return util.JSONResponse{
Code: http.StatusNotFound,
JSON: jsonerror.NotFound("No candidate servers found for room"),
}
}
var lastErr error
for _, server := range servers {
var response *util.JSONResponse
response, lastErr = r.joinRoomUsingServer(roomID, server)
if lastErr != nil {
// There was a problem talking to one of the servers.
util.GetLogger(r.req.Context()).WithError(lastErr).WithField("server", server).Warn("Failed to join room using server")
// Try the next server.
if r.req.Context().Err() != nil {
// The request context has expired so don't bother trying any
// more servers - they will immediately fail due to the expired
// context.
break
} else {
// The request context hasn't expired yet so try the next server.
continue
}
}
return *response
}
// Every server we tried to join through resulted in an error.
// We return the error from the last server.
// TODO: Generate the correct HTTP status code for all different
// kinds of errors that could have happened.
// The possible errors include:
// 1) We can't connect to the remote servers.
// 2) None of the servers we could connect to think we are allowed
// to join the room.
// 3) The remote server returned something invalid.
// 4) We couldn't fetch the public keys needed to verify the
// signatures on the state events.
// 5) ...
util.GetLogger(r.req.Context()).WithError(lastErr).Error("failed to join through any server")
return jsonerror.InternalServerError()
}
// joinRoomUsingServer tries to join a remote room using a given matrix server.
// If there was a failure communicating with the server or the response from the
// server was invalid this returns an error.
// Otherwise this returns a JSONResponse.
func (r joinRoomReq) joinRoomUsingServer(roomID string, server gomatrixserverlib.ServerName) (*util.JSONResponse, error) {
fedJoinReq := federationSenderAPI.PerformJoinRequest{
RoomID: roomID,
UserID: r.userID,
ServerName: server,
}
fedJoinRes := federationSenderAPI.PerformJoinResponse{}
if err := r.fsAPI.PerformJoin(r.req.Context(), &fedJoinReq, &fedJoinRes); err != nil {
return nil, err
}
return &util.JSONResponse{
Code: http.StatusOK,
// TODO: Put the response struct somewhere common.
JSON: struct {
RoomID string `json:"room_id"`
}{roomID},
}, nil
}{joinReq.RoomIDOrAlias},
}
}

View file

@ -39,7 +39,7 @@ func (r *FederationSenderInternalAPI) PerformJoin(
// 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"
respMakeJoin.JoinEvent.Type = "m.room.member"
respMakeJoin.JoinEvent.Type = gomatrixserverlib.MRoomMember
respMakeJoin.JoinEvent.Sender = request.UserID
respMakeJoin.JoinEvent.StateKey = &request.UserID
respMakeJoin.JoinEvent.RoomID = request.RoomID

View file

@ -18,6 +18,12 @@ type RoomserverInternalAPI interface {
response *InputRoomEventsResponse,
) error
PerformJoin(
ctx context.Context,
req *PerformJoinRequest,
res *PerformJoinResponse,
) error
// Query the latest events and state for a room from the room server.
QueryLatestEventsAndState(
ctx context.Context,

View file

@ -8,23 +8,22 @@ import (
)
const (
// RoomserverPerformJoinPath is the HTTP path for the PerformJoinRequest API.
// RoomserverPerformJoinPath is the HTTP path for the PerformJoin API.
RoomserverPerformJoinPath = "/api/roomserver/performJoin"
// RoomserverPerformLeavePath is the HTTP path for the PerformLeaveRequest API.
// RoomserverPerformLeavePath is the HTTP path for the PerformLeave API.
RoomserverPerformLeavePath = "/api/roomserver/performLeave"
)
type PerformJoinRequest struct {
RoomID string `json:"room_id"`
UserID string `json:"user_id"`
Content map[string]interface{} `json:"content"`
RoomIDOrAlias string `json:"room_id_or_alias"`
UserID string `json:"user_id"`
Content map[string]interface{} `json:"content"`
}
type PerformJoinResponse struct {
}
// Handle an instruction to make_join & send_join with a remote server.
func (h *httpRoomserverInternalAPI) PerformJoin(
ctx context.Context,
request *PerformJoinRequest,
@ -45,7 +44,6 @@ type PerformLeaveRequest struct {
type PerformLeaveResponse struct {
}
// Handle an instruction to make_leave & send_leave with a remote server.
func (h *httpRoomserverInternalAPI) PerformLeave(
ctx context.Context,
request *PerformLeaveRequest,

View file

@ -2,8 +2,14 @@ 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
@ -12,5 +18,128 @@ func (r *RoomserverInternalAPI) PerformJoin(
req *api.PerformJoinRequest,
res *api.PerformJoinResponse,
) 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 ID %q does not belong to this homeserver", req.UserID)
}
if strings.HasPrefix(req.RoomIDOrAlias, "!") {
return r.performJoinRoomByID(ctx, req, res)
}
if strings.HasPrefix(req.RoomIDOrAlias, "#") {
return r.performJoinRoomByAlias(ctx, req, res)
}
return fmt.Errorf("unexpected sigil on room %q", req.RoomIDOrAlias)
}
func (r *RoomserverInternalAPI) performJoinRoomByAlias(
ctx context.Context,
req *api.PerformJoinRequest,
res *api.PerformJoinResponse,
) error {
// Look up if we know this room alias.
roomID, err := r.DB.GetRoomIDForAlias(ctx, req.RoomIDOrAlias)
if err != nil {
return err
}
// If we do, then pluck out the room ID and continue the join.
req.RoomIDOrAlias = roomID
return r.performJoinRoomByID(ctx, req, res)
}
func (r *RoomserverInternalAPI) performJoinRoomByID(
ctx context.Context,
req *api.PerformJoinRequest,
res *api.PerformJoinResponse,
) error {
// Prepare the template for the join event.
userID := req.UserID
eb := gomatrixserverlib.EventBuilder{
Type: gomatrixserverlib.MRoomMember,
Sender: userID,
StateKey: &userID,
RoomID: req.RoomIDOrAlias,
Redacts: "",
}
if err := eb.SetUnsigned(struct{}{}); err != nil {
return fmt.Errorf("eb.SetUnsigned: %w", err)
}
// It is possible for the requestoto include some "content" for the
// event. We'll always overwrite the "membership" key, but the rest,
// like "display_name" or "avatar_url", will be kept if supplied.
if req.Content == nil {
req.Content = map[string]interface{}{}
}
req.Content["membership"] = "join"
if err := eb.SetContent(req.Content); err != nil {
return fmt.Errorf("eb.SetContent: %w", err)
}
// Try to construct an actual join event from the template.
// If this succeeds then it is a sign that the room already exists
// locally on the homeserver.
// 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 join event
r.Cfg, // the server configuration
time.Now(), // the event timestamp to use
r, // the roomserver API to use
&buildRes, // the query response
)
switch err {
case nil:
// The room join is local. Send the new join event into the
// roomserver.
inputReq := api.InputRoomEventsRequest{
InputRoomEvents: []api.InputRoomEvent{
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)
}
case common.ErrRoomNoExists:
// The room doesn't exist. First of all check if the room is a local
// room. If it is then there's nothing more to do - the room just
// hasn't been created yet.
if _, domain, derr := gomatrixserverlib.SplitID('!', req.RoomIDOrAlias); derr != nil {
return fmt.Errorf("room ID %q in incorrect format", req.RoomIDOrAlias)
} else if domain == r.Cfg.Matrix.ServerName {
return fmt.Errorf("error trying to join %q room: %w", req.RoomIDOrAlias, derr)
}
// Otherwise, if we've reached this point, the room isn't a local room
// and we should ask the federation sender to try and join for us.
fedReq := fsAPI.PerformJoinRequest{
RoomID: req.RoomIDOrAlias,
UserID: req.UserID,
ServerName: r.Cfg.Matrix.ServerName,
Content: req.Content,
}
fedRes := fsAPI.PerformJoinResponse{}
err = r.fsAPI.PerformJoin(ctx, &fedReq, &fedRes)
if err != nil {
return fmt.Errorf("error joining federated room %q: %w", req.RoomIDOrAlias, err)
}
default:
return fmt.Errorf("error joining room %q: %w", req.RoomIDOrAlias, err)
}
return nil
}