Update gomatrixserverlib dep and add basic /createRoom validation
MODIFY
src/github.com/matrix-org/dendrite/clientapi/common/common.go
src/github.com/matrix-org/dendrite/clientapi/common/common.go
@@ -2,9 +2,10 @@ package common
import (
"encoding/json"
+ "net/http"
+
"github.com/matrix-org/dendrite/clientapi/jsonerror"
"github.com/matrix-org/util"
- "net/http"
)
// UnmarshalJSONRequest into the given interface pointer. Returns an error JSON response if
@@ -12,9 +13,12 @@ import (
func UnmarshalJSONRequest(req *http.Request, iface interface{}) *util.JSONResponse {
defer req.Body.Close()
if err := json.NewDecoder(req.Body).Decode(iface); err != nil {
+ // TODO: We may want to suppress the Error() return in production? It's useful when
+ // debugging because an error will be produced for both invalid/malformed JSON AND
+ // valid JSON with incorrect types for values.
return &util.JSONResponse{
Code: 400,
- JSON: jsonerror.NotJSON("The request body was not JSON"),
+ JSON: jsonerror.BadJSON("The request body could not be decoded into valid JSON. " + err.Error()),
}
}
return nil

MODIFY
src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go
src/github.com/matrix-org/dendrite/clientapi/writers/createroom.go
@@ -2,11 +2,14 @@ package writers
import (
"encoding/json"
+ "fmt"
"net/http"
+ "strings"
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/jsonerror"
"github.com/matrix-org/util"
)
@@ -22,8 +25,56 @@ type createRoomRequest struct {
RoomAliasName string `json:"room_alias_name"`
}
+func (r createRoomRequest) Validate() *util.JSONResponse {
+ whitespace := "\t\n\x0b\x0c\r " // https://docs.python.org/2/library/string.html#string.whitespace
+ // https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/handlers/room.py#L81
+ // Synapse doesn't check for ':' but we will else it will break parsers badly which split things into 2 segments.
+ if strings.ContainsAny(r.RoomAliasName, whitespace+":") {
+ return &util.JSONResponse{
+ Code: 400,
+ JSON: jsonerror.BadJSON("room_alias_name cannot contain whitespace"),
+ }
+ }
+ for _, userID := range r.Invite {
+ // TODO: We should put user ID parsing code into gomatrixserverlib and use that instead
+ // (see https://github.com/matrix-org/gomatrixserverlib/blob/3394e7c7003312043208aa73727d2256eea3d1f6/eventcontent.go#L347 )
+ // It should be a struct (with pointers into a single string to avoid copying) and
+ // we should update all refs to use UserID types rather than strings.
+ // https://github.com/matrix-org/synapse/blob/v0.19.2/synapse/types.py#L92
+ if len(userID) == 0 || userID[0] != '@' {
+ return &util.JSONResponse{
+ Code: 400,
+ JSON: jsonerror.BadJSON("user id must start with '@'"),
+ }
+ }
+ parts := strings.SplitN(userID[1:], ":", 2)
+ if len(parts) != 2 {
+ return &util.JSONResponse{
+ Code: 400,
+ JSON: jsonerror.BadJSON("user id must be in the form @localpart:domain"),
+ }
+ }
+ }
+ return nil
+}
+
+// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
+type createRoomResponse struct {
+ RoomID string `json:"room_id"`
+ RoomAlias string `json:"room_alias,omitempty"` // in synapse not spec
+}
+
// CreateRoom implements /createRoom
func CreateRoom(req *http.Request) util.JSONResponse {
+ serverName := "localhost"
+ // 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)
+}
+
+// createRoom implements /createRoom
+func createRoom(req *http.Request, roomID string) util.JSONResponse {
logger := util.GetLogger(req.Context())
userID, resErr := auth.VerifyAccessToken(req)
if resErr != nil {
@@ -34,9 +85,44 @@ func CreateRoom(req *http.Request) util.JSONResponse {
if resErr != nil {
return *resErr
}
+ // TODO: apply rate-limit
+
+ if resErr = r.Validate(); resErr != nil {
+ return *resErr
+ }
+
+ // TODO: visibility/presets/raw initial state/creation content
+
+ // TODO: Create room alias association
logger.WithFields(log.Fields{
"userID": userID,
+ "roomID": roomID,
}).Info("Creating room")
+
+ // send events into the room in order of:
+ // 1- m.room.create
+ // 2- room creator join member
+ // 3- m.room.power_levels
+ // 4- m.room.canonical_alias (opt) TODO
+ // 5- m.room.join_rules
+ // 6- m.room.history_visibility
+ // 7- m.room.guest_access (opt) TODO
+ // 8- other initial state items TODO
+ // 9- m.room.name (opt)
+ // 10- m.room.topic (opt)
+ // 11- invite events (opt) - with is_direct flag if applicable TODO
+ // 12- 3pid invite events (opt) TODO
+ // 13- m.room.aliases event for HS (if alias specified) TODO
+ // 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.
+
+ // 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
+
return util.MessageResponse(404, "Not implemented yet")
}

MODIFY
vendor/manifest
vendor/manifest
{
"importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib",
- "revision": "48ee56a33d195dc412dd919a0e81af70c9aaf4a3",
+ "revision": "ce2ae9c5812346444b0ca75d57834794cde03fb7",
"branch": "master"
},
{

MODIFY
vendor/src/github.com/matrix-org/gomatrixserverlib/event.go
vendor/src/github.com/matrix-org/gomatrixserverlib/event.go
@@ -326,6 +326,21 @@ func (e Event) Depth() int64 {
return e.fields.Depth
}
+// UnmarshalJSON implements json.Unmarshaller assuming the Event is from an untrusted source.
+// This will cause more checks than might be necessary but is probably better to be safe than sorry.
+func (e *Event) UnmarshalJSON(data []byte) (err error) {
+ *e, err = NewEventFromUntrustedJSON(data)
+ return
+}
+
+// MarshalJSON implements json.Marshaller
+func (e Event) MarshalJSON() ([]byte, error) {
+ if e.eventJSON == nil {
+ return nil, fmt.Errorf("gomatrixserverlib: cannot serialise uninitialised Event")
+ }
+ return e.eventJSON, nil
+}
+
// UnmarshalJSON implements json.Unmarshaller
func (er *EventReference) UnmarshalJSON(data []byte) error {
var tuple []rawJSON

MODIFY
vendor/src/github.com/matrix-org/gomatrixserverlib/eventauth.go
vendor/src/github.com/matrix-org/gomatrixserverlib/eventauth.go
@@ -18,7 +18,8 @@ package gomatrixserverlib
import (
"encoding/json"
"fmt"
- "sort"
+
+ "github.com/matrix-org/util"
)
const (
@@ -43,14 +44,50 @@ type StateNeeded struct {
ThirdPartyInvite []string
}
+// StateNeededForEventBuilder returns the event types and state_keys needed to authenticate the
+// event being built. These events should be put under 'auth_events' for the event being built.
+// Returns an error if the state needed could not be calculated with the given builder, e.g
+// if there is a m.room.member without a membership key.
+func StateNeededForEventBuilder(builder *EventBuilder) (result StateNeeded, err error) {
+ // Extract the 'content' object from the event if it is m.room.member as we need to know 'membership'
+ var content *memberContent
+ if builder.Type == "m.room.member" {
+ if err = json.Unmarshal(builder.content, &content); err != nil {
+ err = errorf("unparsable member event content: %s", err.Error())
+ return
+ }
+ }
+ err = accumulateStateNeeded(&result, builder.Type, builder.Sender, builder.StateKey, content)
+ result.Member = util.UniqueStrings(result.Member)
+ result.ThirdPartyInvite = util.UniqueStrings(result.ThirdPartyInvite)
+ return
+}
+
// StateNeededForAuth returns the event types and state_keys needed to authenticate an event.
// This takes a list of events to facilitate bulk processing when doing auth checks as part of state conflict resolution.
func StateNeededForAuth(events []Event) (result StateNeeded) {
- var members []string
- var thirdpartyinvites []string
-
for _, event := range events {
- switch event.Type() {
+ // Extract the 'content' object from the event if it is m.room.member as we need to know 'membership'
+ var content *memberContent
+ if event.Type() == "m.room.member" {
+ c, err := newMemberContentFromEvent(event)
+ if err == nil {
+ content = &c
+ }
+ }
+ // Ignore errors when accumulating state needed.
+ // The event will be rejected when the actual checks encounter the same error.
+ _ = accumulateStateNeeded(&result, event.Type(), event.Sender(), event.StateKey(), content)
+ }
+
+ // Deduplicate the state keys.
+ result.Member = util.UniqueStrings(result.Member)
+ result.ThirdPartyInvite = util.UniqueStrings(result.ThirdPartyInvite)
+ return
+}
+
+func accumulateStateNeeded(result *StateNeeded, eventType, sender string, stateKey *string, content *memberContent) (err error) {
+ switch eventType {
case "m.room.create":
// The create event doesn't require any state to authenticate.
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L123
@@ -73,30 +110,25 @@ func StateNeededForAuth(events []Event) (result StateNeeded) {
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L370
// * And optionally may require a m.third_party_invite event
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L393
- content, err := newMemberContentFromEvent(event)
- if err != nil {
- // If we hit an error decoding the content we ignore it here.
- // The event will be rejected when the actual checks encounter the same error.
- continue
+ if content == nil {
+ err = errorf("missing memberContent for m.room.member event")
+ return
}
result.Create = true
result.PowerLevels = true
- stateKey := event.StateKey()
if stateKey != nil {
- members = append(members, event.Sender(), *stateKey)
+ result.Member = append(result.Member, sender, *stateKey)
}
if content.Membership == join {
result.JoinRules = true
}
if content.ThirdPartyInvite != nil {
- token, err := thirdPartyInviteToken(content.ThirdPartyInvite)
- if err != nil {
- // If we hit an error decoding the content we ignore it here.
- // The event will be rejected when the actual checks encounter the same error.
- continue
- } else {
- thirdpartyinvites = append(thirdpartyinvites, token)
+ token, tokErr := thirdPartyInviteToken(content.ThirdPartyInvite)
+ if tokErr != nil {
+ err = errorf("could not get third-party token: %s", tokErr)
+ return
}
+ result.ThirdPartyInvite = append(result.ThirdPartyInvite, token)
}
default:
@@ -107,41 +139,13 @@ func StateNeededForAuth(events []Event) (result StateNeeded) {
// https://github.com/matrix-org/synapse/blob/v0.18.5/synapse/api/auth.py#L196
result.Create = true
result.PowerLevels = true
- members = append(members, event.Sender())
+ result.Member = append(result.Member, sender)
}
- }
-
- // Deduplicate the state keys.
- sort.Strings(members)
- result.Member = members[:unique(sort.StringSlice(members))]
- sort.Strings(thirdpartyinvites)
- result.ThirdPartyInvite = thirdpartyinvites[:unique(sort.StringSlice(thirdpartyinvites))]
return
}
-// Remove duplicate items from a sorted list.
-// Takes the same interface as sort.Sort
-// Returns the length of the data without duplicates
-// Uses the last occurrence of a duplicate.
-// O(n).
-func unique(data sort.Interface) int {
- length := data.Len()
- if length == 0 {
- return 0
- }
- j := 0
- for i := 1; i < length; i++ {
- if data.Less(i-1, i) {
- data.Swap(i-1, j)
- j++
- }
- }
- data.Swap(length-1, j)
- return j + 1
-}
-
// thirdPartyInviteToken extracts the token from the third_party_invite.
-func thirdPartyInviteToken(thirdPartyInviteData json.RawMessage) (string, error) {
+func thirdPartyInviteToken(thirdPartyInviteData rawJSON) (string, error) {
var thirdPartyInvite struct {
Signed struct {
Token string `json:"token"`

MODIFY
vendor/src/github.com/matrix-org/gomatrixserverlib/eventauth_test.go
vendor/src/github.com/matrix-org/gomatrixserverlib/eventauth_test.go
@@ -68,7 +68,7 @@ func (tel *testEventList) UnmarshalJSON(data []byte) error {
return nil
}
-func testStateNeededForAuth(t *testing.T, eventdata string, want StateNeeded) {
+func testStateNeededForAuth(t *testing.T, eventdata string, builder *EventBuilder, want StateNeeded) {
var events testEventList
if err := json.Unmarshal([]byte(eventdata), &events); err != nil {
panic(err)
@@ -77,11 +77,24 @@ func testStateNeededForAuth(t *testing.T, eventdata string, want StateNeeded) {
if !stateNeededEquals(got, want) {
t.Errorf("Testing StateNeededForAuth(%#v), wanted %#v got %#v", events, want, got)
}
+ if builder != nil {
+ got, err := StateNeededForEventBuilder(builder)
+ if !stateNeededEquals(got, want) {
+ t.Errorf("Testing StateNeededForEventBuilder(%#v), wanted %#v got %#v", events, want, got)
+ }
+ if err != nil {
+ panic(err)
+ }
+ }
}
func TestStateNeededForCreate(t *testing.T) {
// Create events don't need anything.
- testStateNeededForAuth(t, `[{"type": "m.room.create"}]`, StateNeeded{})
+ skey := ""
+ testStateNeededForAuth(t, `[{"type": "m.room.create"}]`, &EventBuilder{
+ Type: "m.room.create",
+ StateKey: &skey,
+ }, StateNeeded{})
}
func TestStateNeededForMessage(t *testing.T) {
@@ -89,7 +102,10 @@ func TestStateNeededForMessage(t *testing.T) {
testStateNeededForAuth(t, `[{
"type": "m.room.message",
"sender": "@u1:a"
- }]`, StateNeeded{
+ }]`, &EventBuilder{
+ Type: "m.room.message",
+ Sender: "@u1:a",
+ }, StateNeeded{
Create: true,
PowerLevels: true,
Member: []string{"@u1:a"},
@@ -98,18 +114,27 @@ func TestStateNeededForMessage(t *testing.T) {
func TestStateNeededForAlias(t *testing.T) {
// Alias events need only the create event.
- testStateNeededForAuth(t, `[{"type": "m.room.aliases"}]`, StateNeeded{
+ testStateNeededForAuth(t, `[{"type": "m.room.aliases"}]`, &EventBuilder{
+ Type: "m.room.aliases",
+ }, StateNeeded{
Create: true,
})
}
func TestStateNeededForJoin(t *testing.T) {
+ skey := "@u1:a"
+ b := EventBuilder{
+ Type: "m.room.member",
+ StateKey: &skey,
+ Sender: "@u1:a",
+ }
+ b.SetContent(memberContent{"join", nil})
testStateNeededForAuth(t, `[{
"type": "m.room.member",
"state_key": "@u1:a",
"sender": "@u1:a",
"content": {"membership": "join"}
- }]`, StateNeeded{
+ }]`, &b, StateNeeded{
Create: true,
JoinRules: true,
PowerLevels: true,
@@ -118,12 +143,19 @@ func TestStateNeededForJoin(t *testing.T) {
}
func TestStateNeededForInvite(t *testing.T) {
+ skey := "@u2:b"
+ b := EventBuilder{
+ Type: "m.room.member",
+ StateKey: &skey,
+ Sender: "@u1:a",
+ }
+ b.SetContent(memberContent{"invite", nil})
testStateNeededForAuth(t, `[{
"type": "m.room.member",
"state_key": "@u2:b",
"sender": "@u1:a",
"content": {"membership": "invite"}
- }]`, StateNeeded{
+ }]`, &b, StateNeeded{
Create: true,
PowerLevels: true,
Member: []string{"@u1:a", "@u2:b"},
@@ -131,6 +163,13 @@ func TestStateNeededForInvite(t *testing.T) {
}
func TestStateNeededForInvite3PID(t *testing.T) {
+ skey := "@u2:b"
+ b := EventBuilder{
+ Type: "m.room.member",
+ StateKey: &skey,
+ Sender: "@u1:a",
+ }
+ b.SetContent(memberContent{"invite", rawJSON(`{"signed":{"token":"my_token"}}`)})
testStateNeededForAuth(t, `[{
"type": "m.room.member",
"state_key": "@u2:b",
@@ -143,7 +182,7 @@ func TestStateNeededForInvite3PID(t *testing.T) {
}
}
}
- }]`, StateNeeded{
+ }]`, &b, StateNeeded{
Create: true,
PowerLevels: true,
Member: []string{"@u1:a", "@u2:b"},

MODIFY
vendor/src/github.com/matrix-org/gomatrixserverlib/eventcontent.go
vendor/src/github.com/matrix-org/gomatrixserverlib/eventcontent.go
@@ -108,7 +108,7 @@ type memberContent struct {
// We use the membership key in order to check if the user is in the room.
Membership string `json:"membership"`
// We use the third_party_invite key to special case thirdparty invites.
- ThirdPartyInvite json.RawMessage `json:"third_party_invite"`
+ ThirdPartyInvite rawJSON `json:"third_party_invite,omitempty"`
}
// newMemberContentFromAuthEvents loads the member content from the member event for the user ID in the auth events.

Update gomatrixserverlib dep and add basic /createRoom validation (#31)