diff --git a/internal/b/blueprints.go b/internal/b/blueprints.go index 65a8258b..618e09df 100644 --- a/internal/b/blueprints.go +++ b/internal/b/blueprints.go @@ -95,8 +95,17 @@ type Event struct { Sender string StateKey *string Content map[string]interface{} - // This field is ignored in blueprints as clients are unable to set it. Used with federation.Server + + /* The following fields are ignored in blueprints as clients are unable to set them. + * They are used with federation.Server. + */ + Unsigned map[string]interface{} + + // The events needed to authenticate this event. + // This can be either []EventReference for room v1/v2, or []string for room v3 onwards. + // If it is left at nil, MustCreateEvent will populate it automatically based on the room state. + AuthEvents interface{} } func MustValidate(bp Blueprint) Blueprint { diff --git a/internal/federation/server.go b/internal/federation/server.go index 510068e9..7d8d3ca0 100644 --- a/internal/federation/server.go +++ b/internal/federation/server.go @@ -196,12 +196,15 @@ func (s *Server) MustCreateEvent(t *testing.T, room *ServerRoom, ev b.Event) *go RoomID: room.RoomID, PrevEvents: room.ForwardExtremities, Unsigned: unsigned, + AuthEvents: ev.AuthEvents, } - stateNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(&eb) - if err != nil { - t.Fatalf("MustCreateEvent: failed to work out auth_events : %s", err) + if eb.AuthEvents == nil { + stateNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(&eb) + if err != nil { + t.Fatalf("MustCreateEvent: failed to work out auth_events : %s", err) + } + eb.AuthEvents = room.AuthEvents(stateNeeded) } - eb.AuthEvents = room.AuthEvents(stateNeeded) signedEvent, err := eb.Build(time.Now(), gomatrixserverlib.ServerName(s.ServerName), s.KeyID, s.Priv, room.Version) if err != nil { t.Fatalf("MustCreateEvent: failed to sign event: %s", err) diff --git a/internal/federation/server_room.go b/internal/federation/server_room.go index 4c584281..74fa360b 100644 --- a/internal/federation/server_room.go +++ b/internal/federation/server_room.go @@ -167,3 +167,19 @@ func InitialRoomEvents(roomVer gomatrixserverlib.RoomVersion, creator string) [] }, } } + +// EventIDsOrReferences converts a list of events into a list of EventIDs or EventReferences, +// depending on the room version +func (r *ServerRoom) EventIDsOrReferences(events []*gomatrixserverlib.Event) (refs []interface{}) { + refs = make([]interface{}, len(events)) + eventFormat, _ := r.Version.EventFormat() + for i, ev := range events { + switch eventFormat { + case gomatrixserverlib.EventFormatV1: + refs[i] = ev.EventReference() + default: + refs[i] = ev.EventID() + } + } + return +} diff --git a/tests/federation_room_event_auth_test.go b/tests/federation_room_event_auth_test.go new file mode 100644 index 00000000..4a2e8e33 --- /dev/null +++ b/tests/federation_room_event_auth_test.go @@ -0,0 +1,200 @@ +package tests + +import ( + "context" + "encoding/json" + "net/http" + "testing" + "time" + + "github.com/gorilla/mux" + "github.com/matrix-org/gomatrixserverlib" + "github.com/tidwall/gjson" + + "github.com/matrix-org/complement/internal/b" + "github.com/matrix-org/complement/internal/federation" + "github.com/matrix-org/complement/internal/must" +) + +func TestInboundFederationRejectsEventsWithRejectedAuthEvents(t *testing.T) { + /* These tests check that events which refer to rejected events in auth_events + * are themselves rejected. + * + * In order to inject an outlier, we include it as an extra auth_event in a + * regular event. Doing so means that the regular event should itself be + * rejected. + * + * We finish up by sending a final, normal, event which should be accepted + * everywhere. This acts as a sentinel so that we can be sure that the + * events have all been correctly propagated. + * + * The DAG ends up looking like this: + * + * C + * / | \ + * / R \ + * | ^ \ + * | .. O + * | ^ + * X ....... + * | + * S + * + * Where: + * .... represents an "auth_event" link + * C is the room creation series + * R is a rejected event + * O is an outlier, which should be rejected + * X is an event with O among its auth_events, which should be rejected + * as a side-effect of O being rejected + * S is the final regular event, which acts as a sentinel + * + * To check if the outlier is rejected, we simply request the event via + * /rooms/{roomID}/event. If it is rejected, we should get a 404. + */ + + deployment := Deploy(t, b.BlueprintAlice) + defer deployment.Destroy(t) + srv := federation.NewServer(t, deployment, + federation.HandleKeyRequests(), + + // accept incoming presence transactions, etc + federation.HandleTransactionRequests(nil, nil), + ) + cancel := srv.Listen() + defer cancel() + fedClient := srv.FederationClient(deployment) + + /* Create a handler for /event_auth */ + // a map from event ID to events to be returned by /event_auth + eventAuthMap := make(map[string][]*gomatrixserverlib.Event) + srv.Mux().HandleFunc("/_matrix/federation/v1/event_auth/{roomID}/{eventID}", func(w http.ResponseWriter, req *http.Request) { + vars := mux.Vars(req) + eventID := vars["eventID"] + authEvents, ok := eventAuthMap[eventID] + if !ok { + t.Logf("Unexpected /event_auth request for event %s", eventID) + w.WriteHeader(404) + _, _ = w.Write([]byte("{}")) + return + } + res := gomatrixserverlib.RespEventAuth{AuthEvents: authEvents} + responseBytes, _ := json.Marshal(&res) + w.WriteHeader(200) + _, _ = w.Write(responseBytes) + }).Methods("GET") + + // have Alice create a room, and then join it + alice := deployment.Client(t, "hs1", "@alice:hs1") + testRoomID := alice.CreateRoom(t, struct { + Preset string `json:"preset"` + }{ + "public_chat", + }) + charlie := srv.UserID("charlie") + room := srv.MustJoinRoom(t, deployment, "hs1", testRoomID, charlie) + charlieMembershipEvent := room.CurrentState("m.room.member", charlie) + + // have Charlie send a PL event which will be rejected + rejectedEvent := srv.MustCreateEvent(t, room, b.Event{ + Type: "m.room.power_levels", + StateKey: b.Ptr(""), + Sender: charlie, + Content: map[string]interface{}{ + "users": map[string]interface{}{}, + }, + }) + _, err := fedClient.SendTransaction(context.Background(), gomatrixserverlib.Transaction{ + TransactionID: "complement1", + Origin: gomatrixserverlib.ServerName(srv.ServerName), + Destination: "hs1", + OriginServerTS: gomatrixserverlib.AsTimestamp(time.Now()), + PDUs: []json.RawMessage{ + rejectedEvent.JSON(), + }, + }) + must.NotError(t, "failed to SendTransaction", err) + t.Logf("Sent rejected PL event %s", rejectedEvent.EventID()) + + // create an event to be pulled in as an outlier, which is valid according to its prev events, + // but uses the rejected event among its auth events. + outlierEvent := srv.MustCreateEvent(t, room, b.Event{ + Type: "m.room.member", + StateKey: &charlie, + Sender: charlie, + Content: map[string]interface{}{"membership": "join", "test": 1}, + AuthEvents: []string{ + room.CurrentState("m.room.create", "").EventID(), + room.CurrentState("m.room.join_rules", "").EventID(), + rejectedEvent.EventID(), + charlieMembershipEvent.EventID(), + }, + }) + t.Logf("Created outlier event %s", outlierEvent.EventID()) + + // create a regular event which refers to the outlier event in its auth events, + // so that the outlier gets pulled in. + sentEventAuthEvents := []*gomatrixserverlib.Event{ + room.CurrentState("m.room.create", ""), + room.CurrentState("m.room.join_rules", ""), + room.CurrentState("m.room.power_levels", ""), + charlieMembershipEvent, + outlierEvent, + } + sentEvent1 := srv.MustCreateEvent(t, room, b.Event{ + Type: "m.room.message", + Sender: charlie, + Content: map[string]interface{}{"body": "sentEvent1"}, + AuthEvents: room.EventIDsOrReferences(sentEventAuthEvents), + }) + room.AddEvent(sentEvent1) + eventAuthMap[sentEvent1.EventID()] = sentEventAuthEvents + t.Logf("Created sent event 1 %s", sentEvent1.EventID()) + + // finally, a genuine regular event. + sentinelEvent := srv.MustCreateEvent(t, room, b.Event{ + Type: "m.room.message", + Sender: charlie, + Content: map[string]interface{}{"body": "sentinelEvent"}, + }) + t.Logf("Created sentinel event %s", sentinelEvent.EventID()) + + _, err = fedClient.SendTransaction(context.Background(), gomatrixserverlib.Transaction{ + TransactionID: "complement2", + Origin: gomatrixserverlib.ServerName(srv.ServerName), + Destination: "hs1", + OriginServerTS: gomatrixserverlib.AsTimestamp(time.Now()), + PDUs: []json.RawMessage{ + sentEvent1.JSON(), + sentinelEvent.JSON(), + }, + }) + must.NotError(t, "failed to SendTransaction", err) + t.Logf("Sent transaction; awaiting arrival") + + // wait for alice to receive sentinelEvent + alice.SyncUntilTimelineHas( + t, + room.RoomID, + func(ev gjson.Result) bool { + return ev.Get("event_id").Str == sentinelEvent.EventID() + }, + ) + + // now inspect the results. Each of the rejected events should give a 404 for /event + t.Run("Outlier should be rejected", func(t *testing.T) { + res := alice.DoFunc(t, "GET", []string{"_matrix", "client", "r0", "rooms", room.RoomID, "event", outlierEvent.EventID()}) + defer res.Body.Close() + if res.StatusCode != 404 { + t.Errorf("Expected a 404 when fetching outlier event, but got %d", res.StatusCode) + } + }) + + t.Run("sent event 1 should be rejected", func(t *testing.T) { + res := alice.DoFunc(t, "GET", []string{"_matrix", "client", "r0", "rooms", room.RoomID, "event", sentEvent1.EventID()}) + defer res.Body.Close() + if res.StatusCode != 404 { + t.Errorf("Expected a 404 when fetching sent event 1, but got %d", res.StatusCode) + } + }) +}