Skip to content

Commit eb14008

Browse files
authored
Add sticky event parsing (#462)
MSC: matrix-org/matrix-spec-proposals#4354 This is for policyserv so we can block sticky events in our public rooms (they aren't needed there)
1 parent 6697d93 commit eb14008

File tree

3 files changed

+191
-0
lines changed

3 files changed

+191
-0
lines changed

eventV1.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ import (
44
"bytes"
55
"encoding/json"
66
"fmt"
7+
"time"
78

89
"github.com/matrix-org/gomatrixserverlib/spec"
910
"github.com/tidwall/gjson"
1011
"github.com/tidwall/sjson"
1112
"golang.org/x/crypto/ed25519"
1213
)
1314

15+
type stickyEventData struct {
16+
DurationMillis int64 `json:"duration_ms"`
17+
}
18+
1419
type eventV1 struct {
1520
redacted bool
1621
eventJSON []byte
@@ -21,6 +26,9 @@ type eventV1 struct {
2126
EventIDRaw string `json:"event_id,omitempty"`
2227
PrevEvents []eventReference `json:"prev_events"`
2328
AuthEvents []eventReference `json:"auth_events"`
29+
30+
UnstableSticky stickyEventData `json:"msc4354_sticky,omitempty"`
31+
StableSticky stickyEventData `json:"sticky,omitempty"`
2432
}
2533

2634
// MarshalJSON implements json.Marshaller
@@ -259,6 +267,42 @@ func (e *eventV1) ToHeaderedJSON() ([]byte, error) {
259267
return eventJSON, nil
260268
}
261269

270+
func (e *eventV1) assumedStickyStartTime(received time.Time) time.Time {
271+
if e.OriginServerTS().Time().Before(received) {
272+
return e.OriginServerTS().Time()
273+
}
274+
return received
275+
}
276+
277+
func (e *eventV1) calculatedStickyEndTime(startTime time.Time) time.Time {
278+
// Stable first, unstable second
279+
durationMillis := e.StableSticky.DurationMillis
280+
if durationMillis == 0 {
281+
durationMillis = e.UnstableSticky.DurationMillis
282+
}
283+
284+
if durationMillis == 0 {
285+
return time.Time{} // zero, not sticky
286+
}
287+
if durationMillis > 3600000 {
288+
durationMillis = 3600000 // cap at 1 hour
289+
}
290+
291+
return startTime.Add(time.Duration(durationMillis) * time.Millisecond)
292+
}
293+
294+
func (e *eventV1) IsSticky(now time.Time, received time.Time) bool {
295+
endTime := e.StickyEndTime(received)
296+
if endTime.IsZero() {
297+
return false
298+
}
299+
return endTime.After(now)
300+
}
301+
302+
func (e *eventV1) StickyEndTime(received time.Time) time.Time {
303+
return e.calculatedStickyEndTime(e.assumedStickyStartTime(received))
304+
}
305+
262306
func newEventFromUntrustedJSONV1(eventJSON []byte, roomVersion IRoomVersion) (PDU, error) {
263307
if r := gjson.GetBytes(eventJSON, "_*"); r.Exists() {
264308
return nil, fmt.Errorf("gomatrixserverlib NewEventFromUntrustedJSON: found top-level '_' key, is this a headered event: %v", string(eventJSON))

eventV1_test.go

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package gomatrixserverlib
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/base64"
6+
"encoding/json"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
"github.com/tidwall/sjson"
12+
)
13+
14+
func makeStickyEvent(t *testing.T, durationMS int64, originTS int64, stateKey *string) PDU {
15+
verImpl := MustGetRoomVersion(RoomVersionV12)
16+
17+
m := map[string]interface{}{
18+
"sticky": map[string]int64{
19+
"duration_ms": durationMS,
20+
},
21+
"room_id": "!L6nFTAu28CEi9yn9up1SUiKtTNnKt2yomgy2JFRT2Zk",
22+
"type": "m.room.message",
23+
"sender": "@user:localhost",
24+
"content": map[string]interface{}{
25+
"body": "Hello, World!",
26+
"msgtype": "m.text",
27+
},
28+
"origin_server_ts": originTS,
29+
"unsigned": make(map[string]interface{}),
30+
"depth": 1,
31+
"origin": "localhost",
32+
"prev_events": []string{"$65vISquU7WNlFCaJeJ5uohlX4LVEPx5yEkAc1hpRf44"},
33+
"auth_events": []string{"$65vISquU7WNlFCaJeJ5uohlX4LVEPx5yEkAc1hpRf44"},
34+
"hashes": map[string]string{
35+
"sha256": "1234567890",
36+
},
37+
"signatures": map[string]interface{}{
38+
"localhost": map[string]string{
39+
"ed25519:localhost": "doesn't matter because it's not checked",
40+
},
41+
},
42+
}
43+
if stateKey != nil {
44+
m["state_key"] = *stateKey
45+
}
46+
if durationMS < 0 {
47+
delete(m, "sticky")
48+
}
49+
50+
b, err := json.Marshal(m)
51+
assert.NoError(t, err, "failed to marshal sticky message event")
52+
53+
// we need to add hashes manually so we don't cause our event to become redacted
54+
cj, err := CanonicalJSON(b)
55+
assert.NoError(t, err, "failed to canonicalize sticky message event")
56+
for _, key := range []string{"signatures", "unsigned", "hashes"} {
57+
cj, err = sjson.DeleteBytes(cj, key)
58+
assert.NoErrorf(t, err, "failed to delete %s from sticky message event", key)
59+
}
60+
sum := sha256.Sum256(cj)
61+
b, err = sjson.SetBytes(b, "hashes.sha256", base64.RawURLEncoding.EncodeToString(sum[:]))
62+
assert.NoError(t, err, "failed to set sha256 hash on sticky message event")
63+
64+
ev, err := verImpl.NewEventFromUntrustedJSON(b)
65+
assert.NoError(t, err, "failed to create new untrusted sticky message event")
66+
assert.NotNil(t, ev)
67+
return ev
68+
}
69+
70+
func TestIsSticky(t *testing.T) {
71+
now := time.Now()
72+
73+
// Happy path
74+
ev := makeStickyEvent(t, 20000, now.UnixMilli(), nil)
75+
assert.True(t, ev.IsSticky(now, now))
76+
77+
// Origin before now
78+
ev = makeStickyEvent(t, 20000, now.UnixMilli()-10000, nil)
79+
assert.True(t, ev.IsSticky(now, now)) // should use the -10s time from origin as the start time
80+
81+
// Origin in the future
82+
ev = makeStickyEvent(t, 20000, now.UnixMilli()+30000, nil)
83+
assert.True(t, ev.IsSticky(now, now)) // This will switch to using Now() instead of the 30s future, so should be in range
84+
85+
// Origin is well before now, leading to expiration upon receipt
86+
ev = makeStickyEvent(t, 20000, now.UnixMilli()-30000, nil)
87+
assert.False(t, ev.IsSticky(now, now))
88+
89+
// State events can also be sticky
90+
stateKey := "state_key"
91+
ev = makeStickyEvent(t, 20000, now.UnixMilli(), &stateKey)
92+
assert.True(t, ev.IsSticky(now, now))
93+
94+
// Not a sticky event
95+
ev = makeStickyEvent(t, -1, now.UnixMilli(), nil) // -1 creates a non-sticky event
96+
assert.False(t, ev.IsSticky(now, now))
97+
}
98+
99+
func TestStickyEndTime(t *testing.T) {
100+
now := time.Now().UTC().Truncate(time.Millisecond)
101+
nowTS := now.UnixMilli()
102+
received := now
103+
104+
// Happy path: event is a message event, and origin and duration are within range
105+
ev := makeStickyEvent(t, 20000, nowTS, nil)
106+
assert.Equal(t, now.Add(20*time.Second), ev.StickyEndTime(received))
107+
108+
// Origin before now, but duration still within range
109+
ev = makeStickyEvent(t, 20000, nowTS-10000, nil)
110+
assert.Equal(t, now.Add(10*time.Second), ev.StickyEndTime(received)) // +10 s because origin is -10s with a duration of 20s
111+
112+
// Origin and duration before now
113+
ev = makeStickyEvent(t, 20000, nowTS-30000, nil)
114+
assert.Equal(t, received.Add(-10*time.Second), ev.StickyEndTime(received)) // 10s before received (-30+20 = -10)
115+
116+
// Origin in the future (using received time instead), duration still within range
117+
ev = makeStickyEvent(t, 20000, nowTS+10000, nil)
118+
assert.Equal(t, now.Add(20*time.Second), ev.StickyEndTime(received)) // +20s because we'll use the received time as a start time
119+
120+
// Origin is in the future, which places the start time before the origin
121+
ev = makeStickyEvent(t, 20000, nowTS+30000, nil)
122+
assert.Equal(t, received.Add(20*time.Second), ev.StickyEndTime(received)) // The origin is ignored, so +20s for the duration
123+
124+
// Duration is more than an hour
125+
ev = makeStickyEvent(t, 3699999, nowTS, nil)
126+
assert.Equal(t, now.Add(1*time.Hour), ev.StickyEndTime(received))
127+
128+
// State events can also be sticky
129+
stateKey := "state_key"
130+
ev = makeStickyEvent(t, 20000, nowTS, &stateKey)
131+
assert.Equal(t, now.Add(20*time.Second), ev.StickyEndTime(received))
132+
133+
// Not a sticky event
134+
ev = makeStickyEvent(t, -1, nowTS, nil) // -1 creates a non-sticky event
135+
assert.Equal(t, time.Time{}, ev.StickyEndTime(received))
136+
}

pdu.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gomatrixserverlib
33
import (
44
"encoding/json"
55
"fmt"
6+
"time"
67

78
"github.com/matrix-org/gomatrixserverlib/spec"
89
"golang.org/x/crypto/ed25519"
@@ -52,6 +53,16 @@ type PDU interface {
5253
JSON() []byte // TODO: remove
5354
AuthEventIDs() []string // TODO: remove
5455
ToHeaderedJSON() ([]byte, error) // TODO: remove
56+
// IsSticky returns true if the event is *currently* considered "sticky" given the received time.
57+
// Sticky events are annotated as sticky and carry strong delivery guarantees to clients (and
58+
// therefore servers). `received` should be specified as the time the event was received by the
59+
// server if, and only if, the event was received over `/send`. Otherwise, `received` should be
60+
// `time.Now()`. Returns false if the event is not sticky, or no longer sticky. `now` can be supplied
61+
// to override the current time.
62+
IsSticky(now time.Time, received time.Time) bool
63+
// StickyEndTime returns the time at which the event is no longer considered "sticky". See `IsSticky`
64+
// for details on sticky events. Returns `time.Time{}` (zero) if the event is not a sticky event.
65+
StickyEndTime(received time.Time) time.Time
5566
}
5667

5768
// Convert a slice of concrete PDU implementations to a slice of PDUs. This is useful when

0 commit comments

Comments
 (0)