diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go index 47e3ba1c3..a1d9004db 100644 --- a/clientapi/routing/createroom.go +++ b/clientapi/routing/createroom.go @@ -48,6 +48,7 @@ type createRoomRequest struct { RoomVersion gomatrixserverlib.RoomVersion `json:"room_version"` PowerLevelContentOverride json.RawMessage `json:"power_level_content_override"` IsDirect bool `json:"is_direct"` + SenderID string `json:"sender_id"` } func (r createRoomRequest) Validate() *util.JSONResponse { @@ -107,6 +108,208 @@ type createRoomResponse struct { RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec } +// CreateRoomCryptoIDs implements /createRoom +func CreateRoomCryptoIDs( + req *http.Request, device *api.Device, + cfg *config.ClientAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, +) util.JSONResponse { + var createRequest createRoomRequest + resErr := httputil.UnmarshalJSONRequest(req, &createRequest) + if resErr != nil { + return *resErr + } + if resErr = createRequest.Validate(); resErr != nil { + return *resErr + } + evTime, err := httputil.ParseTSParam(req) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.InvalidParam(err.Error()), + } + } + + return makeCreateRoomEvents(req.Context(), createRequest, device, cfg, profileAPI, rsAPI, asAPI, evTime) +} + +func makeCreateRoomEvents( + ctx context.Context, + createRequest createRoomRequest, device *api.Device, + cfg *config.ClientAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, + evTime time.Time, +) util.JSONResponse { + userID, err := spec.NewUserID(device.UserID, true) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("invalid userID") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{}, + } + } + if !cfg.Matrix.IsLocalServerName(userID.Domain()) { + return util.JSONResponse{ + Code: http.StatusForbidden, + JSON: spec.Forbidden(fmt.Sprintf("User domain %q not configured locally", userID.Domain())), + } + } + + logger := util.GetLogger(ctx) + + // TODO: Check room ID doesn't clash with an existing one, and we + // probably shouldn't be using pseudo-random strings, maybe GUIDs? + roomID, err := spec.NewRoomID(fmt.Sprintf("!%s:%s", util.RandomString(16), userID.Domain())) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("invalid roomID") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{}, + } + } + + // Clobber keys: creator, room_version + + roomVersion := rsAPI.DefaultRoomVersion() + if createRequest.RoomVersion != "" { + candidateVersion := gomatrixserverlib.RoomVersion(createRequest.RoomVersion) + _, roomVersionError := roomserverVersion.SupportedRoomVersion(candidateVersion) + if roomVersionError != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.UnsupportedRoomVersion(roomVersionError.Error()), + } + } + roomVersion = candidateVersion + } + + logger.WithFields(log.Fields{ + "userID": userID.String(), + "roomID": roomID.String(), + "roomVersion": roomVersion, + }).Info("Creating new room") + + profile, err := appserviceAPI.RetrieveUserProfile(ctx, userID.String(), asAPI, profileAPI) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("appserviceAPI.RetrieveUserProfile failed") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{}, + } + } + + userDisplayName := profile.DisplayName + userAvatarURL := profile.AvatarURL + + keyID := cfg.Matrix.KeyID + privateKey := cfg.Matrix.PrivateKey + + req := roomserverAPI.PerformCreateRoomRequest{ + InvitedUsers: createRequest.Invite, + RoomName: createRequest.Name, + Visibility: createRequest.Visibility, + Topic: createRequest.Topic, + StatePreset: createRequest.Preset, + CreationContent: createRequest.CreationContent, + InitialState: createRequest.InitialState, + RoomAliasName: createRequest.RoomAliasName, + RoomVersion: roomVersion, + PowerLevelContentOverride: createRequest.PowerLevelContentOverride, + IsDirect: createRequest.IsDirect, + + UserDisplayName: userDisplayName, + UserAvatarURL: userAvatarURL, + KeyID: keyID, + PrivateKey: privateKey, + EventTime: evTime, + + SenderID: createRequest.SenderID, + } + + createEvents, err := rsAPI.PerformCreateRoomCryptoIDs(ctx, *userID, *roomID, &req) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("MakeCreateRoomEvents failed") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{Err: err.Error()}, + } + } + + response := createRoomCryptoIDsResponse{ + RoomID: roomID.String(), + Version: string(roomVersion), + PDUs: ToProtoEvents(ctx, createEvents, rsAPI), + } + + return util.JSONResponse{ + Code: 200, + JSON: response, + } +} + +type createRoomCryptoIDsResponse struct { + RoomID string `json:"room_id"` + Version string `json:"room_version"` + PDUs []json.RawMessage `json:"pdus"` +} + +func ToProtoEvents(ctx context.Context, events []gomatrixserverlib.PDU, rsAPI roomserverAPI.ClientRoomserverAPI) []json.RawMessage { + result := make([]json.RawMessage, len(events)) + for i, event := range events { + result[i] = json.RawMessage(event.JSON()) + //fmt.Printf("\nProcessing %s event (%s)\n", events[i].Type(), events[i].EventID()) + //var rawJson interface{} + //json.Unmarshal(events[i].JSON(), &rawJson) + //fmt.Printf("JSON: %+v\n", rawJson) + //result[i] = gomatrixserverlib.ProtoEvent{ + // SenderID: string(events[i].SenderID()), + // RoomID: events[i].RoomID().String(), + // Type: events[i].Type(), + // StateKey: events[i].StateKey(), + // PrevEvents: events[i].PrevEventIDs(), + // AuthEvents: events[i].AuthEventIDs(), + // Redacts: events[i].Redacts(), + // Depth: events[i].Depth(), + // Content: events[i].Content(), + // Unsigned: events[i].Unsigned(), + // Hashes: events[i].Hashes(), + // OriginServerTimestamp: events[i].OriginServerTS(), + //} + + //roomVersion, _ := rsAPI.QueryRoomVersionForRoom(ctx, events[i].RoomID().String()) + //verImpl, _ := gomatrixserverlib.GetRoomVersion(roomVersion) + //eventJSON, err := json.Marshal(result[i]) + //if err != nil { + // util.GetLogger(ctx).WithError(err).Error("failed marshalling event") + // continue + //} + //pdu, err := verImpl.NewEventFromUntrustedJSON(eventJSON) + //if err != nil { + // util.GetLogger(ctx).WithError(err).Error("failed making event from json") + // continue + //} + //fmt.Printf("\nProcessing %s event (%s) - PDU\n", result[i].Type, pdu.EventID()) + //fmt.Printf(" EventID: %v - %v\n", events[i].EventID(), pdu.EventID()) + //fmt.Printf(" SenderID: %s - %s\n", events[i].SenderID(), pdu.SenderID()) + //fmt.Printf(" RoomID: %s - %s\n", events[i].RoomID().String(), pdu.RoomID().String()) + //fmt.Printf(" Type: %s - %s\n", events[i].Type(), pdu.Type()) + //fmt.Printf(" StateKey: %s - %s\n", *events[i].StateKey(), *pdu.StateKey()) + //fmt.Printf(" PrevEvents: %v - %v\n", events[i].PrevEventIDs(), pdu.PrevEventIDs()) + //fmt.Printf(" AuthEvents: %v - %v\n", events[i].AuthEventIDs(), pdu.AuthEventIDs()) + //fmt.Printf(" Redacts: %s - %s\n", events[i].Redacts(), pdu.Redacts()) + //fmt.Printf(" Depth: %d - %d\n", events[i].Depth(), pdu.Depth()) + //fmt.Printf(" Content: %v - %v\n", events[i].Content(), pdu.Content()) + //fmt.Printf(" Unsigned: %v - %v\n", events[i].Unsigned(), pdu.Unsigned()) + //fmt.Printf(" Hashes: %v - %v\n", events[i].Hashes(), pdu.Hashes()) + //fmt.Printf(" OriginServerTS: %d - %d\n", events[i].OriginServerTS(), pdu.OriginServerTS()) + //json.Unmarshal(eventJSON, &rawJson) + //fmt.Printf("JSON: %+v\n", rawJson) + } + return result +} + // CreateRoom implements /createRoom func CreateRoom( req *http.Request, device *api.Device, @@ -132,7 +335,6 @@ func CreateRoom( return createRoom(req.Context(), createRequest, device, cfg, profileAPI, rsAPI, asAPI, evTime) } -// createRoom implements /createRoom func createRoom( ctx context.Context, createRequest createRoomRequest, device *api.Device, diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go index d4aa1d08d..10b036815 100644 --- a/clientapi/routing/routing.go +++ b/clientapi/routing/routing.go @@ -312,6 +312,24 @@ func Setup( return CreateRoom(req, device, cfg, userAPI, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) + unstableMux.Handle("/org.matrix.msc_cryptoids/createRoom", + httputil.MakeAuthAPI("createRoom", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + logrus.Info("Processing request to /org.matrix.msc_cryptoids/createRoom") + return CreateRoomCryptoIDs(req, device, cfg, userAPI, rsAPI, asAPI) + }), + ).Methods(http.MethodPost, http.MethodOptions) + unstableMux.Handle("/org.matrix.msc_cryptoids/sendPDUs", + httputil.MakeAuthAPI("send_pdus", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { + logrus.Info("Processing request to /org.matrix.msc_cryptoids/sendPDUs") + if r := rateLimits.Limit(req, device); r != nil { + return *r + } + + // NOTE: when making events such as for create_room, multiple PDUs will need to be passed between the client & server. + return SendPDUs(req, device, cfg, userAPI, rsAPI, asAPI) + }), + ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/join/{roomIDOrAlias}", httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { @@ -336,6 +354,7 @@ func Setup( ).Methods(http.MethodPost, http.MethodOptions) if mscCfg.Enabled("msc2753") { + // TODO: update for cryptoIDs v3mux.Handle("/peek/{roomIDOrAlias}", httputil.MakeAuthAPI(spec.Peek, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { @@ -356,6 +375,7 @@ func Setup( return GetJoinedRooms(req, device, rsAPI) }, httputil.WithAllowGuests()), ).Methods(http.MethodGet, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/join", httputil.MakeAuthAPI(spec.Join, userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { @@ -378,6 +398,7 @@ func Setup( return resp.(util.JSONResponse) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/leave", httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { @@ -392,6 +413,7 @@ func Setup( ) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/unpeek", httputil.MakeAuthAPI("unpeek", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -403,6 +425,7 @@ func Setup( ) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/ban", httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -412,6 +435,7 @@ func Setup( return SendBan(req, userAPI, device, vars["roomID"], cfg, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/invite", httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { if r := rateLimits.Limit(req, device); r != nil { @@ -424,6 +448,7 @@ func Setup( return SendInvite(req, userAPI, device, vars["roomID"], cfg, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/kick", httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -433,6 +458,7 @@ func Setup( return SendKick(req, userAPI, device, vars["roomID"], cfg, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/unban", httputil.MakeAuthAPI("membership", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -442,6 +468,7 @@ func Setup( return SendUnban(req, userAPI, device, vars["roomID"], cfg, rsAPI, asAPI) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/send/{eventType}", httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -451,6 +478,7 @@ func Setup( return SendEvent(req, device, vars["roomID"], vars["eventType"], nil, nil, cfg, rsAPI, nil) }, httputil.WithAllowGuests()), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/send/{eventType}/{txnID}", httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -499,6 +527,7 @@ func Setup( return OnIncomingStateTypeRequest(req.Context(), device, rsAPI, vars["roomID"], vars["type"], vars["stateKey"], eventFormat) }, httputil.WithAllowGuests())).Methods(http.MethodGet, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/state/{eventType:[^/]+/?}", httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -511,6 +540,7 @@ func Setup( }, httputil.WithAllowGuests()), ).Methods(http.MethodPut, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/state/{eventType}/{stateKey}", httputil.MakeAuthAPI("send_message", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -559,6 +589,7 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/directory/room/{roomAlias}", httputil.MakeAuthAPI("directory_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -569,6 +600,7 @@ func Setup( }), ).Methods(http.MethodPut, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/directory/room/{roomAlias}", httputil.MakeAuthAPI("directory_room", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -588,6 +620,7 @@ func Setup( }), ).Methods(http.MethodGet, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/directory/list/room/{roomID}", httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -597,6 +630,7 @@ func Setup( return SetVisibility(req, rsAPI, device, vars["roomID"]) }), ).Methods(http.MethodPut, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/directory/list/appservice/{networkID}/{roomID}", httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -608,6 +642,7 @@ func Setup( ).Methods(http.MethodPut, http.MethodOptions) // Undocumented endpoint + // TODO: update for cryptoIDs v3mux.Handle("/directory/list/appservice/{networkID}/{roomID}", httputil.MakeAuthAPI("directory_list", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -648,6 +683,7 @@ func Setup( return SendTyping(req, device, vars["roomID"], vars["userID"], rsAPI, syncProducer) }), ).Methods(http.MethodPut, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/redact/{eventID}", httputil.MakeAuthAPI("rooms_redact", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -657,6 +693,7 @@ func Setup( return SendRedaction(req, device, vars["roomID"], vars["eventID"], cfg, rsAPI, nil, nil) }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/redact/{eventID}/{txnId}", httputil.MakeAuthAPI("rooms_redact", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -668,6 +705,7 @@ func Setup( }), ).Methods(http.MethodPut, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/sendToDevice/{eventType}/{txnID}", httputil.MakeAuthAPI("send_to_device", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) @@ -1144,6 +1182,7 @@ func Setup( }), ).Methods(http.MethodPost, http.MethodOptions) + // TODO: update for cryptoIDs v3mux.Handle("/rooms/{roomID}/upgrade", httputil.MakeAuthAPI("rooms_upgrade", userAPI, func(req *http.Request, device *userapi.Device) util.JSONResponse { vars, err := httputil.URLDecodeMapValues(mux.Vars(req)) diff --git a/clientapi/routing/send_pdus.go b/clientapi/routing/send_pdus.go new file mode 100644 index 000000000..f073f0099 --- /dev/null +++ b/clientapi/routing/send_pdus.go @@ -0,0 +1,140 @@ +// Copyright 2023 The Matrix.org Foundation C.I.C. +// +// 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" + "net/http" + + appserviceAPI "github.com/matrix-org/dendrite/appservice/api" + "github.com/matrix-org/dendrite/clientapi/httputil" + roomserverAPI "github.com/matrix-org/dendrite/roomserver/api" + "github.com/matrix-org/dendrite/roomserver/types" + "github.com/matrix-org/dendrite/setup/config" + "github.com/matrix-org/dendrite/userapi/api" + "github.com/matrix-org/gomatrixserverlib" + "github.com/matrix-org/gomatrixserverlib/spec" + "github.com/matrix-org/util" +) + +type sendPDUsRequest struct { + Version string `json:"room_version"` + PDUs []json.RawMessage `json:"pdus"` +} + +// SendPDUs implements /sendPDUs +func SendPDUs( + req *http.Request, device *api.Device, + cfg *config.ClientAPI, + profileAPI api.ClientUserAPI, rsAPI roomserverAPI.ClientRoomserverAPI, + asAPI appserviceAPI.AppServiceInternalAPI, +) util.JSONResponse { + // TODO: cryptoIDs - should this include an "eventType"? + // if it's a bulk send endpoint, I don't think that makes any sense since there are multiple event types + // In that case, how do I know how to treat the events? + // I could sort them all by roomID? + // Then filter them down based on event type? (how do I collect groups of events such as for room creation?) + // Possibly based on event hash tracking that I know were sent to the client? + // For createRoom, I know what the possible list of events are, so try to find those and collect them to send to room creation. + // Could also sort by depth... but that seems dangerous and depth may not be a field forever + // Does it matter at all? + // Can't I just forward all the events to the roomserver? + // Do I need to do any specific processing on them? + + var pdus sendPDUsRequest + resErr := httputil.UnmarshalJSONRequest(req, &pdus) + if resErr != nil { + return *resErr + } + + userID, err := spec.NewUserID(device.UserID, true) + if err != nil { + return util.JSONResponse{ + Code: http.StatusBadRequest, + JSON: spec.InvalidParam(err.Error()), + } + } + + inputs := make([]roomserverAPI.InputRoomEvent, 0, len(pdus.PDUs)) + for _, event := range pdus.PDUs { + // TODO: cryptoIDs - event hash check? + verImpl, err := gomatrixserverlib.GetRoomVersion(gomatrixserverlib.RoomVersion(pdus.Version)) + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{Err: err.Error()}, + } + } + //eventJSON, err := json.Marshal(event) + //if err != nil { + // return util.JSONResponse{ + // Code: http.StatusInternalServerError, + // JSON: spec.InternalServerError{Err: err.Error()}, + // } + //} + // TODO: cryptoIDs - how should we be converting to a PDU here? + // if the hash matches an event we sent to the client, then the JSON should be good. + // But how do we know how to fill out if the event is redacted if we use the trustedJSON function? + // Also - untrusted JSON seems better - except it strips off the unsigned field? + // Also - gmsl events don't store the `hashes` field... problem? + + pdu, err := verImpl.NewEventFromUntrustedJSON(event) + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{Err: err.Error()}, + } + } + key, err := rsAPI.GetOrCreateUserRoomPrivateKey(req.Context(), *userID, pdu.RoomID()) + if err != nil { + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{Err: err.Error()}, + } + } + pdu = pdu.Sign(string(pdu.SenderID()), "ed25519:1", key) + util.GetLogger(req.Context()).Infof("Processing %s event (%s)", pdu.Type(), pdu.EventID()) + switch pdu.Type() { + case spec.MRoomCreate: + } + + // TODO: cryptoIDs - does it matter which order these are added? + // yes - if the events are for room creation. + // Could make this a client requirement? ie. events are processed based on the order they appear + // We need to check event validity after processing each event. + // ie. what if the client changes power levels that disallow further events they sent? + // We should be doing this already as part of `SendInputRoomEvents`, but how should we pass this + // failure back to the client? + inputs = append(inputs, roomserverAPI.InputRoomEvent{ + Kind: roomserverAPI.KindNew, + Event: &types.HeaderedEvent{PDU: pdu}, + Origin: userID.Domain(), + SendAsServer: roomserverAPI.DoNotSendToOtherServers, + }) + } + + // send the events to the roomserver + if err := roomserverAPI.SendInputRoomEvents(req.Context(), rsAPI, userID.Domain(), inputs, false); err != nil { + util.GetLogger(req.Context()).WithError(err).Error("roomserverAPI.SendInputRoomEvents failed") + return util.JSONResponse{ + Code: http.StatusInternalServerError, + JSON: spec.InternalServerError{Err: err.Error()}, + } + } + + return util.JSONResponse{ + Code: http.StatusOK, + } +} diff --git a/go.mod b/go.mod index c2ab105b1..082f3e10b 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,7 @@ module github.com/matrix-org/dendrite +replace github.com/matrix-org/gomatrixserverlib => ../../gomatrixserverlib/crypto-ids/ + require ( github.com/Arceliar/ironwood v0.0.0-20221025225125-45b4281814c2 github.com/Arceliar/phony v0.0.0-20210209235338-dde1a8dca979 diff --git a/roomserver/api/api.go b/roomserver/api/api.go index ef5bc3d17..d8f0bf8d7 100644 --- a/roomserver/api/api.go +++ b/roomserver/api/api.go @@ -232,6 +232,7 @@ type ClientRoomserverAPI interface { GetRoomIDForAlias(ctx context.Context, req *GetRoomIDForAliasRequest, res *GetRoomIDForAliasResponse) error GetAliasesForRoomID(ctx context.Context, req *GetAliasesForRoomIDRequest, res *GetAliasesForRoomIDResponse) error + PerformCreateRoomCryptoIDs(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *PerformCreateRoomRequest) ([]gomatrixserverlib.PDU, error) PerformCreateRoom(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *PerformCreateRoomRequest) (string, *util.JSONResponse) // PerformRoomUpgrade upgrades a room to a newer version PerformRoomUpgrade(ctx context.Context, roomID string, userID spec.UserID, roomVersion gomatrixserverlib.RoomVersion) (newRoomID string, err error) diff --git a/roomserver/api/perform.go b/roomserver/api/perform.go index 2818efaa3..c24728da8 100644 --- a/roomserver/api/perform.go +++ b/roomserver/api/perform.go @@ -29,6 +29,8 @@ type PerformCreateRoomRequest struct { KeyID gomatrixserverlib.KeyID PrivateKey ed25519.PrivateKey EventTime time.Time + + SenderID string } type PerformJoinRequest struct { diff --git a/roomserver/internal/perform/perform_create_room.go b/roomserver/internal/perform/perform_create_room.go index eb8de7811..38ce06eab 100644 --- a/roomserver/internal/perform/perform_create_room.go +++ b/roomserver/internal/perform/perform_create_room.go @@ -44,6 +44,423 @@ type Creator struct { RSAPI api.RoomserverInternalAPI } +// PerformCreateRoomCryptoIDs handles all the steps necessary to create a new room. +// nolint: gocyclo +func (c *Creator) PerformCreateRoomCryptoIDs(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *api.PerformCreateRoomRequest) ([]gomatrixserverlib.PDU, error) { + verImpl, err := gomatrixserverlib.GetRoomVersion(createRequest.RoomVersion) + if err != nil { + return nil, spec.BadJSON("unknown room version") + } + + createContent := map[string]interface{}{} + if len(createRequest.CreationContent) > 0 { + if err = json.Unmarshal(createRequest.CreationContent, &createContent); err != nil { + util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for creation_content failed") + return nil, spec.BadJSON("invalid create content") + } + } + + senderID := spec.SenderID(createRequest.SenderID) + + // TODO: cryptoIDs - should we be assigning a room NID yet? + _, err = c.DB.AssignRoomNID(ctx, roomID, createRequest.RoomVersion) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("failed to assign roomNID") + return nil, spec.InternalServerError{Err: err.Error()} + } + + if createRequest.RoomVersion == gomatrixserverlib.RoomVersionPseudoIDs { + util.GetLogger(ctx).Infof("StoreUserRoomPublicKey - SenderID: %s UserID: %s RoomID: %s", senderID, userID.String(), roomID.String()) + bytes := spec.Base64Bytes{} + err = bytes.Decode(string(senderID)) + if err != nil { + return nil, err + } + if len(bytes) != ed25519.PublicKeySize { + return nil, spec.BadJSON("SenderID is not a valid ed25519 public key") + } + + // TODO: cryptoIDs - Swap this out for only storing the public key + key, keyErr := c.RSAPI.GetOrCreateUserRoomPrivateKey(ctx, userID, roomID) + if keyErr != nil { + util.GetLogger(ctx).WithError(keyErr).Error("GetOrCreateUserRoomPrivateKey failed") + return nil, spec.InternalServerError{Err: keyErr.Error()} + } + senderID = spec.SenderIDFromPseudoIDKey(key) + //err := c.RSAPI.StoreUserRoomPublicKey(ctx, senderID, userID, roomID) + //if err != nil { + // return nil, spec.InternalServerError{Err: err.Error()} + //} + } + + createContent["creator"] = senderID + createContent["room_version"] = createRequest.RoomVersion + powerLevelContent := eventutil.InitialPowerLevelsContent(string(senderID)) + joinRuleContent := gomatrixserverlib.JoinRuleContent{ + JoinRule: spec.Invite, + } + historyVisibilityContent := gomatrixserverlib.HistoryVisibilityContent{ + HistoryVisibility: historyVisibilityShared, + } + + if createRequest.PowerLevelContentOverride != nil { + // Merge powerLevelContentOverride fields by unmarshalling it atop the defaults + err = json.Unmarshal(createRequest.PowerLevelContentOverride, &powerLevelContent) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("json.Unmarshal for power_level_content_override failed") + return nil, spec.BadJSON("malformed power_level_content_override") + } + } + + var guestsCanJoin bool + switch createRequest.StatePreset { + case spec.PresetPrivateChat: + joinRuleContent.JoinRule = spec.Invite + historyVisibilityContent.HistoryVisibility = historyVisibilityShared + guestsCanJoin = true + case spec.PresetTrustedPrivateChat: + joinRuleContent.JoinRule = spec.Invite + historyVisibilityContent.HistoryVisibility = historyVisibilityShared + for _, invitee := range createRequest.InvitedUsers { + powerLevelContent.Users[invitee] = 100 + } + guestsCanJoin = true + case spec.PresetPublicChat: + joinRuleContent.JoinRule = spec.Public + historyVisibilityContent.HistoryVisibility = historyVisibilityShared + } + + createEvent := gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomCreate, + Content: createContent, + } + powerLevelEvent := gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomPowerLevels, + Content: powerLevelContent, + } + joinRuleEvent := gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomJoinRules, + Content: joinRuleContent, + } + historyVisibilityEvent := gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomHistoryVisibility, + Content: historyVisibilityContent, + } + membershipEvent := gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomMember, + StateKey: string(senderID), + } + + memberContent := gomatrixserverlib.MemberContent{ + Membership: spec.Join, + DisplayName: createRequest.UserDisplayName, + AvatarURL: createRequest.UserAvatarURL, + } + + // If we are creating a room with pseudo IDs, create and sign the MXIDMapping + if createRequest.RoomVersion == gomatrixserverlib.RoomVersionPseudoIDs { + mapping := &gomatrixserverlib.MXIDMapping{ + UserRoomKey: senderID, + UserID: userID.String(), + } + + identity, idErr := c.Cfg.Matrix.SigningIdentityFor(userID.Domain()) // we MUST use the server signing mxid_mapping + if idErr != nil { + logrus.WithError(idErr).WithField("domain", userID.Domain()).Error("unable to find signing identity for domain") + return nil, spec.InternalServerError{Err: idErr.Error()} + } + + // Sign the mapping with the server identity + if err = mapping.Sign(identity.ServerName, identity.KeyID, identity.PrivateKey); err != nil { + return nil, spec.InternalServerError{Err: err.Error()} + } + memberContent.MXIDMapping = mapping + } + membershipEvent.Content = memberContent + + var nameEvent *gomatrixserverlib.FledglingEvent + var topicEvent *gomatrixserverlib.FledglingEvent + var guestAccessEvent *gomatrixserverlib.FledglingEvent + var aliasEvent *gomatrixserverlib.FledglingEvent + + if createRequest.RoomName != "" { + nameEvent = &gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomName, + Content: eventutil.NameContent{ + Name: createRequest.RoomName, + }, + } + } + + if createRequest.Topic != "" { + topicEvent = &gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomTopic, + Content: eventutil.TopicContent{ + Topic: createRequest.Topic, + }, + } + } + + if guestsCanJoin { + guestAccessEvent = &gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomGuestAccess, + Content: eventutil.GuestAccessContent{ + GuestAccess: "can_join", + }, + } + } + + var roomAlias string + if createRequest.RoomAliasName != "" { + roomAlias = fmt.Sprintf("#%s:%s", createRequest.RoomAliasName, userID.Domain()) + // check it's free + // TODO: This races but is better than nothing + hasAliasReq := api.GetRoomIDForAliasRequest{ + Alias: roomAlias, + IncludeAppservices: false, + } + + var aliasResp api.GetRoomIDForAliasResponse + err = c.RSAPI.GetRoomIDForAlias(ctx, &hasAliasReq, &aliasResp) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("aliasAPI.GetRoomIDForAlias failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + if aliasResp.RoomID != "" { + return nil, spec.RoomInUse("Room ID already exists.") + } + + aliasEvent = &gomatrixserverlib.FledglingEvent{ + Type: spec.MRoomCanonicalAlias, + Content: eventutil.CanonicalAlias{ + Alias: roomAlias, + }, + } + } + + var initialStateEvents []gomatrixserverlib.FledglingEvent + for i := range createRequest.InitialState { + if createRequest.InitialState[i].StateKey != "" { + initialStateEvents = append(initialStateEvents, createRequest.InitialState[i]) + continue + } + + switch createRequest.InitialState[i].Type { + case spec.MRoomCreate: + continue + + case spec.MRoomPowerLevels: + powerLevelEvent = createRequest.InitialState[i] + + case spec.MRoomJoinRules: + joinRuleEvent = createRequest.InitialState[i] + + case spec.MRoomHistoryVisibility: + historyVisibilityEvent = createRequest.InitialState[i] + + case spec.MRoomGuestAccess: + guestAccessEvent = &createRequest.InitialState[i] + + case spec.MRoomName: + nameEvent = &createRequest.InitialState[i] + + case spec.MRoomTopic: + topicEvent = &createRequest.InitialState[i] + + default: + initialStateEvents = append(initialStateEvents, createRequest.InitialState[i]) + } + } + + // send events into the room in order of: + // 1- m.room.create + // 2- room creator join member + // 3- m.room.power_levels + // 4- m.room.join_rules + // 5- m.room.history_visibility + // 6- m.room.canonical_alias (opt) + // 7- m.room.guest_access (opt) + // 8- other initial state items + // 9- m.room.name (opt) + // 10- m.room.topic (opt) + // 11- invite events (opt) - with is_direct flag if applicable TODO + // 12- 3pid invite events (opt) TODO + // This differs from Synapse slightly. Synapse would vary the ordering of 3-7 + // depending on if those events were in "initial_state" or not. This made it + // harder to reason about, hence sticking to a strict static ordering. + eventsToMake := []gomatrixserverlib.FledglingEvent{ + createEvent, membershipEvent, powerLevelEvent, joinRuleEvent, historyVisibilityEvent, + } + if guestAccessEvent != nil { + eventsToMake = append(eventsToMake, *guestAccessEvent) + } + eventsToMake = append(eventsToMake, initialStateEvents...) + if nameEvent != nil { + eventsToMake = append(eventsToMake, *nameEvent) + } + if topicEvent != nil { + eventsToMake = append(eventsToMake, *topicEvent) + } + if aliasEvent != nil { + // TODO: bit of a chicken and egg problem here as the alias doesn't exist and cannot until we have made the room. + // This means we might fail creating the alias but say the canonical alias is something that doesn't exist. + eventsToMake = append(eventsToMake, *aliasEvent) + } + + var builtEvents []gomatrixserverlib.PDU + authEvents := gomatrixserverlib.NewAuthEvents(nil) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("rsapi.QuerySenderIDForUser failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + for i, e := range eventsToMake { + depth := i + 1 // depth starts at 1 + + builder := verImpl.NewEventBuilderFromProtoEvent(&gomatrixserverlib.ProtoEvent{ + SenderID: string(senderID), + RoomID: roomID.String(), + Type: e.Type, + StateKey: &e.StateKey, + Depth: int64(depth), + }) + err = builder.SetContent(e.Content) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("builder.SetContent failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + if i > 0 { + builder.PrevEvents = []string{builtEvents[i-1].EventID()} + } + var ev gomatrixserverlib.PDU + if err = builder.AddAuthEvents(&authEvents); err != nil { + util.GetLogger(ctx).WithError(err).Error("AddAuthEvents failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + + ev, err = builder.BuildWithoutSigning(createRequest.EventTime, userID.Domain()) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("buildEvent failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + + if err = gomatrixserverlib.Allowed(ev, &authEvents, func(roomID spec.RoomID, senderID spec.SenderID) (*spec.UserID, error) { + return c.RSAPI.QueryUserIDForSender(ctx, roomID, senderID) + }); err != nil { + util.GetLogger(ctx).WithError(err).Error("gomatrixserverlib.Allowed failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + + // Add the event to the list of auth events + builtEvents = append(builtEvents, ev) + err = authEvents.AddEvent(ev) + if err != nil { + util.GetLogger(ctx).WithError(err).Error("authEvents.AddEvent failed") + return nil, spec.InternalServerError{Err: err.Error()} + } + } + + // TODO(#269): Reserve room alias while we create the room. This stops us + // from creating the room but still failing due to the alias having already + // been taken. + if roomAlias != "" { + aliasAlreadyExists, aliasErr := c.RSAPI.SetRoomAlias(ctx, senderID, roomID, roomAlias) + if aliasErr != nil { + util.GetLogger(ctx).WithError(aliasErr).Error("aliasAPI.SetRoomAlias failed") + return nil, spec.InternalServerError{Err: aliasErr.Error()} + } + + if aliasAlreadyExists { + return nil, spec.RoomInUse("Room alias already exists.") + } + } + + // TODO: cryptoIDs - this shouldn't really be done until the client calls /sendPDUs with these events + // But that would require the visibility setting also being passed along + if createRequest.Visibility == spec.Public { + // expose this room in the published room list + if err = c.RSAPI.PerformPublish(ctx, &api.PerformPublishRequest{ + RoomID: roomID.String(), + Visibility: spec.Public, + }); err != nil { + util.GetLogger(ctx).WithError(err).Error("failed to publish room") + return nil, spec.InternalServerError{Err: err.Error()} + } + } + + // If this is a direct message then we should invite the participants. + //if len(createRequest.InvitedUsers) > 0 { + // Build some stripped state for the invite. + //var globalStrippedState []gomatrixserverlib.InviteStrippedState + //for _, event := range builtEvents { + // // Chosen events from the spec: + // // https://spec.matrix.org/v1.3/client-server-api/#stripped-state + // switch event.Type() { + // case spec.MRoomCreate: + // fallthrough + // case spec.MRoomName: + // fallthrough + // case spec.MRoomAvatar: + // fallthrough + // case spec.MRoomTopic: + // fallthrough + // case spec.MRoomCanonicalAlias: + // fallthrough + // case spec.MRoomEncryption: + // fallthrough + // case spec.MRoomMember: + // fallthrough + // case spec.MRoomJoinRules: + // ev := event + // globalStrippedState = append( + // globalStrippedState, + // gomatrixserverlib.NewInviteStrippedState(ev), + // ) + // } + //} + + // Process the invites. + //for _, invitee := range createRequest.InvitedUsers { + //inviteeUserID, userIDErr := spec.NewUserID(invitee, true) + //if userIDErr != nil { + // util.GetLogger(ctx).WithError(userIDErr).Error("invalid UserID") + // return nil, spec.InternalServerError{} + //} + + // TODO: cryptoIDs - these shouldn't be here + // instead we should return proto invite events? + //err = c.RSAPI.PerformInvite(ctx, &api.PerformInviteRequest{ + // InviteInput: api.InviteInput{ + // RoomID: roomID, + // Inviter: userID, + // Invitee: *inviteeUserID, + // DisplayName: createRequest.UserDisplayName, + // AvatarURL: createRequest.UserAvatarURL, + // Reason: "", + // IsDirect: createRequest.IsDirect, + // KeyID: createRequest.KeyID, + // PrivateKey: createRequest.PrivateKey, + // EventTime: createRequest.EventTime, + // }, + // InviteRoomState: globalStrippedState, + // SendAsServer: string(userID.Domain()), + //}) + //switch e := err.(type) { + //case api.ErrInvalidID: + // return nil, spec.Unknown(e.Error()) + //case api.ErrNotAllowed: + // return nil, spec.Forbidden(e.Error()) + //case nil: + //default: + // util.GetLogger(ctx).WithError(err).Error("PerformInvite failed") + // sentry.CaptureException(err) + // return nil, spec.InternalServerError{} + //} + //} + //} + + return builtEvents, nil +} + // PerformCreateRoom handles all the steps necessary to create a new room. // nolint: gocyclo func (c *Creator) PerformCreateRoom(ctx context.Context, userID spec.UserID, roomID spec.RoomID, createRequest *api.PerformCreateRoomRequest) (string, *util.JSONResponse) { diff --git a/setup/config/config_roomserver.go b/setup/config/config_roomserver.go index 06e7757fb..f47803f48 100644 --- a/setup/config/config_roomserver.go +++ b/setup/config/config_roomserver.go @@ -16,7 +16,8 @@ type RoomServer struct { } func (c *RoomServer) Defaults(opts DefaultOpts) { - c.DefaultRoomVersion = gomatrixserverlib.RoomVersionV10 + //c.DefaultRoomVersion = gomatrixserverlib.RoomVersionV10 + c.DefaultRoomVersion = gomatrixserverlib.RoomVersionPseudoIDs if opts.Generate { if !opts.SingleDatabase { c.Database.ConnectionString = "file:roomserver.db"