diff --git a/clientapi/routing/joinroom.go b/clientapi/routing/joinroom.go index df83c2a9f..c710a4b3f 100644 --- a/clientapi/routing/joinroom.go +++ b/clientapi/routing/joinroom.go @@ -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}, + } } diff --git a/federationsender/internal/perform.go b/federationsender/internal/perform.go index 961d80276..64774b65d 100644 --- a/federationsender/internal/perform.go +++ b/federationsender/internal/perform.go @@ -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 diff --git a/roomserver/api/api.go b/roomserver/api/api.go index c12dbddde..ae4beab21 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -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, diff --git a/roomserver/api/perform.go b/roomserver/api/perform.go index 5725c4fb2..82f815780 100644 --- a/roomserver/api/perform.go +++ b/roomserver/api/perform.go @@ -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, diff --git a/roomserver/internal/perform_join.go b/roomserver/internal/perform_join.go index ec9414ed0..7a0b5c922 100644 --- a/roomserver/internal/perform_join.go +++ b/roomserver/internal/perform_join.go @@ -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 }