Skip to content

Bring high level room joining logic into GMSL #372

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 18 commits into from
Apr 27, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions authchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import (
"fmt"
)

// AuthChainProvider returns the requested list of auth events.
type AuthChainProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, error)
// EventProvider returns the requested list of events.
type EventProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, error)

// VerifyEventAuthChain will verify that the event is allowed according to its auth_events, and then
// recursively verify each of those auth_events.
Expand All @@ -19,7 +19,7 @@ type AuthChainProvider func(roomVer RoomVersion, eventIDs []string) ([]*Event, e
// The `provideEvents` function will only be called for *new* events rather than for everything as it is
// assumed that this function is costly. Failing to provide all the requested events will fail this function.
// Returning an error from `provideEvents` will also fail this function.
func VerifyEventAuthChain(ctx context.Context, eventToVerify *HeaderedEvent, provideEvents AuthChainProvider) error {
func VerifyEventAuthChain(ctx context.Context, eventToVerify *HeaderedEvent, provideEvents EventProvider) error {
eventsByID := make(map[string]*Event) // A lookup table for verifying this auth chain
evv := eventToVerify.Unwrap()
eventsByID[evv.EventID()] = evv
Expand Down
2 changes: 1 addition & 1 deletion authchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestVerifyEventAuthChainCascadeFailure(t *testing.T) {
}
}

func provideEvents(t *testing.T, events [][]byte) gomatrixserverlib.AuthChainProvider {
func provideEvents(t *testing.T, events [][]byte) gomatrixserverlib.EventProvider {
eventMap := make(map[string]*gomatrixserverlib.Event)
for _, eventBytes := range events {
ev, err := gomatrixserverlib.MustGetRoomVersion(gomatrixserverlib.RoomVersionV1).NewEventFromTrustedJSON(eventBytes, false)
Expand Down
12 changes: 6 additions & 6 deletions authstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ func VerifyAuthRulesAtState(ctx context.Context, sp StateProvider, eventToVerify

func checkAllowedByAuthEvents(
event *Event, eventsByID map[string]*Event,
missingAuth AuthChainProvider,
missingAuth EventProvider,
) error {
authEvents := NewAuthEvents(nil)

Expand All @@ -173,7 +173,7 @@ func checkAllowedByAuthEvents(
if !ok {
// We don't have an entry in the eventsByID map - neither an event nor nil.
if missingAuth != nil {
// If we have a AuthChainProvider then ask it for the missing event.
// If we have a EventProvider then ask it for the missing event.
if ev, err := missingAuth(event.Version(), []string{ae}); err == nil && len(ev) > 0 {
// It claims to have returned events - populate the eventsByID
// map and the authEvents provider so that we can retry with the
Expand All @@ -193,7 +193,7 @@ func checkAllowedByAuthEvents(
}
goto retryEvent
} else {
// If we didn't have a AuthChainProvider then we can't get the event
// If we didn't have a EventProvider then we can't get the event
// so just carry on without it. If it was important for anything then
// Check() below will catch it.
continue
Expand All @@ -206,7 +206,7 @@ func checkAllowedByAuthEvents(
}
} else {
// We had an entry in the map but it contains nil, which means that we tried
// to use the AuthChainProvider to retrieve it and failed, so at this point
// to use the EventProvider to retrieve it and failed, so at this point
// we just have to ignore the event.
continue
}
Expand All @@ -229,7 +229,7 @@ func checkAllowedByAuthEvents(
// return parameter). Does not alter any input args.
func CheckStateResponse(
ctx context.Context, r StateResponse, roomVersion RoomVersion,
keyRing JSONVerifier, missingAuth AuthChainProvider,
keyRing JSONVerifier, missingAuth EventProvider,
) ([]*Event, []*Event, error) {
logger := util.GetLogger(ctx)
authEvents := r.GetAuthEvents().UntrustedEvents(roomVersion)
Expand Down Expand Up @@ -322,7 +322,7 @@ func CheckStateResponse(
func CheckSendJoinResponse(
ctx context.Context, roomVersion RoomVersion, r StateResponse,
keyRing JSONVerifier, joinEvent *Event,
missingAuth AuthChainProvider,
missingAuth EventProvider,
) (StateResponse, error) {
// First check that the state is valid and that the events in the response
// are correctly signed.
Expand Down
8 changes: 8 additions & 0 deletions eventversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,14 @@ func StableRoomVersions() map[RoomVersion]IRoomVersion {
return versions
}

func RoomVersionsToList(versions map[RoomVersion]IRoomVersion) []RoomVersion {
var supportedVersions []RoomVersion
for version := range versions {
supportedVersions = append(supportedVersions, version)
}
return supportedVersions
}

// RoomVersionDescription contains information about a room version,
// namely whether it is marked as supported or stable in this server
// version, along with the state resolution algorithm, event ID etc
Expand Down
4 changes: 2 additions & 2 deletions fclient/federationclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ type FederationClient interface {
// Perform operations
LookupRoomAlias(ctx context.Context, origin, s spec.ServerName, roomAlias string) (res RespDirectory, err error)
Peek(ctx context.Context, origin, s spec.ServerName, roomID, peekID string, roomVersions []gomatrixserverlib.RoomVersion) (res RespPeek, err error)
MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string, roomVersions []gomatrixserverlib.RoomVersion) (res RespMakeJoin, err error)
MakeJoin(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res RespMakeJoin, err error)
SendJoin(ctx context.Context, origin, s spec.ServerName, event *gomatrixserverlib.Event) (res RespSendJoin, err error)
MakeLeave(ctx context.Context, origin, s spec.ServerName, roomID, userID string) (res RespMakeLeave, err error)
SendLeave(ctx context.Context, origin, s spec.ServerName, event *gomatrixserverlib.Event) (err error)
Expand Down Expand Up @@ -191,8 +191,8 @@ func makeVersionQueryString(roomVersions []gomatrixserverlib.RoomVersion) string
// See https://matrix.org/docs/spec/server_server/unstable.html#joining-rooms
func (ac *federationClient) MakeJoin(
ctx context.Context, origin, s spec.ServerName, roomID, userID string,
roomVersions []gomatrixserverlib.RoomVersion,
) (res RespMakeJoin, err error) {
roomVersions := gomatrixserverlib.RoomVersionsToList(gomatrixserverlib.StableRoomVersions())
versionQueryString := makeVersionQueryString(roomVersions)
path := federationPathPrefixV1 + "/make_join/" +
url.PathEscape(roomID) + "/" +
Expand Down
238 changes: 238 additions & 0 deletions fclient/performjoin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
package fclient

import (
"context"
"crypto/ed25519"
"encoding/json"
"fmt"
"time"

"github.com/matrix-org/gomatrixserverlib"
"github.com/matrix-org/gomatrixserverlib/spec"
"github.com/sirupsen/logrus"
)

type PerformJoinInput struct {
UserID *spec.UserID
RoomID string
ServerName spec.ServerName
Content map[string]interface{}
Unsigned map[string]interface{}

PrivateKey ed25519.PrivateKey
KeyID gomatrixserverlib.KeyID
KeyRing *gomatrixserverlib.KeyRing

EventProvider func(
ctx context.Context, fedClient FederationClient,
keyRing gomatrixserverlib.JSONVerifier, origin, server spec.ServerName,
) gomatrixserverlib.EventProvider
}

type PerformJoinCallbacks struct {
FederationFailure func(serverName spec.ServerName)
FederationSuccess func(serverName spec.ServerName)
}

func PerformJoin(
ctx context.Context,
fedClient FederationClient,
input PerformJoinInput,
callbacks PerformJoinCallbacks,
) (*gomatrixserverlib.HeaderedEvent, gomatrixserverlib.StateResponse, error) {
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,
input.UserID.Raw(),
)
if err != nil {
// TODO: Check if the user was not allowed to join the room.
callbacks.FederationFailure(input.ServerName)
return nil, nil, fmt.Errorf("r.federation.MakeJoin: %w", err)
}
callbacks.FederationSuccess(input.ServerName)

// 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"
stateKey := input.UserID.Raw()
respMakeJoin.JoinEvent.Type = spec.MRoomMember
respMakeJoin.JoinEvent.Sender = input.UserID.Raw()
respMakeJoin.JoinEvent.StateKey = &stateKey
respMakeJoin.JoinEvent.RoomID = input.RoomID
respMakeJoin.JoinEvent.Redacts = ""
if input.Content == nil {
input.Content = map[string]interface{}{}
}
_ = json.Unmarshal(respMakeJoin.JoinEvent.Content, &input.Content)
input.Content["membership"] = spec.Join
if err = respMakeJoin.JoinEvent.SetContent(input.Content); err != nil {
return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetContent: %w", err)
}
if err = respMakeJoin.JoinEvent.SetUnsigned(struct{}{}); err != nil {
return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.SetUnsigned: %w", err)
}

// 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
if respMakeJoin.RoomVersion == "" {
respMakeJoin.RoomVersion = setDefaultRoomVersionFromJoinEvent(respMakeJoin.JoinEvent)
}
verImpl, err := gomatrixserverlib.GetRoomVersion(respMakeJoin.RoomVersion)
if err != nil {
return nil, nil, err
}

// Build the join event.
event, err := respMakeJoin.JoinEvent.Build(
time.Now(),
origin,
input.KeyID,
input.PrivateKey,
respMakeJoin.RoomVersion,
)
if err != nil {
return nil, nil, fmt.Errorf("respMakeJoin.JoinEvent.Build: %w", err)
}

var respState gomatrixserverlib.StateResponse
// Try to perform a send_join using the newly built event.
respSendJoin, err := fedClient.SendJoin(
context.Background(),
origin,
input.ServerName,
event,
)
if err != nil {
callbacks.FederationFailure(input.ServerName)
return nil, nil, fmt.Errorf("r.federation.SendJoin: %w", err)
}
callbacks.FederationSuccess(input.ServerName)

// If the remote server returned an event in the "event" key of
// the send_join request then we should use that instead. It may
// contain signatures that we don't know about.
if len(respSendJoin.Event) > 0 {
var remoteEvent *gomatrixserverlib.Event
remoteEvent, err = verImpl.NewEventFromUntrustedJSON(respSendJoin.Event)
if err == nil && isWellFormedMembershipEvent(
remoteEvent, input.RoomID, input.UserID,
) {
event = remoteEvent
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We seemingly continue blindly if the event is invalid?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what the logic should be if you receive a bad event inside a send_join response.
Synapse just accepts the event as is and doesn't seem to do any checks against it. So we differ from them in that regard.

At this point in the join dance, the resident server has already accepted that event into it's room graph and propagated it out to the other servers in the room. So for us to reject it leaves us in an awkward situation.


// Sanity-check the join response to ensure that it has a create
// event, that the room version is known, etc.
authEvents := respSendJoin.AuthEvents.UntrustedEvents(respMakeJoin.RoomVersion)
if err = checkEventsContainCreateEvent(authEvents); err != nil {
return nil, nil, fmt.Errorf("sanityCheckAuthChain: %w", err)
}

// Process the join response in a goroutine. The idea here is
// that we'll try and wait for as long as possible for the work
// to complete, but 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 = gomatrixserverlib.CheckSendJoinResponse(
context.Background(),
respMakeJoin.RoomVersion, &respSendJoin,
input.KeyRing,
event,
input.EventProvider(ctx, fedClient, input.KeyRing, origin, input.ServerName),
)
if err != nil {
return nil, nil, 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 event.Headered(respMakeJoin.RoomVersion), respState, nil
}

func setDefaultRoomVersionFromJoinEvent(
joinEvent gomatrixserverlib.EventBuilder,
) gomatrixserverlib.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 gomatrixserverlib.RoomVersionV1
}
return gomatrixserverlib.RoomVersionV4
}

// isWellFormedMembershipEvent returns true if the event looks like a legitimate
// membership event.
func isWellFormedMembershipEvent(event *gomatrixserverlib.Event, roomID string, userID *spec.UserID) bool {
if membership, err := event.Membership(); err != nil {
return false
} else if membership != spec.Join {
return false
}
if event.RoomID() != roomID {
return false
}
if !event.StateKeyEquals(userID.Raw()) {
return false
}
return true
}

func checkEventsContainCreateEvent(events []*gomatrixserverlib.Event) 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 := gomatrixserverlib.RoomVersions()
if _, ok := knownVersions[gomatrixserverlib.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")
}
4 changes: 2 additions & 2 deletions load.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ type EventLoadResult struct {
type EventsLoader struct {
roomVer RoomVersion
keyRing JSONVerifier
provider AuthChainProvider
provider EventProvider
stateProvider StateProvider
// Set to true to do:
// 6. Passes authorization rules based on the current state of the room, otherwise it is "soft failed".
Expand All @@ -27,7 +27,7 @@ type EventsLoader struct {
}

// NewEventsLoader returns a new events loader
func NewEventsLoader(roomVer RoomVersion, keyRing JSONVerifier, stateProvider StateProvider, provider AuthChainProvider, performSoftFailCheck bool) *EventsLoader {
func NewEventsLoader(roomVer RoomVersion, keyRing JSONVerifier, stateProvider StateProvider, provider EventProvider, performSoftFailCheck bool) *EventsLoader {
return &EventsLoader{
roomVer: roomVer,
keyRing: keyRing,
Expand Down