-
Notifications
You must be signed in to change notification settings - Fork 52
/
Copy pathperformjoin.go
391 lines (360 loc) · 11.5 KB
/
performjoin.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
package gomatrixserverlib
import (
"context"
"crypto/ed25519"
"encoding/json"
"fmt"
"time"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/sirupsen/logrus"
)
type PerformJoinInput struct {
UserID *spec.UserID // The user joining the room
RoomID *spec.RoomID // The room the user is joining
ServerName spec.ServerName // The server to attempt to join via
Content map[string]interface{} // The membership event content
Unsigned map[string]interface{} // The event unsigned content, if any
PrivateKey ed25519.PrivateKey // Used to sign the join event
KeyID KeyID // Used to sign the join event
KeyRing *KeyRing // Used to verify the response from send_join
EventProvider EventProvider // Provides full events given a list of event IDs
UserIDQuerier spec.UserIDForSender // Provides userID for a given senderID
GetOrCreateSenderID spec.CreateSenderID // Creates, if needed, new senderID for this room.
StoreSenderIDFromPublicID spec.StoreSenderIDFromPublicID // Creates the senderID -> userID for the room creator
}
type PerformJoinResponse struct {
JoinEvent PDU
StateSnapshot StateResponse
}
// PerformJoin provides high level functionality that will attempt a federated room
// join. On success it will return the new join event and the state snapshot returned
// as part of the join.
func PerformJoin(
ctx context.Context,
fedClient FederatedJoinClient,
input PerformJoinInput,
) (*PerformJoinResponse, *FederationError) {
if input.UserID == nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: false,
Err: fmt.Errorf("UserID is nil"),
}
}
if input.RoomID == nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: false,
Err: fmt.Errorf("RoomID is nil"),
}
}
if input.KeyRing == nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: false,
Err: fmt.Errorf("KeyRing is nil"),
}
}
origin := input.UserID.Domain()
// Try to perform a make_join using the information supplied in the
// request.
respMakeJoin, err := fedClient.MakeJoin(
ctx,
origin,
input.ServerName,
input.RoomID.String(),
input.UserID.String(),
)
if err != nil {
// TODO: Check if the user was not allowed to join the room.
return nil, &FederationError{
ServerName: input.ServerName,
Transient: true,
Reachable: false,
Err: fmt.Errorf("r.federation.MakeJoin: %w", err),
}
}
// Set all the fields to be what they should be, this should be a no-op
// but it's possible that the remote server returned us something "odd"
joinEvent := respMakeJoin.GetJoinEvent()
joinEvent.Type = spec.MRoomMember
joinEvent.RoomID = input.RoomID.String()
joinEvent.Redacts = ""
// Work out if we support the room version that has been supplied in
// the make_join response.
// "If not provided, the room version is assumed to be either "1" or "2"."
// https://matrix.org/docs/spec/server_server/unstable#get-matrix-federation-v1-make-join-roomid-userid
roomVersion := respMakeJoin.GetRoomVersion()
if roomVersion == "" {
roomVersion = setDefaultRoomVersionFromJoinEvent(joinEvent)
}
verImpl, err := GetRoomVersion(roomVersion)
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: err,
}
}
if input.Content == nil {
input.Content = map[string]interface{}{}
}
var senderID spec.SenderID
signingKey := input.PrivateKey
keyID := input.KeyID
origOrigin := origin
switch respMakeJoin.GetRoomVersion() {
case RoomVersionPseudoIDs:
// we successfully did a make_join, create a senderID for this user now
senderID, signingKey, err = input.GetOrCreateSenderID(ctx, *input.UserID, *input.RoomID, string(respMakeJoin.GetRoomVersion()))
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("Cannot create user room key"),
}
}
keyID = "ed25519:1"
origin = spec.ServerName(senderID)
mapping := MXIDMapping{
UserRoomKey: senderID,
UserID: input.UserID.String(),
}
if err = mapping.Sign(origOrigin, input.KeyID, input.PrivateKey); err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("cannot sign mxid_mapping: %w", err),
}
}
input.Content["mxid_mapping"] = mapping
default:
senderID = spec.SenderID(input.UserID.String())
}
stateKey := string(senderID)
joinEvent.SenderID = string(senderID)
joinEvent.StateKey = &stateKey
joinEB := verImpl.NewEventBuilderFromProtoEvent(&joinEvent)
_ = json.Unmarshal(joinEvent.Content, &input.Content)
input.Content["membership"] = spec.Join
if err = joinEB.SetContent(input.Content); err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err),
}
}
if err = joinEB.SetUnsigned(struct{}{}); err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err),
}
}
// Build the join event.
var event PDU
event, err = joinEB.Build(
time.Now(),
origin,
keyID,
signingKey,
)
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err),
}
}
var respState StateResponse
// Try to perform a send_join using the newly built event.
respSendJoin, err := fedClient.SendJoin(
context.Background(),
origOrigin,
input.ServerName,
event,
)
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: true,
Reachable: false,
Err: fmt.Errorf("r.federation.SendJoin: %w", err),
}
}
// If the remote server returned an event in the "event" key of
// the send_join response then we should use that instead. It may
// contain signatures that we don't know about.
if len(respSendJoin.GetJoinEvent()) > 0 {
var remoteEvent PDU
remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.GetJoinEvent())
if err == nil && isWellFormedJoinMemberEvent(
remoteEvent, input.RoomID, senderID,
) {
event = remoteEvent
}
}
// Sanity-check the join response to ensure that it has a create
// event, that the room version is known, etc.
authEvents := respSendJoin.GetAuthEvents().UntrustedEvents(roomVersion)
if err = checkEventsContainCreateEvent(authEvents); err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("sanityCheckAuthChain: %w", err),
}
}
// get the membership events of all users, so we can store the mxid_mappings
// TODO: better way?
if roomVersion == RoomVersionPseudoIDs {
stateEvents := respSendJoin.GetStateEvents().UntrustedEvents(roomVersion)
events := append(authEvents, stateEvents...)
err = storeMXIDMappings(ctx, events, *input.RoomID, input.KeyRing, input.StoreSenderIDFromPublicID)
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("unable to store mxid_mapping: %w", err),
}
}
}
// Process the send_join response. The idea here is that we'll try and wait
// for as long as possible for the work to complete by using a background
// context instead of the provided ctx. If the client does give up waiting,
// we'll still continue to process the join anyway so that we don't waste the effort.
// TODO: Can we expand Check here to return a list of missing auth
// events rather than failing one at a time?
respState, err = CheckSendJoinResponse(
context.Background(),
roomVersion, StateResponse(respSendJoin),
input.KeyRing,
event,
input.EventProvider,
input.UserIDQuerier,
)
if err != nil {
return nil, &FederationError{
ServerName: input.ServerName,
Transient: false,
Reachable: true,
Err: fmt.Errorf("respSendJoin.Check: %w", err),
}
}
// If we successfully performed a send_join above then the other
// server now thinks we're a part of the room. Send the newly
// returned state to the roomserver to update our local view.
if input.Unsigned != nil {
event, err = event.SetUnsigned(input.Unsigned)
if err != nil {
// non-fatal, log and continue
logrus.WithError(err).Errorf("Failed to set unsigned content")
}
}
return &PerformJoinResponse{
JoinEvent: event,
StateSnapshot: respState,
}, nil
}
func storeMXIDMappings(
ctx context.Context,
events []PDU,
roomID spec.RoomID,
keyRing JSONVerifier,
storeSenderID spec.StoreSenderIDFromPublicID,
) error {
for _, ev := range events {
if ev.Type() != spec.MRoomMember {
continue
}
mapping, err := getMXIDMapping(ev)
if err != nil {
return err
}
// we already validated it is a valid roomversion, so this should be safe to use.
verImpl := MustGetRoomVersion(ev.Version())
if err := validateMXIDMappingSignatures(ctx, ev, *mapping, keyRing, verImpl); err != nil {
logrus.WithError(err).Error("invalid signature for mxid_mapping")
continue
}
if err := storeSenderID(ctx, ev.SenderID(), mapping.UserID, roomID); err != nil {
return err
}
}
return nil
}
func setDefaultRoomVersionFromJoinEvent(
joinEvent ProtoEvent,
) RoomVersion {
// if auth events are not event references we know it must be v3+
// we have to do these shenanigans to satisfy sytest, specifically for:
// "Outbound federation rejects m.room.create events with an unknown room version"
hasEventRefs := true
authEvents, ok := joinEvent.AuthEvents.([]interface{})
if ok {
if len(authEvents) > 0 {
_, ok = authEvents[0].(string)
if ok {
// event refs are objects, not strings, so we know we must be dealing with a v3+ room.
hasEventRefs = false
}
}
}
if hasEventRefs {
return RoomVersionV1
}
return RoomVersionV4
}
// isWellFormedJoinMemberEvent returns true if the event looks like a legitimate
// membership event.
func isWellFormedJoinMemberEvent(event PDU, roomID *spec.RoomID, senderID spec.SenderID) bool { // nolint: interfacer
if membership, err := event.Membership(); err != nil {
return false
} else if membership != spec.Join {
return false
}
if event.RoomID().String() != roomID.String() {
return false
}
if !event.StateKeyEquals(string(senderID)) {
return false
}
return true
}
func checkEventsContainCreateEvent(events []PDU) error {
// sanity check we have a create event and it has a known room version
for _, ev := range events {
if ev.Type() == spec.MRoomCreate && ev.StateKeyEquals("") {
// make sure the room version is known
content := ev.Content()
verBody := struct {
Version string `json:"room_version"`
}{}
err := json.Unmarshal(content, &verBody)
if err != nil {
return err
}
if verBody.Version == "" {
// https://matrix.org/docs/spec/client_server/r0.6.0#m-room-create
// The version of the room. Defaults to "1" if the key does not exist.
verBody.Version = "1"
}
knownVersions := RoomVersions()
if _, ok := knownVersions[RoomVersion(verBody.Version)]; !ok {
return fmt.Errorf("m.room.create event has an unknown room version: %s", verBody.Version)
}
return nil
}
}
return fmt.Errorf("response is missing m.room.create event")
}