package roomserver import ( "context" "crypto/rand" "encoding/base64" "encoding/json" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/auth" "github.com/dariuszjaniak/warp/server/internal/matrix/event" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/storage" ) // The createRoom presets, which are shorthand for a join rule, a history // visibility and a guest access policy together. const ( PresetPrivateChat = "private_chat" PresetTrustedPrivateChat = "trusted_private_chat" PresetPublicChat = "public_chat" ) // Directory visibility values for createRoom and // /directory/list/room/{roomID}. const ( VisibilityPublic = "public" VisibilityPrivate = "private" ) // CreateRoomRequest is the createRoom body, already parsed. type CreateRoomRequest struct { // Creator is the full user ID creating the room. Creator string // Version is the room version to create at; empty uses this server's // default. Version string // Preset is one of the three presets; empty derives it from Visibility // the way the spec does. Preset string // Visibility is "public" to publish the room in this server's directory. Visibility string // RoomAliasName is the localpart of an alias to create and set as the // canonical alias. RoomAliasName string Name string Topic string // Invite lists user IDs to invite once the room exists. Invite []string // CreationContent is merged into the m.room.create content. It is how a // client sets m.federate and creates a space. CreationContent json.RawMessage // InitialState are state events to send after the room's own setup // events and before the invites. InitialState []StateEvent // PowerLevelContentOverride is merged over the m.room.power_levels // content this server would otherwise send. PowerLevelContentOverride json.RawMessage // IsDirect marks the creator's and the invitees' membership events as a // direct chat. IsDirect bool } // StateEvent is one state event in createRoom's initial_state. type StateEvent struct { Type string `json:"type"` StateKey string `json:"state_key"` Content json.RawMessage `json:"content"` } // CreateRoomResponse is what the client is told. type CreateRoomResponse struct { RoomID string // Alias is the full alias created from RoomAliasName, or empty. Alias string } // CreateRoom builds a room from nothing. // // The order of the events is the spec's order, and it is not arbitrary: each // one is authorised against the state the ones before it produced. The // create event comes first because nothing can be authorised without it; the // creator's join comes next because every later event needs a sender who is // in the room; the power levels come before the join rules so that the // creator still has the power to set them. // // A failure part-way through leaves a room that exists and is incomplete. // That is the lesser evil: the alternative is holding every event of a new // room in memory and writing them in one transaction, which would make the // pipeline that authorises them a special case, and the pipeline is the one // thing in here that must not have special cases. A half-built room is // visible only to its creator and can be purged. func (r *API) CreateRoom(ctx context.Context, req CreateRoomRequest) (CreateRoomResponse, error) { if !r.isLocal(req.Creator) { return CreateRoomResponse{}, fmt.Errorf("%w: %s is not a user of this server", ErrForbidden, req.Creator) } version := r.defaultRoomVersion if req.Version != "" { if err := roomversion.Validate(req.Version); err != nil { return CreateRoomResponse{}, fmt.Errorf("%w: %s", ErrUnknownRoomVersion, err) } version = roomversion.Version(req.Version) } rules, err := roomversion.Get(version) if err != nil { return CreateRoomResponse{}, err } preset, err := presetFor(req) if err != nil { return CreateRoomResponse{}, err } var alias string if req.RoomAliasName != "" { if alias, err = r.aliasFor(req.RoomAliasName); err != nil { return CreateRoomResponse{}, err } } roomID, createEvent, err := r.buildCreateEvent(ctx, req, rules) if err != nil { return CreateRoomResponse{}, err } if err := r.store.CreateRoom(ctx, storage.NewRoom{ ID: roomID, Version: string(version), Creator: req.Creator, Published: req.Visibility == VisibilityPublic, CreatedAtMS: r.nowMS(), }); err != nil { return CreateRoomResponse{}, err } // The alias is claimed before any event is signed, so that a name // already taken does not leave a room behind. It cannot be claimed // before the room row, because an alias points at a room; so a clash // takes the room row back out, which is safe precisely because nothing // has been written into the room yet. if alias != "" { if err := r.store.CreateAlias(ctx, storage.RoomAlias{ Alias: alias, RoomID: roomID, Creator: req.Creator, CreatedAtMS: r.nowMS(), }); err != nil { if purgeErr := r.store.PurgeRoom(ctx, roomID); purgeErr != nil { r.log.Error("could not remove the room an alias clash abandoned", "room_id", roomID, "error", purgeErr) } return CreateRoomResponse{}, err } } // The create event is the one event that cannot go through send: there // is no room state to read and no extremity to descend from, and in // room version 12 the room ID is its own event ID, so it has to exist // before the room does. if err := r.persistCreateEvent(ctx, roomID, string(version), rules, createEvent); err != nil { return CreateRoomResponse{}, err } for _, step := range r.creationEvents(ctx, req, preset, rules, roomID) { if _, err := r.send(ctx, step); err != nil { return CreateRoomResponse{}, fmt.Errorf("creating %s: setting up %s: %w", roomID, step.Type, err) } } if alias != "" { if _, err := r.send(ctx, sendRequest{ RoomID: roomID, Type: TypeCanonicalAlias, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{"alias": alias}), }); err != nil { return CreateRoomResponse{}, fmt.Errorf("creating %s: setting the canonical alias: %w", roomID, err) } } // Invites come last, so that an invited user who looks immediately // finds a room that is fully set up. for _, invitee := range req.Invite { if err := r.InviteUser(ctx, InviteRequest{ RoomID: roomID, Sender: req.Creator, Target: invitee, IsDirect: req.IsDirect, }); err != nil { return CreateRoomResponse{}, fmt.Errorf("creating %s: inviting %s: %w", roomID, invitee, err) } } r.log.Info("created room", "room_id", roomID, "version", version, "creator", req.Creator, "preset", preset) return CreateRoomResponse{RoomID: roomID, Alias: alias}, nil } // buildCreateEvent builds the m.room.create event and works out the room ID. // // From room version 12 the room ID *is* the create event's ID, so the event // has to be built before the room can be named; in earlier versions the ID // is minted here and written into the event. The event is checked against // the authorisation rules with an empty state, which is the one case rule 1 // covers. func (r *API) buildCreateEvent(ctx context.Context, req CreateRoomRequest, rules roomversion.Rules) (string, *event.Event, error) { content, err := r.createContent(req, rules) if err != nil { return "", nil, err } roomID := "" if !rules.RoomIDIsCreateEventID { if roomID, err = r.generateRoomID(); err != nil { return "", nil, err } } builder := event.Builder{ RoomID: roomID, Type: event.TypeCreate, StateKey: statePtr(""), Sender: req.Creator, Content: content, Depth: 1, } created, err := builder.Build(r.now(), r.signingKey, rules) if err != nil { return "", nil, fmt.Errorf("building the create event: %w", err) } if rules.RoomIDIsCreateEventID { if roomID, err = event.RoomIDOfCreateEvent(created); err != nil { return "", nil, err } } // Rule 1 against an empty state: the version is one we support, the // creator is well-formed, and in version 12 the room ID matches. if err := auth.Check(ctx, created, auth.State{}, nil); err != nil { return "", nil, err } return roomID, created, nil } // createContent assembles the m.room.create content. // // creation_content from the client is merged in underneath, so a client can // set m.federate or make a space without being able to overwrite the // properties the room version defines — room_version and creator are put on // last for that reason. func (r *API) createContent(req CreateRoomRequest, rules roomversion.Rules) (json.RawMessage, error) { content := map[string]json.RawMessage{} if len(req.CreationContent) > 0 { if err := json.Unmarshal(req.CreationContent, &content); err != nil { return nil, fmt.Errorf("%w: creation_content must be a JSON object", ErrInvalidContent) } // These are the server's to decide, whatever the client sent. delete(content, "room_version") delete(content, "creator") delete(content, "additional_creators") } content["room_version"] = mustMarshal(string(rules.Version)) if rules.CreateEventRequiresCreator { content["creator"] = mustMarshal(req.Creator) } raw, err := json.Marshal(content) if err != nil { return nil, fmt.Errorf("%w: creation_content cannot be encoded: %v", ErrInvalidContent, err) } return raw, nil } // persistCreateEvent writes the create event, which is the room's entire // state and its only extremity. // // The state it is authorised against is empty, which is what an empty // snapshot records; the state *after* it — the create event alone — persist // derives from the event itself, as it does for any other state event. func (r *API) persistCreateEvent(ctx context.Context, roomID, version string, rules roomversion.Rules, created *event.Event) error { return r.persist(ctx, persistRequest{ Room: storage.Room{ID: roomID, Version: version}, Rules: rules, Event: created, StateBefore: auth.State{}, }) } // creationEvents are the events that turn a bare create event into a usable // room, in the order they have to be sent. func (r *API) creationEvents(ctx context.Context, req CreateRoomRequest, preset string, rules roomversion.Rules, roomID string) []sendRequest { steps := []sendRequest{ // The creator joins before anything else: every event after this // one has to be sent by a member of the room. { RoomID: roomID, Type: event.TypeMember, StateKey: statePtr(req.Creator), Sender: req.Creator, Content: r.membershipContent(ctx, event.MembershipJoin, req.Creator, "", req.IsDirect), }, // Power levels before the join rules, so that the creator is still // the only one who can set them. { RoomID: roomID, Type: event.TypePowerLevels, StateKey: statePtr(""), Sender: req.Creator, Content: r.powerLevelsContent(req, preset, rules), }, { RoomID: roomID, Type: event.TypeJoinRules, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{"join_rule": presetJoinRule(preset)}), }, { RoomID: roomID, Type: TypeHistoryVisibility, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{ "history_visibility": HistoryVisibilityShared, }), }, { RoomID: roomID, Type: TypeGuestAccess, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{ "guest_access": presetGuestAccess(preset), }), }, } // initial_state comes after the server's own setup events and may // overwrite any of them, which is what lets a client create an // encrypted or world-readable room in one call. for _, s := range req.InitialState { steps = append(steps, sendRequest{ RoomID: roomID, Type: s.Type, StateKey: statePtr(s.StateKey), Sender: req.Creator, Content: s.Content, }) } // name and topic come after initial_state, because the spec gives the // dedicated parameters precedence over an initial_state event of the // same type. if req.Name != "" { steps = append(steps, sendRequest{ RoomID: roomID, Type: TypeName, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{"name": req.Name}), }) } if req.Topic != "" { steps = append(steps, sendRequest{ RoomID: roomID, Type: TypeTopic, StateKey: statePtr(""), Sender: req.Creator, Content: mustMarshal(map[string]any{"topic": req.Topic}), }) } return steps } // powerLevelsContent is the m.room.power_levels event a new room gets. // // The levels are the spec's defaults written out rather than left implicit, // because a client reading the event is entitled to see what the room // actually does, and because power_level_content_override has to have // something to override. func (r *API) powerLevelsContent(req CreateRoomRequest, preset string, rules roomversion.Rules) json.RawMessage { users := map[string]any{} if !rules.CreatorsHaveInfinitePower { // Up to room version 11 the creator's power has to be written down. // From version 12 it cannot be: naming a creator in this event is // itself a rejection. users[req.Creator] = auth.CreatorPowerLevel } // trusted_private_chat makes everyone invited an equal, which is the // whole point of the preset. if preset == PresetTrustedPrivateChat { for _, invitee := range req.Invite { if rules.CreatorsHaveInfinitePower && invitee == req.Creator { continue } users[invitee] = auth.CreatorPowerLevel } } content := map[string]any{ "ban": auth.DefaultBan, "kick": auth.DefaultKick, "redact": auth.DefaultRedact, "invite": auth.DefaultInvite, "events_default": auth.DefaultEventsDefault, "state_default": auth.DefaultStateDefault, "users_default": auth.DefaultUsersDefault, "users": users, "events": map[string]any{ // Tombstoning a room replaces it, so it is kept at the level // the spec reserves for the room's owner even once others have // been promoted to moderator. "m.room.tombstone": 100, "m.room.power_levels": 100, "m.room.history_visibility": 100, "m.room.canonical_alias": auth.DefaultStateDefault, "m.room.avatar": auth.DefaultStateDefault, TypeEncryption: 100, "m.room.server_acl": 100, // Joining a group call is a state event of the member's own // (MatrixRTC, MSC3401), so anyone who may talk in the room // may also join its call. Starting one is not a separate act. TypeCallMember: 0, }, "notifications": map[string]any{"room": auth.DefaultNotification}, } raw := mustMarshal(content) if len(req.PowerLevelContentOverride) == 0 { return raw } // A malformed override is left out rather than failing the room: the // client is told nothing either way, and a room with the default levels // is recoverable where a room that does not exist is not. The override // is still authorised like any other power levels event, so it cannot // be used to give anybody more power than the creator has. merged, err := mergeJSON(raw, req.PowerLevelContentOverride) if err != nil { r.log.Warn("ignoring an unusable power_level_content_override", "error", err) return raw } return merged } // presetFor resolves the preset, defaulting the way the spec does: from the // directory visibility when the client named no preset. func presetFor(req CreateRoomRequest) (string, error) { switch req.Preset { case PresetPrivateChat, PresetTrustedPrivateChat, PresetPublicChat: return req.Preset, nil case "": if req.Visibility == VisibilityPublic { return PresetPublicChat, nil } return PresetPrivateChat, nil default: return "", fmt.Errorf("%w: %q is not a known preset", ErrInvalidContent, req.Preset) } } func presetJoinRule(preset string) string { if preset == PresetPublicChat { return auth.JoinRulePublic } return auth.JoinRuleInvite } func presetGuestAccess(preset string) string { if preset == PresetPublicChat { return GuestAccessForbidden } return GuestAccessCanJoin } // roomIDRandomBytes is how much randomness a room ID of a version that // still needs one carries. 16 bytes is the same 128 bits as a UUID, encoded // without padding. const roomIDRandomBytes = 16 // generateRoomID mints a "!opaque:server" room ID, for the room versions // before 12 where the ID is not derived from the create event. func (r *API) generateRoomID() (string, error) { buf := make([]byte, roomIDRandomBytes) if _, err := rand.Read(buf); err != nil { return "", fmt.Errorf("generating a room ID: %w", err) } roomID := "!" + base64.RawURLEncoding.EncodeToString(buf) + ":" + r.serverName if err := id.ValidateRoomID(roomID); err != nil { return "", err } return roomID, nil } // statePtr makes a state key addressable, since the empty string is a // perfectly good one and nil means "not a state event". func statePtr(s string) *string { return &s } // mustMarshal encodes a value this package built itself. // // Every caller passes a map or a string, which cannot fail to encode; a // failure here would be a bug in this file rather than anything a client // could cause. func mustMarshal(v any) json.RawMessage { raw, err := json.Marshal(v) if err != nil { panic("roomserver: encoding a value this package built: " + err.Error()) } return raw } // mergeJSON overlays the properties of override onto base, at the top level // only. // // A shallow merge is what the spec asks for: power_level_content_override // replacing the whole "users" map rather than merging into it is the // documented behaviour, and the one a client setting up a room relies on. func mergeJSON(base, override json.RawMessage) (json.RawMessage, error) { var merged, extra map[string]json.RawMessage if err := json.Unmarshal(base, &merged); err != nil { return nil, err } if err := json.Unmarshal(override, &extra); err != nil { return nil, fmt.Errorf("%w: the override must be a JSON object", ErrInvalidContent) } for key, value := range extra { merged[key] = value } return json.Marshal(merged) } // TypeCallMember is a member's state in a room's group call (MatrixRTC). // It is still under its MSC3401 name, which is what clients send. const TypeCallMember = "org.matrix.msc3401.call.member"