// Package event is the Matrix event (PDU) core: parsing and validating // events, computing their hashes and identifiers, signing and verifying // them, and applying the redaction algorithm. // // It is pure library code. Nothing here reaches a database or the network, // and nothing here decides whether an event is *allowed* — that is the // auth package. The division matters: these are the operations whose // results every server in a room must agree on byte for byte, so they are // kept small, deterministic and heavily tested. // // An Event is immutable once parsed. The canonical JSON, the event ID and // the decoded fields are worked out up front and then only read, which is // what lets an event be passed around and cached without copying. // // # A known gap // // Room versions 1 to 5 are allowed to contain integers outside the range // canonical JSON permits, and the spec asks servers to cope where they // can. Warp does not: such an event is rejected whatever its room version. // It only arises for events that were already outside the current grammar // when they were created, and handling them means a second, lenient // encoder — which waits for a room that needs it: joining the rooms of // today over federation (PLAN.md phase 5) does not. package event import ( "encoding/json" "errors" "fmt" "github.com/dariuszjaniak/warp/server/internal/matrix/canonicaljson" "github.com/dariuszjaniak/warp/server/internal/matrix/id" "github.com/dariuszjaniak/warp/server/internal/matrix/roomversion" "github.com/dariuszjaniak/warp/server/internal/matrix/signing" ) // The event types that carry meaning for the event core itself: the ones // the authorisation rules turn on, plus the two the redaction algorithm // treats specially. const ( TypeCreate = "m.room.create" TypeMember = "m.room.member" TypeJoinRules = "m.room.join_rules" TypePowerLevels = "m.room.power_levels" TypeThirdPartyInvite = "m.room.third_party_invite" TypeHistoryVisibility = "m.room.history_visibility" TypeRedaction = "m.room.redaction" TypeAliases = "m.room.aliases" ) // The membership values an m.room.member event may carry. const ( MembershipJoin = "join" MembershipLeave = "leave" MembershipInvite = "invite" MembershipBan = "ban" MembershipKnock = "knock" ) // ErrInvalidEvent is the root of every parsing and validation error here, // so a caller that only needs to answer M_BAD_JSON can test for it. var ErrInvalidEvent = errors.New("invalid event") // Event is a parsed, structurally valid PDU. // // "Structurally valid" is a lower bar than "allowed in the room": an Event // has the fields the spec requires, of the right types and within the size // limits, and its ID has been computed. Whether it may actually be added to // the room is a question for the auth package. type Event struct { rules roomversion.Rules raw []byte id string fields fields } // fields are the event properties the server itself reads. Anything else // stays in raw, untouched, because a server must pass through properties it // does not understand rather than dropping them. type fields struct { RoomID string `json:"room_id"` Type string `json:"type"` StateKey *string `json:"state_key"` Sender string `json:"sender"` Content json.RawMessage `json:"content"` PrevEvents []string `json:"prev_events"` AuthEvents []string `json:"auth_events"` Depth int64 `json:"depth"` OriginServerTS int64 `json:"origin_server_ts"` Hashes hashes `json:"hashes"` Signatures signing.Signatures `json:"signatures"` Unsigned json.RawMessage `json:"unsigned"` // Redacts is the top-level property m.room.redaction used up to room // version 10. From version 11 the target is content.redacts instead, // where the redaction algorithm and therefore the signature can cover // it; Redacts is populated from whichever place this room version uses. Redacts string `json:"redacts"` } type hashes struct { SHA256 string `json:"sha256"` } // Parse validates an event and works out its ID. // // raw is canonicalised first, so an event may arrive with its keys in any // order — what it may not do is use a float, an out-of-range integer or // invalid UTF-8, because then there are no canonical bytes to hash and no // two servers would agree on its ID. func Parse(raw []byte, rules roomversion.Rules) (*Event, error) { canonical, err := canonicaljson.Encode(raw) if err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err) } e := &Event{rules: rules, raw: canonical} if err := json.Unmarshal(canonical, &e.fields); err != nil { return nil, fmt.Errorf("%w: %w", ErrInvalidEvent, err) } if err := e.validate(); err != nil { return nil, err } // From room version 11 the redaction target lives in the content. Read // it from there so that the rest of the server can ask one question. if rules.RedactsInContent && e.fields.Type == TypeRedaction { var content struct { Redacts string `json:"redacts"` } if err := json.Unmarshal(e.fields.Content, &content); err != nil { return nil, fmt.Errorf("%w: m.room.redaction content: %w", ErrInvalidEvent, err) } e.fields.Redacts = content.Redacts } if e.id, err = computeEventID(canonical, rules); err != nil { return nil, err } return e, nil } // Accessors. They return the decoded fields; the JSON-valued ones hand back // the event's own storage, which callers must treat as read-only. // EventID returns the event's identifier, including the '$' sigil. func (e *Event) EventID() string { return e.id } // RoomID returns the room this event belongs to. It is empty for an // m.room.create event in a room version where the room ID *is* the create // event's ID; use RoomIDOfCreateEvent for that case. func (e *Event) RoomID() string { return e.fields.RoomID } // Type returns the event type, such as "m.room.message". func (e *Event) Type() string { return e.fields.Type } // Sender returns the user ID that sent the event. func (e *Event) Sender() string { return e.fields.Sender } // StateKey returns the state key and whether the event has one. An event // with a state key is a state event, even if the key is the empty string. func (e *Event) StateKey() (string, bool) { if e.fields.StateKey == nil { return "", false } return *e.fields.StateKey, true } // IsState reports whether the event is a state event. func (e *Event) IsState() bool { return e.fields.StateKey != nil } // Content returns the event content as JSON. func (e *Event) Content() json.RawMessage { return e.fields.Content } // PrevEventIDs returns the event IDs of the events this one follows. func (e *Event) PrevEventIDs() []string { return e.fields.PrevEvents } // AuthEventIDs returns the event IDs of the events that authorise this one. func (e *Event) AuthEventIDs() []string { return e.fields.AuthEvents } // Depth returns the event's depth in the room DAG. func (e *Event) Depth() int64 { return e.fields.Depth } // OriginServerTS returns the origin server's timestamp, in milliseconds // since the epoch. It is a claim by that server, not a fact: it is used for // ordering and display, never for authorisation. func (e *Event) OriginServerTS() int64 { return e.fields.OriginServerTS } // Unsigned returns the unsigned property, which is not covered by the // event's hashes or signatures and may differ between servers. func (e *Event) Unsigned() json.RawMessage { return e.fields.Unsigned } // Signatures returns the event's signature block. func (e *Event) Signatures() signing.Signatures { return e.fields.Signatures } // Redacts returns the event ID an m.room.redaction event targets, from // wherever this room version keeps it. It is empty on any other event. func (e *Event) Redacts() string { return e.fields.Redacts } // Rules returns the room version rules the event was parsed under. func (e *Event) Rules() roomversion.Rules { return e.rules } // Version returns the event's room version. func (e *Event) Version() roomversion.Version { return e.rules.Version } // JSON returns the event as canonical JSON. The slice is the event's own // and must not be modified. func (e *Event) JSON() []byte { return e.raw } // ContentHash returns the content hash carried in the event, as the // unpadded base64 string it is stored as. func (e *Event) ContentHash() string { return e.fields.Hashes.SHA256 } // Membership returns the membership from an m.room.member event's content. func (e *Event) Membership() (string, error) { if e.fields.Type != TypeMember { return "", fmt.Errorf("%s is not an m.room.member event", e.id) } var content struct { Membership string `json:"membership"` } if err := json.Unmarshal(e.fields.Content, &content); err != nil { return "", fmt.Errorf("%w: m.room.member content: %w", ErrInvalidEvent, err) } return content.Membership, nil } // SenderServer returns the server name of the event's sender, which is the // server that must have signed it. func (e *Event) SenderServer() (string, error) { return id.ServerNameOf(e.fields.Sender) } // RoomIDOfCreateEvent returns the room ID an m.room.create event creates, // in the room versions where the room ID is the create event's own ID with // a '!' in place of the '$'. // // Deriving the room ID this way is what removed the domain from room IDs: // a room is no longer named after the server that happened to create it. func RoomIDOfCreateEvent(e *Event) (string, error) { if e.fields.Type != TypeCreate { return "", fmt.Errorf("%s is not an m.room.create event", e.id) } if !e.rules.RoomIDIsCreateEventID { // In these versions the create event carries the room ID like any // other event, so there is nothing to derive. return e.fields.RoomID, nil } return "!" + e.id[1:], nil }