Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions eventV1.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ import (
"bytes"
"encoding/json"
"fmt"
"time"

"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
"golang.org/x/crypto/ed25519"
)

type stickyEventData struct {
DurationMillis int64 `json:"duration_ms"`
}

type eventV1 struct {
redacted bool
eventJSON []byte
Expand All @@ -21,6 +26,9 @@ type eventV1 struct {
EventIDRaw string `json:"event_id,omitempty"`
PrevEvents []eventReference `json:"prev_events"`
AuthEvents []eventReference `json:"auth_events"`

UnstableSticky stickyEventData `json:"msc4354_sticky,omitempty"`
StableSticky stickyEventData `json:"sticky,omitempty"`
}

// MarshalJSON implements json.Marshaller
Expand Down Expand Up @@ -259,6 +267,46 @@ func (e *eventV1) ToHeaderedJSON() ([]byte, error) {
return eventJSON, nil
}

func (e *eventV1) assumedStickyStartTime(received time.Time) time.Time {
if e.OriginServerTS().Time().Before(received) {
return e.OriginServerTS().Time()
}
return received
}

func (e *eventV1) calculatedStickyEndTime(startTime time.Time) time.Time {
if e.StateKey() != nil {
return time.Time{} // zero, state events can't be sticky (only message events)
}

// Stable first, unstable second
durationMillis := e.StableSticky.DurationMillis
if durationMillis == 0 {
durationMillis = e.UnstableSticky.DurationMillis
}

if durationMillis == 0 {
return time.Time{} // zero, not sticky
}
if durationMillis > 3600000 {
durationMillis = 3600000 // cap at 1 hour
}

return startTime.Add(time.Duration(durationMillis) * time.Millisecond)
}

func (e *eventV1) IsSticky(received time.Time) bool {
endTime := e.StickyEndTime(received)
if endTime.IsZero() {
return false
}
return endTime.After(time.Now())
}

func (e *eventV1) StickyEndTime(received time.Time) time.Time {
return e.calculatedStickyEndTime(e.assumedStickyStartTime(received))
}

func newEventFromUntrustedJSONV1(eventJSON []byte, roomVersion IRoomVersion) (PDU, error) {
if r := gjson.GetBytes(eventJSON, "_*"); r.Exists() {
return nil, fmt.Errorf("gomatrixserverlib NewEventFromUntrustedJSON: found top-level '_' key, is this a headered event: %v", string(eventJSON))
Expand Down
136 changes: 136 additions & 0 deletions eventV1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package gomatrixserverlib

import (
"crypto/sha256"
"encoding/base64"
"encoding/json"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/tidwall/sjson"
)

func makeStickyEvent(t *testing.T, durationMS int64, originTS int64, stateKey *string) PDU {
verImpl := MustGetRoomVersion(RoomVersionV12)

m := map[string]interface{}{
"sticky": map[string]int64{
"duration_ms": durationMS,
},
"room_id": "!L6nFTAu28CEi9yn9up1SUiKtTNnKt2yomgy2JFRT2Zk",
"type": "m.room.message",
"sender": "@user:localhost",
"content": map[string]interface{}{
"body": "Hello, World!",
"msgtype": "m.text",
},
"origin_server_ts": originTS,
"unsigned": make(map[string]interface{}),
"depth": 1,
"origin": "localhost",
"prev_events": []string{"$65vISquU7WNlFCaJeJ5uohlX4LVEPx5yEkAc1hpRf44"},
"auth_events": []string{"$65vISquU7WNlFCaJeJ5uohlX4LVEPx5yEkAc1hpRf44"},
"hashes": map[string]string{
"sha256": "1234567890",
},
"signatures": map[string]interface{}{
"localhost": map[string]string{
"ed25519:localhost": "doesn't matter because it's not checked",
},
},
}
if stateKey != nil {
m["state_key"] = *stateKey
}
if durationMS < 0 {
delete(m, "sticky")
}

b, err := json.Marshal(m)
assert.NoError(t, err, "failed to marshal sticky message event")

// we need to add hashes manually so we don't cause our event to become redacted
cj, err := CanonicalJSON(b)
assert.NoError(t, err, "failed to canonicalize sticky message event")
for _, key := range []string{"signatures", "unsigned", "hashes"} {
cj, err = sjson.DeleteBytes(cj, key)
assert.NoErrorf(t, err, "failed to delete %s from sticky message event", key)
}
sum := sha256.Sum256(cj)
b, err = sjson.SetBytes(b, "hashes.sha256", base64.RawURLEncoding.EncodeToString(sum[:]))
assert.NoError(t, err, "failed to set sha256 hash on sticky message event")

ev, err := verImpl.NewEventFromUntrustedJSON(b)
assert.NoError(t, err, "failed to create new untrusted sticky message event")
assert.NotNil(t, ev)
return ev
}

func TestIsSticky(t *testing.T) {
// Note: IsSticky internally uses `time.Now()`, so we can't play with the time too much.

// Happy path
ev := makeStickyEvent(t, 20000, time.Now().UnixMilli(), nil)
assert.True(t, ev.IsSticky(time.Now()))

// Origin before now
ev = makeStickyEvent(t, 20000, time.Now().UnixMilli()-10000, nil)
assert.True(t, ev.IsSticky(time.Now())) // should use the -10s time from origin as the start time

// Origin in the future
ev = makeStickyEvent(t, 20000, time.Now().UnixMilli()+30000, nil)
assert.True(t, ev.IsSticky(time.Now())) // This will switch to using Now() instead of the 30s future, so should be in range

// Origin is well before now, leading to expiration upon receipt
ev = makeStickyEvent(t, 20000, time.Now().UnixMilli()-30000, nil)
assert.False(t, ev.IsSticky(time.Now()))

// Not a message event
stateKey := "state_key"
ev = makeStickyEvent(t, 20000, time.Now().UnixMilli(), &stateKey)
assert.False(t, ev.IsSticky(time.Now()))

// Not a sticky event
ev = makeStickyEvent(t, -1, time.Now().UnixMilli(), nil) // -1 creates a non-sticky event
assert.False(t, ev.IsSticky(time.Now()))
}

func TestStickyEndTime(t *testing.T) {
now := time.Now().UTC().Truncate(time.Millisecond)
nowTS := now.UnixMilli()
received := now

// Happy path: event is a message event, and origin and duration are within range
ev := makeStickyEvent(t, 20000, nowTS, nil)
assert.Equal(t, now.Add(20*time.Second), ev.StickyEndTime(received))

// Origin before now, but duration still within range
ev = makeStickyEvent(t, 20000, nowTS-10000, nil)
assert.Equal(t, now.Add(10*time.Second), ev.StickyEndTime(received)) // +10 s because origin is -10s with a duration of 20s

// Origin and duration before now
ev = makeStickyEvent(t, 20000, nowTS-30000, nil)
assert.Equal(t, received.Add(-10*time.Second), ev.StickyEndTime(received)) // 10s before received (-30+20 = -10)

// Origin in the future (using received time instead), duration still within range
ev = makeStickyEvent(t, 20000, nowTS+10000, nil)
assert.Equal(t, now.Add(20*time.Second), ev.StickyEndTime(received)) // +20s because we'll use the received time as a start time

// Origin is in the future, which places the start time before the origin
ev = makeStickyEvent(t, 20000, nowTS+30000, nil)
assert.Equal(t, received.Add(20*time.Second), ev.StickyEndTime(received)) // The origin is ignored, so +20s for the duration

// Duration is more than an hour
ev = makeStickyEvent(t, 3699999, nowTS, nil)
assert.Equal(t, now.Add(1*time.Hour), ev.StickyEndTime(received))

// Not a message event
stateKey := "state_key"
ev = makeStickyEvent(t, 20000, nowTS, &stateKey)
assert.Equal(t, time.Time{}, ev.StickyEndTime(received))

// Not a sticky event
ev = makeStickyEvent(t, -1, nowTS, nil) // -1 creates a non-sticky event
assert.Equal(t, time.Time{}, ev.StickyEndTime(received))
}
10 changes: 10 additions & 0 deletions pdu.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gomatrixserverlib
import (
"encoding/json"
"fmt"
"time"

"github.com/matrix-org/gomatrixserverlib/spec"
"golang.org/x/crypto/ed25519"
Expand Down Expand Up @@ -52,6 +53,15 @@ type PDU interface {
JSON() []byte // TODO: remove
AuthEventIDs() []string // TODO: remove
ToHeaderedJSON() ([]byte, error) // TODO: remove
// IsSticky returns true if the event is *currently* considered "sticky" given the received time.
// Sticky events are annotated as sticky and carry strong delivery guarantees to clients (and
// therefore servers). `received` should be specified as the time the event was received by the
// server if, and only if, the event was received over `/send`. Otherwise, `received` should be
// `time.Now()`. Returns false if the event is not sticky, or no longer sticky.
IsSticky(received time.Time) bool
// StickyEndTime returns the time at which the event is no longer considered "sticky". See `IsSticky`
// for details on sticky events. Returns `time.Time{}` (zero) if the event is not a sticky event.
StickyEndTime(received time.Time) time.Time
}

// Convert a slice of concrete PDU implementations to a slice of PDUs. This is useful when
Expand Down
Loading