From 6ca36304556fa8f87e904e27033f69661125ef42 Mon Sep 17 00:00:00 2001 From: Kegan Dougal Date: Thu, 9 Mar 2017 16:18:40 +0000 Subject: [PATCH] Create the initial set of state events for room creation This includes authing the resulting events. --- .../dendrite/clientapi/clientapi.go | 15 +- .../dendrite/clientapi/common/common.go | 9 + .../dendrite/clientapi/common/eventcontent.go | 62 +++++++ .../dendrite/clientapi/config/config.go | 10 + .../dendrite/clientapi/routing/routing.go | 23 +-- .../dendrite/clientapi/writers/createroom.go | 172 ++++++++++++++++-- vendor/manifest | 2 +- .../matrix-org/gomatrixserverlib/event.go | 2 +- 8 files changed, 263 insertions(+), 32 deletions(-) create mode 100644 src/github.com/matrix-org/dendrite/clientapi/common/eventcontent.go create mode 100644 src/github.com/matrix-org/dendrite/clientapi/config/config.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/clientapi.go b/src/github.com/matrix-org/dendrite/clientapi/clientapi.go index 95055b29e..a487135bc 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/clientapi.go +++ b/src/github.com/matrix-org/dendrite/clientapi/clientapi.go @@ -5,6 +5,9 @@ import ( "os" "path/filepath" + "golang.org/x/crypto/ed25519" + + "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/routing" log "github.com/Sirupsen/logrus" @@ -36,6 +39,16 @@ func main() { setupLogging(logDir) } log.Info("Starting clientapi") - routing.Setup(http.DefaultServeMux, http.DefaultClient) + // TODO: Rather than generating a new key on every startup, we should be + // reading a PEM formatted file instead. + _, privKey, err := ed25519.GenerateKey(nil) + if err != nil { + log.Panicf("Failed to generate private key: %s", err) + } + routing.Setup(http.DefaultServeMux, http.DefaultClient, config.ClientAPI{ + ServerName: "localhost", + KeyID: "ed25519:something", + PrivateKey: privKey, + }) log.Fatal(http.ListenAndServe(bindAddr, nil)) } diff --git a/src/github.com/matrix-org/dendrite/clientapi/common/common.go b/src/github.com/matrix-org/dendrite/clientapi/common/common.go index cbc94a6f5..f76c85cc7 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/common/common.go +++ b/src/github.com/matrix-org/dendrite/clientapi/common/common.go @@ -8,6 +8,15 @@ import ( "github.com/matrix-org/util" ) +// StateTuple is the tuple of an event type and an event state_key, typically used as a key +// in maps. +type StateTuple struct { + // Type is the event type e.g "m.room.name" + Type string + // Key is the state key e.g. "" + Key string +} + // UnmarshalJSONRequest into the given interface pointer. Returns an error JSON response if // there was a problem unmarshalling. Calling this function consumes the request body. func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse { diff --git a/src/github.com/matrix-org/dendrite/clientapi/common/eventcontent.go b/src/github.com/matrix-org/dendrite/clientapi/common/eventcontent.go new file mode 100644 index 000000000..061bc18e1 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/common/eventcontent.go @@ -0,0 +1,62 @@ +package common + +// CreateContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-create +type CreateContent struct { + Creator string `json:"creator"` + Federate *bool `json:"m.federate,omitempty"` +} + +// MemberContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-member +type MemberContent struct { + Membership string `json:"membership"` + DisplayName string `json:"displayname,omitempty"` + AvatarURL string `json:"avatar_url,omitempty"` + // TODO: ThirdPartyInvite string `json:"third_party_invite,omitempty"` +} + +// JoinRulesContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-join-rules +type JoinRulesContent struct { + JoinRule string `json:"join_rule"` +} + +// HistoryVisibilityContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-history-visibility +type HistoryVisibilityContent struct { + HistoryVisibility string `json:"history_visibility"` +} + +// PowerLevelContent is the event content for http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-power-levels +type PowerLevelContent struct { + EventsDefault int `json:"events_default"` + Invite int `json:"invite"` + StateDefault int `json:"state_default"` + Redact int `json:"redact"` + Ban int `json:"ban"` + UsersDefault int `json:"users_default"` + Events map[string]int `json:"events"` + Kick int `json:"kick"` + Users map[string]int `json:"users"` +} + +// InitialPowerLevelsContent returns the initial values for m.room.power_levels on room creation +// if they have not been specified. +// http://matrix.org/docs/spec/client_server/r0.2.0.html#m-room-power-levels +// https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L294 +func InitialPowerLevelsContent(roomCreator string) PowerLevelContent { + return PowerLevelContent{ + EventsDefault: 0, + Invite: 0, + StateDefault: 50, + Redact: 50, + Ban: 50, + UsersDefault: 0, + Events: map[string]int{ + "m.room.name": 50, + "m.room.power_levels": 100, + "m.room.history_visibility": 100, + "m.room.canonical_alias": 50, + "m.room.avatar": 50, + }, + Kick: 50, + Users: map[string]int{roomCreator: 100}, + } +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/config/config.go b/src/github.com/matrix-org/dendrite/clientapi/config/config.go new file mode 100644 index 000000000..f743dcba8 --- /dev/null +++ b/src/github.com/matrix-org/dendrite/clientapi/config/config.go @@ -0,0 +1,10 @@ +package config + +import "golang.org/x/crypto/ed25519" + +// ClientAPI contains the config information necessary to spin up a clientapi process. +type ClientAPI struct { + ServerName string + PrivateKey ed25519.PrivateKey + KeyID string +} diff --git a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go index 22fcfbd68..50f502405 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go +++ b/src/github.com/matrix-org/dendrite/clientapi/routing/routing.go @@ -4,6 +4,7 @@ import ( "net/http" "github.com/gorilla/mux" + "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/readers" "github.com/matrix-org/dendrite/clientapi/writers" "github.com/matrix-org/util" @@ -14,17 +15,17 @@ const pathPrefixR0 = "/_matrix/client/r0" // Setup registers HTTP handlers with the given ServeMux. It also supplies the given http.Client // to clients which need to make outbound HTTP requests. -func Setup(servMux *http.ServeMux, httpClient *http.Client) { +func Setup(servMux *http.ServeMux, httpClient *http.Client, cfg config.ClientAPI) { apiMux := mux.NewRouter() r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter() - r0mux.Handle("/createRoom", make("createRoom", wrap(func(req *http.Request) util.JSONResponse { - return writers.CreateRoom(req) + r0mux.Handle("/createRoom", make("createRoom", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { + return writers.CreateRoom(req, cfg) }))) - r0mux.Handle("/sync", make("sync", wrap(func(req *http.Request) util.JSONResponse { + r0mux.Handle("/sync", make("sync", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { return readers.Sync(req) }))) r0mux.Handle("/rooms/{roomID}/send/{eventType}", - make("send_message", wrap(func(req *http.Request) util.JSONResponse { + make("send_message", util.NewJSONRequestHandler(func(req *http.Request) util.JSONResponse { vars := mux.Vars(req) return writers.SendMessage(req, vars["roomID"], vars["eventType"]) })), @@ -38,15 +39,3 @@ func Setup(servMux *http.ServeMux, httpClient *http.Client) { func make(metricsName string, h util.JSONRequestHandler) http.Handler { return prometheus.InstrumentHandler(metricsName, util.MakeJSONAPI(h)) } - -// jsonRequestHandlerWrapper is a wrapper to allow in-line functions to conform to util.JSONRequestHandler -type jsonRequestHandlerWrapper struct { - function func(req *http.Request) util.JSONResponse -} - -func (r *jsonRequestHandlerWrapper) OnIncomingRequest(req *http.Request) util.JSONResponse { - return r.function(req) -} -func wrap(f func(req *http.Request) util.JSONResponse) *jsonRequestHandlerWrapper { - return &jsonRequestHandlerWrapper{f} -} diff --git a/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go b/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go index 1dde64e74..a901d7ea4 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go +++ b/src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go @@ -5,11 +5,14 @@ import ( "fmt" "net/http" "strings" + "time" log "github.com/Sirupsen/logrus" "github.com/matrix-org/dendrite/clientapi/auth" "github.com/matrix-org/dendrite/clientapi/common" + "github.com/matrix-org/dendrite/clientapi/config" "github.com/matrix-org/dendrite/clientapi/jsonerror" + "github.com/matrix-org/gomatrixserverlib" "github.com/matrix-org/util" ) @@ -64,17 +67,23 @@ type createRoomResponse struct { RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec } +// fledglingEvent is a helper representation of an event used when creating many events in succession. +type fledglingEvent struct { + Type string + StateKey *string + Content interface{} +} + // CreateRoom implements /createRoom -func CreateRoom(req *http.Request) util.JSONResponse { - serverName := "localhost" +func CreateRoom(req *http.Request, cfg config.ClientAPI) util.JSONResponse { // TODO: Check room ID doesn't clash with an existing one, and we // probably shouldn't be using pseudo-random strings, maybe GUIDs? - roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), serverName) - return createRoom(req, roomID) + roomID := fmt.Sprintf("!%s:%s", util.RandomString(16), cfg.ServerName) + return createRoom(req, cfg, roomID) } // createRoom implements /createRoom -func createRoom(req *http.Request, roomID string) util.JSONResponse { +func createRoom(req *http.Request, cfg config.ClientAPI, roomID string) util.JSONResponse { logger := util.GetLogger(req.Context()) userID, resErr := auth.VerifyAccessToken(req) if resErr != nil { @@ -98,7 +107,11 @@ func createRoom(req *http.Request, roomID string) util.JSONResponse { logger.WithFields(log.Fields{ "userID": userID, "roomID": roomID, - }).Info("Creating room") + }).Info("Creating new room") + + // Remember events we've built and key off the state tuple so we can look them up easily when filling in auth_events + builtEventMap := make(map[common.StateTuple]*gomatrixserverlib.Event) + var builtEvents []*gomatrixserverlib.Event // send events into the room in order of: // 1- m.room.create @@ -117,12 +130,147 @@ func createRoom(req *http.Request, roomID string) util.JSONResponse { // This differs from Synapse slightly. Synapse would vary the ordering of 3-7 // depending on if those events were in "initial_state" or not. This made it // harder to reason about, hence sticking to a strict static ordering. + // TODO: Synapse has txn/token ID on each event. Do we need to do this here? + eventsToMake := []fledglingEvent{ + {"m.room.create", emptyString(), common.CreateContent{Creator: userID}}, + {"m.room.member", &userID, common.MemberContent{Membership: "join"}}, // TODO: Set avatar_url / displayname + {"m.room.power_levels", emptyString(), common.InitialPowerLevelsContent(userID)}, + // TODO: m.room.canonical_alias + {"m.room.join_rules", emptyString(), common.JoinRulesContent{"public"}}, // FIXME: Allow this to be changed + {"m.room.history_visibility", emptyString(), common.HistoryVisibilityContent{"joined"}}, // FIXME: Allow this to be changed + // TODO: m.room.guest_access + // TODO: Other initial state items + // TODO: m.room.name + // TODO: m.room.topic + // TODO: invite events + // TODO: 3pid invite events + // TODO m.room.aliases + } - // f.e event: - // - validate required keys/types (EventValidator in synapse) - // - set additional keys (displayname/avatar_url for m.room.member) - // - set token(?) and txn id - // - then https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/message.py#L419 + for i, e := range eventsToMake { + depth := i + 1 // depth starts at 1 - return util.MessageResponse(404, "Not implemented yet") + builder := gomatrixserverlib.EventBuilder{ + Sender: userID, + RoomID: roomID, + Type: e.Type, + StateKey: e.StateKey, + Depth: int64(depth), + } + builder.SetContent(e.Content) + ev, err := buildEvent(&builder, builtEventMap, cfg) + if err != nil { + return util.ErrorResponse(err) + } + builtEventMap[common.StateTuple{e.Type, *e.StateKey}] = ev + builtEvents = append(builtEvents, ev) + } + authEvents := authEventProvider{builtEventMap} + + // auth each event in turn + for _, e := range builtEvents { + if err := gomatrixserverlib.Allowed(*e, &authEvents); err != nil { + return util.ErrorResponse(err) + } + } + + return util.JSONResponse{ + Code: 200, + JSON: builtEvents, + } +} + +// buildEvent fills out auth_events for the builder then builds the event +func buildEvent(builder *gomatrixserverlib.EventBuilder, + events map[common.StateTuple]*gomatrixserverlib.Event, + cfg config.ClientAPI) (*gomatrixserverlib.Event, error) { + + eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder) + if err != nil { + return nil, err + } + builder.AuthEvents = authEventsFromStateNeeded(eventsNeeded, events) + eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.ServerName) + now := time.Now() + event, err := builder.Build(eventID, now, cfg.ServerName, cfg.KeyID, cfg.PrivateKey) + if err != nil { + return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %s", builder.Type, err) + } + return &event, nil +} + +func authEventsFromStateNeeded(eventsNeeded gomatrixserverlib.StateNeeded, + events map[common.StateTuple]*gomatrixserverlib.Event) (authEvents []gomatrixserverlib.EventReference) { + + // These events are only "needed" if they exist, so if they don't exist we can safely ignore them. + if eventsNeeded.Create { + ev := events[common.StateTuple{"m.room.create", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + if eventsNeeded.JoinRules { + ev := events[common.StateTuple{"m.room.join_rules", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + if eventsNeeded.PowerLevels { + ev := events[common.StateTuple{"m.room.power_levels", ""}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + + for _, userID := range eventsNeeded.Member { + ev := events[common.StateTuple{"m.room.member", userID}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + for _, token := range eventsNeeded.ThirdPartyInvite { + ev := events[common.StateTuple{"m.room.member", token}] + if ev != nil { + authEvents = append(authEvents, ev.EventReference()) + } + } + return +} + +type authEventProvider struct { + events map[common.StateTuple]*gomatrixserverlib.Event +} + +func (a *authEventProvider) Create() (ev *gomatrixserverlib.Event, err error) { + return a.fetch(common.StateTuple{"m.room.create", ""}) +} + +func (a *authEventProvider) JoinRules() (ev *gomatrixserverlib.Event, err error) { + return a.fetch(common.StateTuple{"m.room.join_rules", ""}) +} + +func (a *authEventProvider) PowerLevels() (ev *gomatrixserverlib.Event, err error) { + return a.fetch(common.StateTuple{"m.room.power_levels", ""}) +} + +func (a *authEventProvider) Member(stateKey string) (ev *gomatrixserverlib.Event, err error) { + return a.fetch(common.StateTuple{"m.room.member", stateKey}) +} + +func (a *authEventProvider) ThirdPartyInvite(stateKey string) (ev *gomatrixserverlib.Event, err error) { + return a.fetch(common.StateTuple{"m.room.third_party_invite", stateKey}) +} + +func (a *authEventProvider) fetch(tuple common.StateTuple) (ev *gomatrixserverlib.Event, err error) { + ev, ok := a.events[tuple] + if !ok { + err = fmt.Errorf("Cannot find auth event %+v", tuple) + return + } + return +} + +func emptyString() *string { + skey := "" + return &skey } diff --git a/vendor/manifest b/vendor/manifest index fe5d5bbc4..5435122fa 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -92,7 +92,7 @@ { "importpath": "github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib", - "revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7", + "revision": "4218890fdd60e73cc5539ec40b86fd51568f4a19", "branch": "master" }, { diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go index e1b6ee7d8..439642a31 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go @@ -100,7 +100,7 @@ func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID strin EventBuilder EventID string `json:"event_id"` RawContent rawJSON `json:"content"` - RawUnsigned rawJSON `json:"unsigned"` + RawUnsigned rawJSON `json:"unsigned,omitempty"` OriginServerTS int64 `json:"origin_server_ts"` Origin string `json:"origin"` }