CI / Test (ubuntu-latest) (push) Canceled after 0s
CI / Test (windows-latest) (push) Canceled after 0s
CI / Storage tests on PostgreSQL (push) Canceled after 0s
CI / Complement (federation off) (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
CI / Smoke test the binary (push) Canceled after 0s
Implements PLAN.md Phase 2: the library every later phase is built on, as pure code under server/internal/matrix with no HTTP and no storage. canonicaljson: the canonical JSON encoding, hand-written because encoding/json escapes <, > and & and accepts floats. Integers only, in [-(2^53)+1, 2^53-1]; lone surrogate escapes rejected rather than turned into U+FFFD. signing: the Signing JSON algorithm alongside the existing key handling, and one lenient base64 decoder shared by the packages above it. id: identifier grammars as they appear inside events, deliberately laxer than userapi's account rules because rooms carry historical user IDs. roomversion: the registry. Versions 3 to 12 are supported, each written out in full as a Rules value with the flags the other packages read. Versions 1 and 2 are known but refused: their event format and state resolution v1 wait for federation. createRoom will default to 11, as the spec asks for a while after 1.16, with 12 available. event: PDU parsing and validation, content and reference hashes, event IDs, the redaction algorithm for every supported version, signing and verification, and a Builder. auth: the authorisation rules in spec order, power levels with their defaults, v12 creators with infinite power, the auth events selection algorithm, rule 3's auth_events checks, and the separate test for whether a redaction applies. Comments use the room version 12 rule numbering, with the version 11 numbers given where they differ. stateres: state resolution v2, and version 12's amendment (an empty starting state and the conflicted state subgraph). Tested against the spec's own vectors byte for byte: the canonical JSON examples, both JSON signing vectors and both event signing vectors. State resolution ports scenarios from Synapse's suite. canonicaljson and event.Parse have fuzz targets. The capabilities endpoint now advertises the registry instead of a hard-coded "11". docs/event-core.md describes the packages and the known gaps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
149 lines
5.2 KiB
Go
149 lines
5.2 KiB
Go
package event
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// The limits the spec puts on an event.
|
|
const (
|
|
// MaxEventSize is the largest an event may be as canonical JSON,
|
|
// signatures included. 64 KiB is generous for a message and small
|
|
// enough that a room's state cannot be used to exhaust a server.
|
|
MaxEventSize = 65536
|
|
|
|
// MaxFieldLength caps the type and state_key properties.
|
|
MaxFieldLength = 255
|
|
|
|
// MaxPrevEvents and MaxAuthEvents cap the DAG references an event may
|
|
// carry. The auth events selection algorithm never needs more than a
|
|
// handful, so a longer list is a sign of something wrong rather than a
|
|
// legitimate event.
|
|
MaxPrevEvents = 20
|
|
MaxAuthEvents = 10
|
|
|
|
// MaxDepth is the largest depth an event may claim. Once a room gets
|
|
// here every later event stays at the limit.
|
|
MaxDepth = canonicaljson.MaxInt
|
|
)
|
|
|
|
// validate applies the structural rules: the properties the spec marks
|
|
// required are present and of the right type, the identifiers are
|
|
// well-formed, and nothing is over its size limit.
|
|
//
|
|
// It deliberately does not look at the room state. Everything checkable
|
|
// from the event alone is checked here so that the authorisation rules can
|
|
// assume a well-formed event and concern themselves only with permission.
|
|
func (e *Event) validate() error {
|
|
if e.rules.EventIDFormat == roomversion.EventIDFormatExplicit {
|
|
// Unreachable today: Get refuses to return rules for room versions
|
|
// 1 and 2. The check is here so that adding them to the registry
|
|
// fails loudly rather than computing nonsense event IDs.
|
|
return fmt.Errorf("%w: room version %s carries event IDs in the event, which Warp cannot read yet",
|
|
ErrInvalidEvent, e.rules.Version)
|
|
}
|
|
|
|
if n := len(e.raw); n > MaxEventSize {
|
|
return fmt.Errorf("%w: event is %d bytes, the limit is %d", ErrInvalidEvent, n, MaxEventSize)
|
|
}
|
|
|
|
if e.fields.Type == "" {
|
|
return fmt.Errorf("%w: type is required", ErrInvalidEvent)
|
|
}
|
|
if n := len(e.fields.Type); n > MaxFieldLength {
|
|
return fmt.Errorf("%w: type is %d bytes, the limit is %d", ErrInvalidEvent, n, MaxFieldLength)
|
|
}
|
|
if err := id.ValidateUserID(e.fields.Sender); err != nil {
|
|
return fmt.Errorf("%w: sender: %w", ErrInvalidEvent, err)
|
|
}
|
|
if sk, ok := e.StateKey(); ok {
|
|
if n := len(sk); n > MaxFieldLength {
|
|
return fmt.Errorf("%w: state_key is %d bytes, the limit is %d", ErrInvalidEvent, n, MaxFieldLength)
|
|
}
|
|
}
|
|
|
|
if err := e.validateRoomID(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.validateContent(); err != nil {
|
|
return err
|
|
}
|
|
if err := e.validateDAGReferences(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if e.fields.Depth > MaxDepth {
|
|
return fmt.Errorf("%w: depth %d is above the limit of %d", ErrInvalidEvent, e.fields.Depth, MaxDepth)
|
|
}
|
|
if e.fields.Hashes.SHA256 == "" {
|
|
return fmt.Errorf("%w: hashes.sha256 is required", ErrInvalidEvent)
|
|
}
|
|
if len(e.fields.Signatures) == 0 {
|
|
return fmt.Errorf("%w: signatures is required", ErrInvalidEvent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateRoomID checks the room_id property, which from room version 12 is
|
|
// absent on the create event because the room ID is derived from it.
|
|
func (e *Event) validateRoomID() error {
|
|
isCreate := e.fields.Type == TypeCreate
|
|
if e.rules.RoomIDIsCreateEventID && isCreate {
|
|
if e.fields.RoomID != "" {
|
|
// Rule 1.2 for this room version rejects it as well; failing
|
|
// here too means we never compute an ID for an event whose
|
|
// room is ambiguous.
|
|
return fmt.Errorf("%w: an m.room.create event in a version %s room must not have a room_id",
|
|
ErrInvalidEvent, e.rules.Version)
|
|
}
|
|
return nil
|
|
}
|
|
if err := id.ValidateRoomID(e.fields.RoomID); err != nil {
|
|
return fmt.Errorf("%w: room_id: %w", ErrInvalidEvent, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateContent checks that content is present and is an object. The
|
|
// spec marks it required on every event, and the authorisation rules and
|
|
// the redaction algorithm both read into it.
|
|
func (e *Event) validateContent() error {
|
|
if len(e.fields.Content) == 0 {
|
|
return fmt.Errorf("%w: content is required", ErrInvalidEvent)
|
|
}
|
|
var obj map[string]json.RawMessage
|
|
if err := json.Unmarshal(e.fields.Content, &obj); err != nil {
|
|
return fmt.Errorf("%w: content must be an object: %w", ErrInvalidEvent, err)
|
|
}
|
|
if obj == nil {
|
|
return fmt.Errorf("%w: content must be an object, not null", ErrInvalidEvent)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// validateDAGReferences checks prev_events and auth_events: within their
|
|
// limits, and every entry a well-formed event ID.
|
|
func (e *Event) validateDAGReferences() error {
|
|
if n := len(e.fields.PrevEvents); n > MaxPrevEvents {
|
|
return fmt.Errorf("%w: %d prev_events, the limit is %d", ErrInvalidEvent, n, MaxPrevEvents)
|
|
}
|
|
if n := len(e.fields.AuthEvents); n > MaxAuthEvents {
|
|
return fmt.Errorf("%w: %d auth_events, the limit is %d", ErrInvalidEvent, n, MaxAuthEvents)
|
|
}
|
|
for _, eventID := range e.fields.PrevEvents {
|
|
if err := id.ValidateEventID(eventID); err != nil {
|
|
return fmt.Errorf("%w: prev_events: %w", ErrInvalidEvent, err)
|
|
}
|
|
}
|
|
for _, eventID := range e.fields.AuthEvents {
|
|
if err := id.ValidateEventID(eventID); err != nil {
|
|
return fmt.Errorf("%w: auth_events: %w", ErrInvalidEvent, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|