Create the initial set of state events for room creation

This includes authing the resulting events.
This commit is contained in:
Kegan Dougal 2017-03-09 16:18:40 +00:00
parent 533e5dba5a
commit 6ca3630455
8 changed files with 263 additions and 32 deletions

View file

@ -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))
}

View file

@ -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 {

View file

@ -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},
}
}

View file

@ -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
}

View file

@ -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}
}

View file

@ -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
}

2
vendor/manifest vendored
View file

@ -92,7 +92,7 @@
{
"importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib",
"revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7",
"revision": "4218890fdd60e73cc5539ec40b86fd51568f4a19",
"branch": "master"
},
{

View file

@ -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"`
}