Skip to content
Merged
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/docker/go-connections v0.4.0
github.com/docker/go-units v0.4.0 // indirect
github.com/gorilla/mux v1.8.0
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26 h1:Hr3zjRsq2bhrnp3Ky1qgx/fzCtCALOoGYylh2tpS9K4=
github.com/matrix-org/gomatrix v0.0.0-20190528120928-7df988a63f26/go.mod h1:3fxX6gUjWyI/2Bt7J1OLhpCzOfO/bB3AiX0cJtEKud0=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37 h1:si2CZZpwOLWZfDXfgHPkaTlaAkdJvpJzr1zVqyKXd0I=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210122154608-a38974bd8a37/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210621174423-185789a71f3a h1:ttpygr7uLtACsxDoTol1BTVqmMOyib0ZBovMOD6OzkU=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210621174423-185789a71f3a/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58 h1:PVn5mCHmdONm0k5d0/H+fdtI+/C156+J7vylnnvQWPY=
github.com/matrix-org/gomatrixserverlib v0.0.0-20210624115417-42ac4e797a58/go.mod h1:JsAzE1Ll3+gDWS9JSUHPJiiyAksvOOnGWF2nXdg4ZzU=
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7 h1:ntrLa/8xVzeSs8vHFHK25k0C+NV74sYMJnNSg5NoSRo=
github.com/matrix-org/util v0.0.0-20190711121626-527ce5ddefc7/go.mod h1:vVQlW/emklohkZnOPwD3LrZUBqdfsbiyO3p1lNV8F6U=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
Expand Down
49 changes: 49 additions & 0 deletions internal/federation/handle.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,55 @@ func HandleMakeSendJoinRequests() func(*Server) {
}
}

// HandleInviteRequests is an option which makes the server process invite requests.
//
// inviteCallback is a callback function that if non-nil will be called and passed the incoming invite event
func HandleInviteRequests(inviteCallback func(*gomatrixserverlib.Event)) func(*Server) {
return func(s *Server) {
// https://matrix.org/docs/spec/server_server/r0.1.4#put-matrix-federation-v2-invite-roomid-eventid
s.mux.Handle("/_matrix/federation/v2/invite/{roomID}/{eventID}", http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
fedReq, errResp := gomatrixserverlib.VerifyHTTPRequest(
req, time.Now(), gomatrixserverlib.ServerName(s.ServerName), s.keyRing,
)
if fedReq == nil {
w.WriteHeader(errResp.Code)
b, _ := json.Marshal(errResp.JSON)
w.Write(b)
return
}

var inviteRequest gomatrixserverlib.InviteV2Request
if err := json.Unmarshal(fedReq.Content(), &inviteRequest); err != nil {
log.Printf(
"complement: Unable to unmarshal incoming /invite request: %s",
err.Error(),
)

errResp := util.MessageResponse(400, err.Error())
w.WriteHeader(errResp.Code)
b, _ := json.Marshal(errResp.JSON)
w.Write(b)
return
}

if inviteCallback != nil {
inviteCallback(inviteRequest.Event())
}

// Sign the event before we send it back
signedEvent := inviteRequest.Event().Sign(s.ServerName, s.KeyID, s.Priv)

// Send the response
res := map[string]interface{}{
"event": signedEvent,
}
w.WriteHeader(200)
b, _ := json.Marshal(res)
w.Write(b)
})).Methods("PUT")
}
}

// HandleDirectoryLookups will automatically return room IDs for any aliases present on this server.
func HandleDirectoryLookups() func(*Server) {
return func(s *Server) {
Expand Down
48 changes: 42 additions & 6 deletions internal/federation/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,12 +133,8 @@ func (s *Server) MakeAliasMapping(aliasLocalpart, roomID string) string {
// The `events` will be added to this room. Returns the created room.
func (s *Server) MustMakeRoom(t *testing.T, roomVer gomatrixserverlib.RoomVersion, events []b.Event) *ServerRoom {
roomID := fmt.Sprintf("!%d:%s", len(s.rooms), s.ServerName)
room := &ServerRoom{
RoomID: roomID,
Version: roomVer,
State: make(map[string]*gomatrixserverlib.Event),
ForwardExtremities: make([]string, 0),
}
room := newRoom(roomVer, roomID)

// sign all these events
for _, ev := range events {
signedEvent := s.MustCreateEvent(t, room, ev)
Expand Down Expand Up @@ -196,6 +192,37 @@ func (s *Server) MustCreateEvent(t *testing.T, room *ServerRoom, ev b.Event) *go
return signedEvent
}

// MustJoinRoom will make the server send a make_join and a send_join to join a room
// It returns the resultant room.
func (s *Server) MustJoinRoom(t *testing.T, deployment *docker.Deployment, remoteServer gomatrixserverlib.ServerName, roomID string, userID string) *ServerRoom {
t.Helper()
fedClient := s.FederationClient(deployment)
makeJoinResp, err := fedClient.MakeJoin(context.Background(), remoteServer, roomID, userID, SupportedRoomVersions())
if err != nil {
t.Fatalf("MustJoinRoom: make_join failed: %v", err)
}
roomVer := makeJoinResp.RoomVersion
joinEvent, err := makeJoinResp.JoinEvent.Build(time.Now(), gomatrixserverlib.ServerName(s.ServerName), s.KeyID, s.Priv, roomVer)
if err != nil {
t.Fatalf("MustJoinRoom: failed to sign event: %v", err)
}
sendJoinResp, err := fedClient.SendJoin(context.Background(), gomatrixserverlib.ServerName(remoteServer), joinEvent, roomVer)
if err != nil {
t.Fatalf("MustJoinRoom: send_join failed: %v", err)
}

room := newRoom(roomVer, roomID)
for _, ev := range sendJoinResp.StateEvents {
room.replaceCurrentState(ev)
}
room.AddEvent(joinEvent)
s.rooms[roomID] = room

t.Logf("Server.MustJoinRoom joined room ID %s", roomID)

return room
}

// Mux returns this server's router so you can attach additional paths
func (s *Server) Mux() *mux.Router {
return s.mux
Expand Down Expand Up @@ -481,3 +508,12 @@ func (f *basicKeyFetcher) FetchKeys(
func (f *basicKeyFetcher) FetcherName() string {
return "basicKeyFetcher"
}

// SupportedRoomVersions is a convenience method which returns a list of the room versions supported by gomatrixserverlib.
func SupportedRoomVersions() []gomatrixserverlib.RoomVersion {
supportedRoomVersions := make([]gomatrixserverlib.RoomVersion, 0, 10)
for v := range gomatrixserverlib.SupportedRoomVersions() {
supportedRoomVersions = append(supportedRoomVersions, v)
}
return supportedRoomVersions
}
10 changes: 10 additions & 0 deletions internal/federation/server_room.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,16 @@ type ServerRoom struct {
Depth int64
}

// newRoom creates an empty room structure with no events
func newRoom(roomVer gomatrixserverlib.RoomVersion, roomId string) *ServerRoom {
return &ServerRoom{
RoomID: roomId,
Version: roomVer,
State: make(map[string]*gomatrixserverlib.Event),
ForwardExtremities: make([]string, 0),
}
}

// AddEvent adds a new event to the timeline, updating current state if it is a state event.
// Updates depth and forward extremities.
func (r *ServerRoom) AddEvent(ev *gomatrixserverlib.Event) {
Expand Down
83 changes: 83 additions & 0 deletions tests/federation_room_invite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package tests

import (
"fmt"
"testing"
"time"

"github.com/matrix-org/gomatrixserverlib"

"github.com/matrix-org/complement/internal/b"
"github.com/matrix-org/complement/internal/federation"
)

// This test ensures that invite rejections are correctly sent out over federation.
//
// We start with two users in a room - alice@hs1, and 'delia' on the Complement test server.
// alice sends an invite to charlie@hs2, which he rejects.
// We check that delia sees the rejection.
//
func TestFederationRejectInvite(t *testing.T) {
deployment := Deploy(t, b.BlueprintFederationTwoLocalOneRemote)
defer deployment.Destroy(t)
alice := deployment.Client(t, "hs1", "@alice:hs1")
charlie := deployment.Client(t, "hs2", "@charlie:hs2")

// we'll awaken this Waiter when we receive a membership event for Charlie
var waiter *Waiter

srv := federation.NewServer(t, deployment,
federation.HandleKeyRequests(),
federation.HandleTransactionRequests(func(ev *gomatrixserverlib.Event) {
sk := "<nil>"
if ev.StateKey() != nil {
sk = *ev.StateKey()
}
t.Logf("Received PDU %s/%s", ev.Type(), sk)
if waiter != nil && ev.Type() == "m.room.member" && ev.StateKeyEquals(charlie.UserID) {
waiter.Finish()
}
}, nil),
)
srv.UnexpectedRequestsAreErrors = false
cancel := srv.Listen()
defer cancel()
delia := srv.UserID("delia")

// Alice creates the room, and delia joins
roomID := alice.CreateRoom(t, map[string]interface{}{"preset": "public_chat"})
room := srv.MustJoinRoom(t, deployment, "hs1", roomID, delia)

// Alice invites Charlie; Delia should see the invite
waiter = NewWaiter()
alice.InviteRoom(t, roomID, charlie.UserID)
waiter.Wait(t, 5*time.Second)
if err := checkMembershipForUser(room, charlie.UserID, "invite"); err != nil {
t.Errorf("Membership state for charlie after invite: %v", err)
}

// Charlie rejects the invite; Delia should see the rejection.
waiter = NewWaiter()
charlie.LeaveRoom(t, roomID)
waiter.Wait(t, 5*time.Second)
if err := checkMembershipForUser(room, charlie.UserID, "leave"); err != nil {
t.Errorf("Membership state for charlie after reject: %v", err)
}
}

func checkMembershipForUser(room *federation.ServerRoom, userID, wantMembership string) (err error) {
Comment thread
kegsay marked this conversation as resolved.
state := room.CurrentState("m.room.member", userID)
if state == nil {
err = fmt.Errorf("no membership state for %s", userID)
return
}
m, err := state.Membership()
if err != nil {
return
}
if m != wantMembership {
err = fmt.Errorf("incorrect membership state: got %s, want %s", m, wantMembership)
return
}
return
}
49 changes: 44 additions & 5 deletions tests/msc2403_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,17 @@ import (
"fmt"
"net/url"
"testing"
"time"

"github.com/matrix-org/gomatrixserverlib"

"github.com/tidwall/gjson"

"github.com/matrix-org/complement/internal/b"
"github.com/matrix-org/complement/internal/client"
"github.com/matrix-org/complement/internal/federation"
"github.com/matrix-org/complement/internal/match"
"github.com/matrix-org/complement/internal/must"
"github.com/tidwall/gjson"
)

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

// Create a server to observe
Comment thread
kegsay marked this conversation as resolved.
inviteWaiter := NewWaiter()
srv := federation.NewServer(t, deployment,
federation.HandleKeyRequests(),
federation.HandleInviteRequests(func(ev *gomatrixserverlib.Event) {
inviteWaiter.Finish()
}),
federation.HandleTransactionRequests(nil, nil),
)
cancel := srv.Listen()
defer cancel()
srv.UnexpectedRequestsAreErrors = false
david := srv.UserID("david")

// Create a room for alice and bob to test knocking with
roomIDOne := alice.CreateRoom(t, struct {
Preset string `json:"preset"`
Expand All @@ -48,9 +67,12 @@ func TestKnocking(t *testing.T) {
"private_chat", // Set to private in order to get an invite-only room
"7", // Room version required for knocking.
})
alice.InviteRoom(t, roomIDOne, david)
inviteWaiter.Wait(t, 5*time.Second)
serverRoomOne := srv.MustJoinRoom(t, deployment, "hs1", roomIDOne, david)

// Test knocking between two users on the same homeserver
knockingBetweenTwoUsersTest(t, roomIDOne, alice, bob, false)
knockingBetweenTwoUsersTest(t, roomIDOne, alice, bob, serverRoomOne, false)

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

// Test knocking between two users, each on a separate homeserver
knockingBetweenTwoUsersTest(t, roomIDTwo, alice, charlie, true)
knockingBetweenTwoUsersTest(t, roomIDTwo, alice, charlie, serverRoomTwo, true)
}

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

t.Run("Users in the room see a user's membership update when they knock", func(t *testing.T) {
// check the membership seen over the federation
knockerState := serverRoom.CurrentState("m.room.member", knockingUser.UserID)
if knockerState == nil {
t.Errorf("Did not get membership state for knocking user")
} else {
m, err := knockerState.Membership()
if err != nil {
t.Errorf("Unable to unpack membership state for knocking user: %v", err)
} else if m != "knock" {
t.Errorf("membership for knocking user: got %#v, want \"knock\"", m)
}
}

inRoomUser.SyncUntilTimelineHas(t, roomID, func(ev gjson.Result) bool {
if ev.Get("type").Str != "m.room.member" || ev.Get("sender").Str != knockingUser.UserID {
return false
Expand All @@ -119,7 +158,7 @@ func knockingBetweenTwoUsersTest(t *testing.T, roomID string, inRoomUser, knocki
})
})

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