Skip to content

Commit e354e62

Browse files
authored
Test that knocks and invite rejections are sent out correctly over federation (#131)
This is a regression test for matrix-org/synapse#10222, and also extends the knock tests to ensure we don't do the same thing there.
1 parent 42a1a96 commit e354e62

File tree

7 files changed

+233
-14
lines changed

7 files changed

+233
-14
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ require (
99
github.com/docker/go-connections v0.4.0
1010
github.com/docker/go-units v0.4.0 // indirect
1111
github.com/gorilla/mux v1.8.0
12-
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37
12+
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58
1313
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7
1414
github.com/opencontainers/go-digest v1.0.0 // indirect
1515
github.com/pkg/errors v0.9.1 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
5353
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
5454
github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26 h1:Hr3zjRsq2bhrnp3Ky1qgx/fzCtCALOoGYylh2tpS9K4=
5555
github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26/go.mod h1:3fxX6gUjWyI/2Bt7J1OLhpCzOfO/bB3AiX0cJtEKud0=
56-
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37 h1:si2CZZpwOLWZfDXfgHPkaTlaAkdJvpJzr1zVqyKXd0I=
57-
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
56+
github.com/matrix-org/gomatrixserverlib v0.0.0-20210621174423-185789a71f3a h1:ttpygr7uLtACsxDoTol1BTVqmMOyib0ZBovMOD6OzkU=
57+
github.com/matrix-org/gomatrixserverlib v0.0.0-20210621174423-185789a71f3a/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
58+
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58 h1:PVn5mCHmdONm0k5d0/H+fdtI+/C156+J7vylnnvQWPY=
59+
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
5860
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7 h1:ntrLa/8xVzeSs8vHFHK25k0C+NV74sYMJnNSg5NoSRo=
5961
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7/go.mod h1:vVQlW/emklohkZnOPwD3LrZUBqdfsbiyO3p1lNV8F6U=
6062
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=

internal/federation/handle.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,55 @@ func HandleMakeSendJoinRequests() func(*Server) {
128128
}
129129
}
130130

131+
// HandleInviteRequests is an option which makes the server process invite requests.
132+
//
133+
// inviteCallback is a callback function that if non-nil will be called and passed the incoming invite event
134+
func HandleInviteRequests(inviteCallback func(*gomatrixserverlib.Event)) func(*Server) {
135+
return func(s *Server) {
136+
// https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v2-invite-roomid-eventid
137+
s.mux.Handle("/_matrix/federation/v2/invite/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
138+
fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
139+
req, time.Now(), gomatrixserverlib.ServerName(s.ServerName), s.keyRing,
140+
)
141+
if fedReq == nil {
142+
w.WriteHeader(errResp.Code)
143+
b, _ := json.Marshal(errResp.JSON)
144+
w.Write(b)
145+
return
146+
}
147+
148+
var inviteRequest gomatrixserverlib.InviteV2Request
149+
if err := json.Unmarshal(fedReq.Content(), &inviteRequest); err != nil {
150+
log.Printf(
151+
"complement: Unable to unmarshal incoming /invite request: %s",
152+
err.Error(),
153+
)
154+
155+
errResp := util.MessageResponse(400, err.Error())
156+
w.WriteHeader(errResp.Code)
157+
b, _ := json.Marshal(errResp.JSON)
158+
w.Write(b)
159+
return
160+
}
161+
162+
if inviteCallback != nil {
163+
inviteCallback(inviteRequest.Event())
164+
}
165+
166+
// Sign the event before we send it back
167+
signedEvent := inviteRequest.Event().Sign(s.ServerName, s.KeyID, s.Priv)
168+
169+
// Send the response
170+
res := map[string]interface{}{
171+
"event": signedEvent,
172+
}
173+
w.WriteHeader(200)
174+
b, _ := json.Marshal(res)
175+
w.Write(b)
176+
})).Methods("PUT")
177+
}
178+
}
179+
131180
// HandleDirectoryLookups will automatically return room IDs for any aliases present on this server.
132181
func HandleDirectoryLookups() func(*Server) {
133182
return func(s *Server) {

internal/federation/server.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -133,12 +133,8 @@ func (s *Server) MakeAliasMapping(aliasLocalpart, roomID string) string {
133133
// The `events` will be added to this room. Returns the created room.
134134
func (s *Server) MustMakeRoom(t *testing.T, roomVer gomatrixserverlib.RoomVersion, events []b.Event) *ServerRoom {
135135
roomID := fmt.Sprintf("!%d:%s", len(s.rooms), s.ServerName)
136-
room := &ServerRoom{
137-
RoomID: roomID,
138-
Version: roomVer,
139-
State: make(map[string]*gomatrixserverlib.Event),
140-
ForwardExtremities: make([]string, 0),
141-
}
136+
room := newRoom(roomVer, roomID)
137+
142138
// sign all these events
143139
for _, ev := range events {
144140
signedEvent := s.MustCreateEvent(t, room, ev)
@@ -196,6 +192,37 @@ func (s *Server) MustCreateEvent(t *testing.T, room *ServerRoom, ev b.Event) *go
196192
return signedEvent
197193
}
198194

195+
// MustJoinRoom will make the server send a make_join and a send_join to join a room
196+
// It returns the resultant room.
197+
func (s *Server) MustJoinRoom(t *testing.T, deployment *docker.Deployment, remoteServer gomatrixserverlib.ServerName, roomID string, userID string) *ServerRoom {
198+
t.Helper()
199+
fedClient := s.FederationClient(deployment)
200+
makeJoinResp, err := fedClient.MakeJoin(context.Background(), remoteServer, roomID, userID, SupportedRoomVersions())
201+
if err != nil {
202+
t.Fatalf("MustJoinRoom: make_join failed: %v", err)
203+
}
204+
roomVer := makeJoinResp.RoomVersion
205+
joinEvent, err := makeJoinResp.JoinEvent.Build(time.Now(), gomatrixserverlib.ServerName(s.ServerName), s.KeyID, s.Priv, roomVer)
206+
if err != nil {
207+
t.Fatalf("MustJoinRoom: failed to sign event: %v", err)
208+
}
209+
sendJoinResp, err := fedClient.SendJoin(context.Background(), gomatrixserverlib.ServerName(remoteServer), joinEvent, roomVer)
210+
if err != nil {
211+
t.Fatalf("MustJoinRoom: send_join failed: %v", err)
212+
}
213+
214+
room := newRoom(roomVer, roomID)
215+
for _, ev := range sendJoinResp.StateEvents {
216+
room.replaceCurrentState(ev)
217+
}
218+
room.AddEvent(joinEvent)
219+
s.rooms[roomID] = room
220+
221+
t.Logf("Server.MustJoinRoom joined room ID %s", roomID)
222+
223+
return room
224+
}
225+
199226
// Mux returns this server's router so you can attach additional paths
200227
func (s *Server) Mux() *mux.Router {
201228
return s.mux
@@ -488,3 +515,12 @@ func (f *basicKeyFetcher) FetchKeys(
488515
func (f *basicKeyFetcher) FetcherName() string {
489516
return "basicKeyFetcher"
490517
}
518+
519+
// SupportedRoomVersions is a convenience method which returns a list of the room versions supported by gomatrixserverlib.
520+
func SupportedRoomVersions() []gomatrixserverlib.RoomVersion {
521+
supportedRoomVersions := make([]gomatrixserverlib.RoomVersion, 0, 10)
522+
for v := range gomatrixserverlib.SupportedRoomVersions() {
523+
supportedRoomVersions = append(supportedRoomVersions, v)
524+
}
525+
return supportedRoomVersions
526+
}

internal/federation/server_room.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ type ServerRoom struct {
1919
Depth int64
2020
}
2121

22+
// newRoom creates an empty room structure with no events
23+
func newRoom(roomVer gomatrixserverlib.RoomVersion, roomId string) *ServerRoom {
24+
return &ServerRoom{
25+
RoomID: roomId,
26+
Version: roomVer,
27+
State: make(map[string]*gomatrixserverlib.Event),
28+
ForwardExtremities: make([]string, 0),
29+
}
30+
}
31+
2232
// AddEvent adds a new event to the timeline, updating current state if it is a state event.
2333
// Updates depth and forward extremities.
2434
func (r *ServerRoom) AddEvent(ev *gomatrixserverlib.Event) {
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package tests
2+
3+
import (
4+
"fmt"
5+
"testing"
6+
"time"
7+
8+
"github.com/matrix-org/gomatrixserverlib"
9+
10+
"github.com/matrix-org/complement/internal/b"
11+
"github.com/matrix-org/complement/internal/federation"
12+
)
13+
14+
// This test ensures that invite rejections are correctly sent out over federation.
15+
//
16+
// We start with two users in a room - alice@hs1, and 'delia' on the Complement test server.
17+
// alice sends an invite to charlie@hs2, which he rejects.
18+
// We check that delia sees the rejection.
19+
//
20+
func TestFederationRejectInvite(t *testing.T) {
21+
deployment := Deploy(t, b.BlueprintFederationTwoLocalOneRemote)
22+
defer deployment.Destroy(t)
23+
alice := deployment.Client(t, "hs1", "@alice:hs1")
24+
charlie := deployment.Client(t, "hs2", "@charlie:hs2")
25+
26+
// we'll awaken this Waiter when we receive a membership event for Charlie
27+
var waiter *Waiter
28+
29+
srv := federation.NewServer(t, deployment,
30+
federation.HandleKeyRequests(),
31+
federation.HandleTransactionRequests(func(ev *gomatrixserverlib.Event) {
32+
sk := "<nil>"
33+
if ev.StateKey() != nil {
34+
sk = *ev.StateKey()
35+
}
36+
t.Logf("Received PDU %s/%s", ev.Type(), sk)
37+
if waiter != nil && ev.Type() == "m.room.member" && ev.StateKeyEquals(charlie.UserID) {
38+
waiter.Finish()
39+
}
40+
}, nil),
41+
)
42+
srv.UnexpectedRequestsAreErrors = false
43+
cancel := srv.Listen()
44+
defer cancel()
45+
delia := srv.UserID("delia")
46+
47+
// Alice creates the room, and delia joins
48+
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
49+
room := srv.MustJoinRoom(t, deployment, "hs1", roomID, delia)
50+
51+
// Alice invites Charlie; Delia should see the invite
52+
waiter = NewWaiter()
53+
alice.InviteRoom(t, roomID, charlie.UserID)
54+
waiter.Wait(t, 5*time.Second)
55+
if err := checkMembershipForUser(room, charlie.UserID, "invite"); err != nil {
56+
t.Errorf("Membership state for charlie after invite: %v", err)
57+
}
58+
59+
// Charlie rejects the invite; Delia should see the rejection.
60+
waiter = NewWaiter()
61+
charlie.LeaveRoom(t, roomID)
62+
waiter.Wait(t, 5*time.Second)
63+
if err := checkMembershipForUser(room, charlie.UserID, "leave"); err != nil {
64+
t.Errorf("Membership state for charlie after reject: %v", err)
65+
}
66+
}
67+
68+
func checkMembershipForUser(room *federation.ServerRoom, userID, wantMembership string) (err error) {
69+
state := room.CurrentState("m.room.member", userID)
70+
if state == nil {
71+
err = fmt.Errorf("no membership state for %s", userID)
72+
return
73+
}
74+
m, err := state.Membership()
75+
if err != nil {
76+
return
77+
}
78+
if m != wantMembership {
79+
err = fmt.Errorf("incorrect membership state: got %s, want %s", m, wantMembership)
80+
return
81+
}
82+
return
83+
}

tests/msc2403_test.go

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,17 @@ import (
1010
"fmt"
1111
"net/url"
1212
"testing"
13+
"time"
14+
15+
"github.com/matrix-org/gomatrixserverlib"
16+
17+
"github.com/tidwall/gjson"
1318

1419
"github.com/matrix-org/complement/internal/b"
1520
"github.com/matrix-org/complement/internal/client"
21+
"github.com/matrix-org/complement/internal/federation"
1622
"github.com/matrix-org/complement/internal/match"
1723
"github.com/matrix-org/complement/internal/must"
18-
"github.com/tidwall/gjson"
1924
)
2025

2126
// A reason to include in the request body when testing knock reason parameters
@@ -40,6 +45,20 @@ func TestKnocking(t *testing.T) {
4045
charlieUserID := "@charlie:hs2"
4146
charlie := deployment.Client(t, "hs2", charlieUserID)
4247

48+
// Create a server to observe
49+
inviteWaiter := NewWaiter()
50+
srv := federation.NewServer(t, deployment,
51+
federation.HandleKeyRequests(),
52+
federation.HandleInviteRequests(func(ev *gomatrixserverlib.Event) {
53+
inviteWaiter.Finish()
54+
}),
55+
federation.HandleTransactionRequests(nil, nil),
56+
)
57+
cancel := srv.Listen()
58+
defer cancel()
59+
srv.UnexpectedRequestsAreErrors = false
60+
david := srv.UserID("david")
61+
4362
// Create a room for alice and bob to test knocking with
4463
roomIDOne := alice.CreateRoom(t, struct {
4564
Preset string `json:"preset"`
@@ -48,9 +67,12 @@ func TestKnocking(t *testing.T) {
4867
"private_chat", // Set to private in order to get an invite-only room
4968
"7", // Room version required for knocking.
5069
})
70+
alice.InviteRoom(t, roomIDOne, david)
71+
inviteWaiter.Wait(t, 5*time.Second)
72+
serverRoomOne := srv.MustJoinRoom(t, deployment, "hs1", roomIDOne, david)
5173

5274
// Test knocking between two users on the same homeserver
53-
knockingBetweenTwoUsersTest(t, roomIDOne, alice, bob, false)
75+
knockingBetweenTwoUsersTest(t, roomIDOne, alice, bob, serverRoomOne, false)
5476

5577
// Create a room for alice and charlie to test knocking with
5678
roomIDTwo := alice.CreateRoom(t, struct {
@@ -60,12 +82,16 @@ func TestKnocking(t *testing.T) {
6082
"private_chat", // Set to private in order to get an invite-only room
6183
"7", // Room version required for knocking.
6284
})
85+
inviteWaiter = NewWaiter()
86+
alice.InviteRoom(t, roomIDTwo, david)
87+
inviteWaiter.Wait(t, 5*time.Second)
88+
serverRoomTwo := srv.MustJoinRoom(t, deployment, "hs1", roomIDTwo, david)
6389

6490
// Test knocking between two users, each on a separate homeserver
65-
knockingBetweenTwoUsersTest(t, roomIDTwo, alice, charlie, true)
91+
knockingBetweenTwoUsersTest(t, roomIDTwo, alice, charlie, serverRoomTwo, true)
6692
}
6793

68-
func knockingBetweenTwoUsersTest(t *testing.T, roomID string, inRoomUser, knockingUser *client.CSAPI, federation bool) {
94+
func knockingBetweenTwoUsersTest(t *testing.T, roomID string, inRoomUser, knockingUser *client.CSAPI, serverRoom *federation.ServerRoom, testFederation bool) {
6995
t.Run("Knocking on a room with a join rule other than 'knock' should fail", func(t *testing.T) {
7096
knockOnRoomWithStatus(t, knockingUser, roomID, "Can I knock anyways?", []string{"hs1"}, 403)
7197
})
@@ -109,6 +135,19 @@ func knockingBetweenTwoUsersTest(t *testing.T, roomID string, inRoomUser, knocki
109135
})
110136

111137
t.Run("Users in the room see a user's membership update when they knock", func(t *testing.T) {
138+
// check the membership seen over the federation
139+
knockerState := serverRoom.CurrentState("m.room.member", knockingUser.UserID)
140+
if knockerState == nil {
141+
t.Errorf("Did not get membership state for knocking user")
142+
} else {
143+
m, err := knockerState.Membership()
144+
if err != nil {
145+
t.Errorf("Unable to unpack membership state for knocking user: %v", err)
146+
} else if m != "knock" {
147+
t.Errorf("membership for knocking user: got %#v, want \"knock\"", m)
148+
}
149+
}
150+
112151
inRoomUser.SyncUntilTimelineHas(t, roomID, func(ev gjson.Result) bool {
113152
if ev.Get("type").Str != "m.room.member" || ev.Get("sender").Str != knockingUser.UserID {
114153
return false
@@ -119,7 +158,7 @@ func knockingBetweenTwoUsersTest(t *testing.T, roomID string, inRoomUser, knocki
119158
})
120159
})
121160

122-
if !federation {
161+
if !testFederation {
123162
// Rescinding a knock over federation is currently not specced
124163
t.Run("A user that has knocked on a local room can rescind their knock and then knock again", func(t *testing.T) {
125164
// We need to carry out an incremental sync after knocking in order to get leave information

0 commit comments

Comments
 (0)