aboutsummaryrefslogtreecommitdiff
path: root/clientapi/routing
diff options
context:
space:
mode:
Diffstat (limited to 'clientapi/routing')
-rw-r--r--clientapi/routing/account_data.go76
-rw-r--r--clientapi/routing/createroom.go337
-rw-r--r--clientapi/routing/device.go155
-rw-r--r--clientapi/routing/directory.go183
-rw-r--r--clientapi/routing/filter.go123
-rw-r--r--clientapi/routing/joinroom.go333
-rw-r--r--clientapi/routing/login.go152
-rw-r--r--clientapi/routing/logout.go71
-rw-r--r--clientapi/routing/membership.go217
-rw-r--r--clientapi/routing/memberships.go60
-rw-r--r--clientapi/routing/profile.go292
-rw-r--r--clientapi/routing/register.go958
-rw-r--r--clientapi/routing/register_test.go209
-rw-r--r--clientapi/routing/routing.go413
-rw-r--r--clientapi/routing/sendevent.go153
-rw-r--r--clientapi/routing/sendtyping.go80
-rw-r--r--clientapi/routing/threepid.go178
-rw-r--r--clientapi/routing/voip.go78
-rw-r--r--clientapi/routing/whoami.go34
19 files changed, 4102 insertions, 0 deletions
diff --git a/clientapi/routing/account_data.go b/clientapi/routing/account_data.go
new file mode 100644
index 00000000..30e00f72
--- /dev/null
+++ b/clientapi/routing/account_data.go
@@ -0,0 +1,76 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "io/ioutil"
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/gomatrixserverlib"
+
+ "github.com/matrix-org/util"
+)
+
+// SaveAccountData implements PUT /user/{userId}/[rooms/{roomId}/]account_data/{type}
+func SaveAccountData(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+ userID string, roomID string, dataType string, syncProducer *producers.SyncAPIProducer,
+) util.JSONResponse {
+ if req.Method != http.MethodPut {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+ }
+
+ if userID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("userID does not match the current user"),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ defer req.Body.Close() // nolint: errcheck
+
+ body, err := ioutil.ReadAll(req.Body)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := accountDB.SaveAccountData(
+ req.Context(), localpart, roomID, dataType, string(body),
+ ); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := syncProducer.SendData(userID, roomID, dataType); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/createroom.go b/clientapi/routing/createroom.go
new file mode 100644
index 00000000..a7187c49
--- /dev/null
+++ b/clientapi/routing/createroom.go
@@ -0,0 +1,337 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
+ roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+ log "github.com/sirupsen/logrus"
+)
+
+// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-createroom
+type createRoomRequest struct {
+ Invite []string `json:"invite"`
+ Name string `json:"name"`
+ Visibility string `json:"visibility"`
+ Topic string `json:"topic"`
+ Preset string `json:"preset"`
+ CreationContent map[string]interface{} `json:"creation_content"`
+ InitialState []fledglingEvent `json:"initial_state"`
+ RoomAliasName string `json:"room_alias_name"`
+ GuestCanJoin bool `json:"guest_can_join"`
+}
+
+const (
+ presetPrivateChat = "private_chat"
+ presetTrustedPrivateChat = "trusted_private_chat"
+ presetPublicChat = "public_chat"
+)
+
+const (
+ joinRulePublic = "public"
+ joinRuleInvite = "invite"
+)
+const (
+ historyVisibilityShared = "shared"
+ // TODO: These should be implemented once history visibility is implemented
+ // historyVisibilityWorldReadable = "world_readable"
+ // historyVisibilityInvited = "invited"
+)
+
+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: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("room_alias_name cannot contain whitespace or ':'"),
+ }
+ }
+ 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 _, _, err := gomatrixserverlib.SplitID('@', userID); err != nil {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("user id must be in the form @localpart:domain"),
+ }
+ }
+ }
+ switch r.Preset {
+ case presetPrivateChat, presetTrustedPrivateChat, presetPublicChat, "":
+ default:
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("preset must be any of 'private_chat', 'trusted_private_chat', 'public_chat'"),
+ }
+ }
+
+ 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
+}
+
+// fledglingEvent is a helper representation of an event used when creating many events in succession.
+type fledglingEvent struct {
+ Type string `json:"type"`
+ StateKey string `json:"state_key"`
+ Content interface{} `json:"content"`
+}
+
+// CreateRoom implements /createRoom
+func CreateRoom(
+ req *http.Request, device *authtypes.Device,
+ cfg config.Dendrite, producer *producers.RoomserverProducer,
+ accountDB *accounts.Database, aliasAPI roomserverAPI.RoomserverAliasAPI,
+ asAPI appserviceAPI.AppServiceQueryAPI,
+) util.JSONResponse {
+ // TODO (#267): 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), cfg.Matrix.ServerName)
+ return createRoom(req, device, cfg, roomID, producer, accountDB, aliasAPI, asAPI)
+}
+
+// createRoom implements /createRoom
+// nolint: gocyclo
+func createRoom(
+ req *http.Request, device *authtypes.Device,
+ cfg config.Dendrite, roomID string, producer *producers.RoomserverProducer,
+ accountDB *accounts.Database, aliasAPI roomserverAPI.RoomserverAliasAPI,
+ asAPI appserviceAPI.AppServiceQueryAPI,
+) util.JSONResponse {
+ logger := util.GetLogger(req.Context())
+ userID := device.UserID
+ var r createRoomRequest
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return *resErr
+ }
+ // TODO: apply rate-limit
+
+ if resErr = r.Validate(); resErr != nil {
+ return *resErr
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+ // TODO: visibility/presets/raw initial state/creation content
+ // TODO: Create room alias association
+ // Make sure this doesn't fall into an application service's namespace though!
+
+ logger.WithFields(log.Fields{
+ "userID": userID,
+ "roomID": roomID,
+ }).Info("Creating new room")
+
+ profile, err := appserviceAPI.RetreiveUserProfile(req.Context(), userID, asAPI, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ membershipContent := common.MemberContent{
+ Membership: "join",
+ DisplayName: profile.DisplayName,
+ AvatarURL: profile.AvatarURL,
+ }
+
+ var joinRules, historyVisibility string
+ switch r.Preset {
+ case presetPrivateChat:
+ joinRules = joinRuleInvite
+ historyVisibility = historyVisibilityShared
+ case presetTrustedPrivateChat:
+ joinRules = joinRuleInvite
+ historyVisibility = historyVisibilityShared
+ // TODO If trusted_private_chat, all invitees are given the same power level as the room creator.
+ case presetPublicChat:
+ joinRules = joinRulePublic
+ historyVisibility = historyVisibilityShared
+ default:
+ // Default room rules, r.Preset was previously checked for valid values so
+ // only a request with no preset should end up here.
+ joinRules = joinRuleInvite
+ historyVisibility = historyVisibilityShared
+ }
+
+ var builtEvents []gomatrixserverlib.Event
+
+ // 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)
+ // 8- other initial state items
+ // 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.
+ // TODO: Synapse has txn/token ID on each event. Do we need to do this here?
+ eventsToMake := []fledglingEvent{
+ {"m.room.create", "", common.CreateContent{Creator: userID}},
+ {"m.room.member", userID, membershipContent},
+ {"m.room.power_levels", "", common.InitialPowerLevelsContent(userID)},
+ // TODO: m.room.canonical_alias
+ {"m.room.join_rules", "", common.JoinRulesContent{JoinRule: joinRules}},
+ {"m.room.history_visibility", "", common.HistoryVisibilityContent{HistoryVisibility: historyVisibility}},
+ }
+ if r.GuestCanJoin {
+ eventsToMake = append(eventsToMake, fledglingEvent{"m.room.guest_access", "", common.GuestAccessContent{GuestAccess: "can_join"}})
+ }
+ eventsToMake = append(eventsToMake, r.InitialState...)
+ if r.Name != "" {
+ eventsToMake = append(eventsToMake, fledglingEvent{"m.room.name", "", common.NameContent{Name: r.Name}})
+ }
+ if r.Topic != "" {
+ eventsToMake = append(eventsToMake, fledglingEvent{"m.room.topic", "", common.TopicContent{Topic: r.Topic}})
+ }
+ // TODO: invite events
+ // TODO: 3pid invite events
+ // TODO: m.room.aliases
+
+ authEvents := gomatrixserverlib.NewAuthEvents(nil)
+ for i, e := range eventsToMake {
+ depth := i + 1 // depth starts at 1
+
+ builder := gomatrixserverlib.EventBuilder{
+ Sender: userID,
+ RoomID: roomID,
+ Type: e.Type,
+ StateKey: &e.StateKey,
+ Depth: int64(depth),
+ }
+ err = builder.SetContent(e.Content)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+ if i > 0 {
+ builder.PrevEvents = []gomatrixserverlib.EventReference{builtEvents[i-1].EventReference()}
+ }
+ var ev *gomatrixserverlib.Event
+ ev, err = buildEvent(&builder, &authEvents, cfg, evTime)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err = gomatrixserverlib.Allowed(*ev, &authEvents); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // Add the event to the list of auth events
+ builtEvents = append(builtEvents, *ev)
+ err = authEvents.AddEvent(ev)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+ }
+
+ // send events to the room server
+ _, err = producer.SendEvents(req.Context(), builtEvents, cfg.Matrix.ServerName, nil)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // TODO(#269): Reserve room alias while we create the room. This stops us
+ // from creating the room but still failing due to the alias having already
+ // been taken.
+ var roomAlias string
+ if r.RoomAliasName != "" {
+ roomAlias = fmt.Sprintf("#%s:%s", r.RoomAliasName, cfg.Matrix.ServerName)
+
+ aliasReq := roomserverAPI.SetRoomAliasRequest{
+ Alias: roomAlias,
+ RoomID: roomID,
+ UserID: userID,
+ }
+
+ var aliasResp roomserverAPI.SetRoomAliasResponse
+ err = aliasAPI.SetRoomAlias(req.Context(), &aliasReq, &aliasResp)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if aliasResp.AliasExists {
+ return util.MessageResponse(400, "Alias already exists")
+ }
+ }
+
+ response := createRoomResponse{
+ RoomID: roomID,
+ RoomAlias: roomAlias,
+ }
+
+ return util.JSONResponse{
+ Code: 200,
+ JSON: response,
+ }
+}
+
+// buildEvent fills out auth_events for the builder then builds the event
+func buildEvent(
+ builder *gomatrixserverlib.EventBuilder,
+ provider gomatrixserverlib.AuthEventProvider,
+ cfg config.Dendrite,
+ evTime time.Time,
+) (*gomatrixserverlib.Event, error) {
+ eventsNeeded, err := gomatrixserverlib.StateNeededForEventBuilder(builder)
+ if err != nil {
+ return nil, err
+ }
+ refs, err := eventsNeeded.AuthEventReferences(provider)
+ if err != nil {
+ return nil, err
+ }
+ builder.AuthEvents = refs
+ eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), cfg.Matrix.ServerName)
+ event, err := builder.Build(eventID, evTime, cfg.Matrix.ServerName, cfg.Matrix.KeyID, cfg.Matrix.PrivateKey)
+ if err != nil {
+ return nil, fmt.Errorf("cannot build event %s : Builder failed to build. %s", builder.Type, err)
+ }
+ return &event, nil
+}
diff --git a/clientapi/routing/device.go b/clientapi/routing/device.go
new file mode 100644
index 00000000..cf6f24a7
--- /dev/null
+++ b/clientapi/routing/device.go
@@ -0,0 +1,155 @@
+// Copyright 2017 Paul Tötterman <paul.totterman@iki.fi>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "database/sql"
+ "encoding/json"
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+type deviceJSON struct {
+ DeviceID string `json:"device_id"`
+ UserID string `json:"user_id"`
+}
+
+type devicesJSON struct {
+ Devices []deviceJSON `json:"devices"`
+}
+
+type deviceUpdateJSON struct {
+ DisplayName *string `json:"display_name"`
+}
+
+// GetDeviceByID handles /devices/{deviceID}
+func GetDeviceByID(
+ req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
+ deviceID string,
+) util.JSONResponse {
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ ctx := req.Context()
+ dev, err := deviceDB.GetDeviceByID(ctx, localpart, deviceID)
+ if err == sql.ErrNoRows {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("Unknown device"),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: deviceJSON{
+ DeviceID: dev.ID,
+ UserID: dev.UserID,
+ },
+ }
+}
+
+// GetDevicesByLocalpart handles /devices
+func GetDevicesByLocalpart(
+ req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
+) util.JSONResponse {
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ ctx := req.Context()
+ deviceList, err := deviceDB.GetDevicesByLocalpart(ctx, localpart)
+
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ res := devicesJSON{}
+
+ for _, dev := range deviceList {
+ res.Devices = append(res.Devices, deviceJSON{
+ DeviceID: dev.ID,
+ UserID: dev.UserID,
+ })
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: res,
+ }
+}
+
+// UpdateDeviceByID handles PUT on /devices/{deviceID}
+func UpdateDeviceByID(
+ req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
+ deviceID string,
+) util.JSONResponse {
+ if req.Method != http.MethodPut {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad Method"),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ ctx := req.Context()
+ dev, err := deviceDB.GetDeviceByID(ctx, localpart, deviceID)
+ if err == sql.ErrNoRows {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("Unknown device"),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if dev.UserID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("device not owned by current user"),
+ }
+ }
+
+ defer req.Body.Close() // nolint: errcheck
+
+ payload := deviceUpdateJSON{}
+
+ if err := json.NewDecoder(req.Body).Decode(&payload); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := deviceDB.UpdateDevice(ctx, localpart, deviceID, payload.DisplayName); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/directory.go b/clientapi/routing/directory.go
new file mode 100644
index 00000000..b23dfbfb
--- /dev/null
+++ b/clientapi/routing/directory.go
@@ -0,0 +1,183 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "fmt"
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/common/config"
+ roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrix"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// DirectoryRoom looks up a room alias
+func DirectoryRoom(
+ req *http.Request,
+ roomAlias string,
+ federation *gomatrixserverlib.FederationClient,
+ cfg *config.Dendrite,
+ rsAPI roomserverAPI.RoomserverAliasAPI,
+) util.JSONResponse {
+ _, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
+ }
+ }
+
+ if domain == cfg.Matrix.ServerName {
+ // Query the roomserver API to check if the alias exists locally
+ queryReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: roomAlias}
+ var queryRes roomserverAPI.GetRoomIDForAliasResponse
+ if err = rsAPI.GetRoomIDForAlias(req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // List any roomIDs found associated with this alias
+ if len(queryRes.RoomID) > 0 {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: queryRes,
+ }
+ }
+ } else {
+ // Query the federation for this room alias
+ resp, err := federation.LookupRoomAlias(req.Context(), domain, roomAlias)
+ if err != nil {
+ switch err.(type) {
+ case gomatrix.HTTPError:
+ default:
+ // TODO: Return 502 if the remote server errored.
+ // TODO: Return 504 if the remote server timed out.
+ return httputil.LogThenError(req, err)
+ }
+ }
+ if len(resp.RoomID) > 0 {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: resp,
+ }
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound(
+ fmt.Sprintf("Room alias %s not found", roomAlias),
+ ),
+ }
+}
+
+// SetLocalAlias implements PUT /directory/room/{roomAlias}
+// TODO: Check if the user has the power level to set an alias
+func SetLocalAlias(
+ req *http.Request,
+ device *authtypes.Device,
+ alias string,
+ cfg *config.Dendrite,
+ aliasAPI roomserverAPI.RoomserverAliasAPI,
+) util.JSONResponse {
+ _, domain, err := gomatrixserverlib.SplitID('#', alias)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
+ }
+ }
+
+ if domain != cfg.Matrix.ServerName {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Alias must be on local homeserver"),
+ }
+ }
+
+ // Check that the alias does not fall within an exclusive namespace of an
+ // application service
+ // TODO: This code should eventually be refactored with:
+ // 1. The new method for checking for things matching an AS's namespace
+ // 2. Using an overall Regex object for all AS's just like we did for usernames
+ for _, appservice := range cfg.Derived.ApplicationServices {
+ if aliasNamespaces, ok := appservice.NamespaceMap["aliases"]; ok {
+ for _, namespace := range aliasNamespaces {
+ if namespace.Exclusive && namespace.RegexpObject.MatchString(alias) {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.ASExclusive("Alias is reserved by an application service"),
+ }
+ }
+ }
+ }
+ }
+
+ var r struct {
+ RoomID string `json:"room_id"`
+ }
+ if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
+ return *resErr
+ }
+
+ queryReq := roomserverAPI.SetRoomAliasRequest{
+ UserID: device.UserID,
+ RoomID: r.RoomID,
+ Alias: alias,
+ }
+ var queryRes roomserverAPI.SetRoomAliasResponse
+ if err := aliasAPI.SetRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if queryRes.AliasExists {
+ return util.JSONResponse{
+ Code: http.StatusConflict,
+ JSON: jsonerror.Unknown("The alias " + alias + " already exists."),
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// RemoveLocalAlias implements DELETE /directory/room/{roomAlias}
+// TODO: Check if the user has the power level to remove an alias
+func RemoveLocalAlias(
+ req *http.Request,
+ device *authtypes.Device,
+ alias string,
+ aliasAPI roomserverAPI.RoomserverAliasAPI,
+) util.JSONResponse {
+ queryReq := roomserverAPI.RemoveRoomAliasRequest{
+ Alias: alias,
+ UserID: device.UserID,
+ }
+ var queryRes roomserverAPI.RemoveRoomAliasResponse
+ if err := aliasAPI.RemoveRoomAlias(req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/filter.go b/clientapi/routing/filter.go
new file mode 100644
index 00000000..109c55da
--- /dev/null
+++ b/clientapi/routing/filter.go
@@ -0,0 +1,123 @@
+// Copyright 2017 Jan Christian Grünhage
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "encoding/json"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/gomatrix"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// GetFilter implements GET /_matrix/client/r0/user/{userId}/filter/{filterId}
+func GetFilter(
+ req *http.Request, device *authtypes.Device, accountDB *accounts.Database, userID string, filterID string,
+) util.JSONResponse {
+ if req.Method != http.MethodGet {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+ }
+ if userID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot get filters for other users"),
+ }
+ }
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ res, err := accountDB.GetFilter(req.Context(), localpart, filterID)
+ if err != nil {
+ //TODO better error handling. This error message is *probably* right,
+ // but if there are obscure db errors, this will also be returned,
+ // even though it is not correct.
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.NotFound("No such filter"),
+ }
+ }
+ filter := gomatrix.Filter{}
+ err = json.Unmarshal(res, &filter)
+ if err != nil {
+ httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: filter,
+ }
+}
+
+type filterResponse struct {
+ FilterID string `json:"filter_id"`
+}
+
+//PutFilter implements POST /_matrix/client/r0/user/{userId}/filter
+func PutFilter(
+ req *http.Request, device *authtypes.Device, accountDB *accounts.Database, userID string,
+) util.JSONResponse {
+ if req.Method != http.MethodPost {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+ }
+ if userID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot create filters for other users"),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ var filter gomatrix.Filter
+
+ if reqErr := httputil.UnmarshalJSONRequest(req, &filter); reqErr != nil {
+ return *reqErr
+ }
+
+ filterArray, err := json.Marshal(filter)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Filter is malformed"),
+ }
+ }
+
+ filterID, err := accountDB.PutFilter(req.Context(), localpart, filterArray)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: filterResponse{FilterID: filterID},
+ }
+}
diff --git a/clientapi/routing/joinroom.go b/clientapi/routing/joinroom.go
new file mode 100644
index 00000000..c98688de
--- /dev/null
+++ b/clientapi/routing/joinroom.go
@@ -0,0 +1,333 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrix"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// JoinRoomByIDOrAlias implements the "/join/{roomIDOrAlias}" API.
+// https://matrix.org/docs/spec/client_server/r0.2.0.html#post-matrix-client-r0-join-roomidoralias
+func JoinRoomByIDOrAlias(
+ req *http.Request,
+ device *authtypes.Device,
+ roomIDOrAlias string,
+ cfg config.Dendrite,
+ federation *gomatrixserverlib.FederationClient,
+ producer *producers.RoomserverProducer,
+ queryAPI roomserverAPI.RoomserverQueryAPI,
+ aliasAPI roomserverAPI.RoomserverAliasAPI,
+ keyRing gomatrixserverlib.KeyRing,
+ accountDB *accounts.Database,
+) util.JSONResponse {
+ var content map[string]interface{} // must be a JSON object
+ if resErr := httputil.UnmarshalJSONRequest(req, &content); resErr != nil {
+ return *resErr
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ profile, err := accountDB.GetProfileByLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ content["membership"] = "join"
+ content["displayname"] = profile.DisplayName
+ content["avatar_url"] = profile.AvatarURL
+
+ r := joinRoomReq{
+ req, evTime, content, device.UserID, cfg, federation, producer, queryAPI, aliasAPI, keyRing,
+ }
+
+ if strings.HasPrefix(roomIDOrAlias, "!") {
+ return r.joinRoomByID(roomIDOrAlias)
+ }
+ if strings.HasPrefix(roomIDOrAlias, "#") {
+ return r.joinRoomByAlias(roomIDOrAlias)
+ }
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Invalid first character for room ID or alias"),
+ }
+}
+
+type joinRoomReq struct {
+ req *http.Request
+ evTime time.Time
+ content map[string]interface{}
+ userID string
+ cfg config.Dendrite
+ federation *gomatrixserverlib.FederationClient
+ producer *producers.RoomserverProducer
+ queryAPI roomserverAPI.RoomserverQueryAPI
+ aliasAPI roomserverAPI.RoomserverAliasAPI
+ keyRing gomatrixserverlib.KeyRing
+}
+
+// joinRoomByID joins a room by room ID
+func (r joinRoomReq) joinRoomByID(roomID string) util.JSONResponse {
+ // A client should only join a room by room ID when it has an invite
+ // to the room. If the server is already in the room then we can
+ // lookup the invite and process the request as a normal state event.
+ // If the server is not in the room the we will need to look up the
+ // remote server the invite came from in order to request a join event
+ // from that server.
+ queryReq := roomserverAPI.QueryInvitesForUserRequest{
+ RoomID: roomID, TargetUserID: r.userID,
+ }
+ var queryRes roomserverAPI.QueryInvitesForUserResponse
+ if err := r.queryAPI.QueryInvitesForUser(r.req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+
+ servers := []gomatrixserverlib.ServerName{}
+ seenInInviterIDs := map[gomatrixserverlib.ServerName]bool{}
+ for _, userID := range queryRes.InviteSenderUserIDs {
+ _, domain, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+ if !seenInInviterIDs[domain] {
+ servers = append(servers, domain)
+ seenInInviterIDs[domain] = true
+ }
+ }
+
+ // Also add the domain extracted from the roomID as a last resort to join
+ // in case the client is erroneously trying to join by ID without an invite
+ // or all previous attempts at domains extracted from the inviter IDs fail
+ // Note: It's no guarantee we'll succeed because a room isn't bound to the domain in its ID
+ _, domain, err := gomatrixserverlib.SplitID('!', roomID)
+ if err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+ if domain != r.cfg.Matrix.ServerName && !seenInInviterIDs[domain] {
+ servers = append(servers, domain)
+ }
+
+ return r.joinRoomUsingServers(roomID, servers)
+
+}
+
+// joinRoomByAlias joins a room using a room alias.
+func (r joinRoomReq) joinRoomByAlias(roomAlias string) util.JSONResponse {
+ _, domain, err := gomatrixserverlib.SplitID('#', roomAlias)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Room alias must be in the form '#localpart:domain'"),
+ }
+ }
+ if domain == r.cfg.Matrix.ServerName {
+ queryReq := roomserverAPI.GetRoomIDForAliasRequest{Alias: roomAlias}
+ var queryRes roomserverAPI.GetRoomIDForAliasResponse
+ if err = r.aliasAPI.GetRoomIDForAlias(r.req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+
+ if len(queryRes.RoomID) > 0 {
+ return r.joinRoomUsingServers(queryRes.RoomID, []gomatrixserverlib.ServerName{r.cfg.Matrix.ServerName})
+ }
+ // If the response doesn't contain a non-empty string, return an error
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("Room alias " + roomAlias + " not found."),
+ }
+ }
+ // If the room isn't local, use federation to join
+ return r.joinRoomByRemoteAlias(domain, roomAlias)
+}
+
+func (r joinRoomReq) joinRoomByRemoteAlias(
+ domain gomatrixserverlib.ServerName, roomAlias string,
+) util.JSONResponse {
+ resp, err := r.federation.LookupRoomAlias(r.req.Context(), domain, roomAlias)
+ if err != nil {
+ switch x := err.(type) {
+ case gomatrix.HTTPError:
+ if x.Code == http.StatusNotFound {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("Room alias not found"),
+ }
+ }
+ }
+ return httputil.LogThenError(r.req, err)
+ }
+
+ return r.joinRoomUsingServers(resp.RoomID, resp.Servers)
+}
+
+func (r joinRoomReq) writeToBuilder(eb *gomatrixserverlib.EventBuilder, roomID string) error {
+ eb.Type = "m.room.member"
+
+ err := eb.SetContent(r.content)
+ if err != nil {
+ return err
+ }
+
+ err = eb.SetUnsigned(struct{}{})
+ if err != nil {
+ return err
+ }
+
+ eb.Sender = r.userID
+ eb.StateKey = &r.userID
+ eb.RoomID = roomID
+ eb.Redacts = ""
+
+ return nil
+}
+
+func (r joinRoomReq) joinRoomUsingServers(
+ roomID string, servers []gomatrixserverlib.ServerName,
+) util.JSONResponse {
+ var eb gomatrixserverlib.EventBuilder
+ err := r.writeToBuilder(&eb, roomID)
+ if err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+
+ var queryRes roomserverAPI.QueryLatestEventsAndStateResponse
+ event, err := common.BuildEvent(r.req.Context(), &eb, r.cfg, r.evTime, r.queryAPI, &queryRes)
+ if err == nil {
+ if _, err = r.producer.SendEvents(r.req.Context(), []gomatrixserverlib.Event{*event}, r.cfg.Matrix.ServerName, nil); err != nil {
+ return httputil.LogThenError(r.req, err)
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct {
+ RoomID string `json:"room_id"`
+ }{roomID},
+ }
+ }
+ if err != common.ErrRoomNoExists {
+ return httputil.LogThenError(r.req, err)
+ }
+
+ if len(servers) == 0 {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("No candidate servers found for room"),
+ }
+ }
+
+ var lastErr error
+ for _, server := range servers {
+ var response *util.JSONResponse
+ response, lastErr = r.joinRoomUsingServer(roomID, server)
+ if lastErr != nil {
+ // There was a problem talking to one of the servers.
+ util.GetLogger(r.req.Context()).WithError(lastErr).WithField("server", server).Warn("Failed to join room using server")
+ // Try the next server.
+ continue
+ }
+ return *response
+ }
+
+ // Every server we tried to join through resulted in an error.
+ // We return the error from the last server.
+
+ // TODO: Generate the correct HTTP status code for all different
+ // kinds of errors that could have happened.
+ // The possible errors include:
+ // 1) We can't connect to the remote servers.
+ // 2) None of the servers we could connect to think we are allowed
+ // to join the room.
+ // 3) The remote server returned something invalid.
+ // 4) We couldn't fetch the public keys needed to verify the
+ // signatures on the state events.
+ // 5) ...
+ return httputil.LogThenError(r.req, lastErr)
+}
+
+// joinRoomUsingServer tries to join a remote room using a given matrix server.
+// If there was a failure communicating with the server or the response from the
+// server was invalid this returns an error.
+// Otherwise this returns a JSONResponse.
+func (r joinRoomReq) joinRoomUsingServer(roomID string, server gomatrixserverlib.ServerName) (*util.JSONResponse, error) {
+ respMakeJoin, err := r.federation.MakeJoin(r.req.Context(), server, roomID, r.userID)
+ if err != nil {
+ // TODO: Check if the user was not allowed to join the room.
+ return nil, err
+ }
+
+ // 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"
+ err = r.writeToBuilder(&respMakeJoin.JoinEvent, roomID)
+ if err != nil {
+ return nil, err
+ }
+
+ eventID := fmt.Sprintf("$%s:%s", util.RandomString(16), r.cfg.Matrix.ServerName)
+ event, err := respMakeJoin.JoinEvent.Build(
+ eventID, r.evTime, r.cfg.Matrix.ServerName, r.cfg.Matrix.KeyID, r.cfg.Matrix.PrivateKey,
+ )
+ if err != nil {
+ res := httputil.LogThenError(r.req, err)
+ return &res, nil
+ }
+
+ respSendJoin, err := r.federation.SendJoin(r.req.Context(), server, event)
+ if err != nil {
+ return nil, err
+ }
+
+ if err = respSendJoin.Check(r.req.Context(), r.keyRing, event); err != nil {
+ return nil, err
+ }
+
+ if err = r.producer.SendEventWithState(
+ r.req.Context(), gomatrixserverlib.RespState(respSendJoin), event,
+ ); err != nil {
+ res := httputil.LogThenError(r.req, err)
+ return &res, nil
+ }
+
+ return &util.JSONResponse{
+ Code: http.StatusOK,
+ // TODO: Put the response struct somewhere common.
+ JSON: struct {
+ RoomID string `json:"room_id"`
+ }{roomID},
+ }, nil
+}
diff --git a/clientapi/routing/login.go b/clientapi/routing/login.go
new file mode 100644
index 00000000..cb221880
--- /dev/null
+++ b/clientapi/routing/login.go
@@ -0,0 +1,152 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "context"
+ "database/sql"
+ "github.com/matrix-org/dendrite/clientapi/auth"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/userutil"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+type loginFlows struct {
+ Flows []flow `json:"flows"`
+}
+
+type flow struct {
+ Type string `json:"type"`
+ Stages []string `json:"stages"`
+}
+
+type passwordRequest struct {
+ User string `json:"user"`
+ Password string `json:"password"`
+ InitialDisplayName *string `json:"initial_device_display_name"`
+ DeviceID string `json:"device_id"`
+}
+
+type loginResponse struct {
+ UserID string `json:"user_id"`
+ AccessToken string `json:"access_token"`
+ HomeServer gomatrixserverlib.ServerName `json:"home_server"`
+ DeviceID string `json:"device_id"`
+}
+
+func passwordLogin() loginFlows {
+ f := loginFlows{}
+ s := flow{"m.login.password", []string{"m.login.password"}}
+ f.Flows = append(f.Flows, s)
+ return f
+}
+
+// Login implements GET and POST /login
+func Login(
+ req *http.Request, accountDB *accounts.Database, deviceDB *devices.Database,
+ cfg config.Dendrite,
+) util.JSONResponse {
+ if req.Method == http.MethodGet { // TODO: support other forms of login other than password, depending on config options
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: passwordLogin(),
+ }
+ } else if req.Method == http.MethodPost {
+ var r passwordRequest
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return *resErr
+ }
+ if r.User == "" {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("'user' must be supplied."),
+ }
+ }
+
+ util.GetLogger(req.Context()).WithField("user", r.User).Info("Processing login request")
+
+ localpart, err := userutil.ParseUsernameParam(r.User, &cfg.Matrix.ServerName)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidUsername(err.Error()),
+ }
+ }
+
+ acc, err := accountDB.GetAccountByPassword(req.Context(), localpart, r.Password)
+ if err != nil {
+ // Technically we could tell them if the user does not exist by checking if err == sql.ErrNoRows
+ // but that would leak the existence of the user.
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("username or password was incorrect, or the account does not exist"),
+ }
+ }
+
+ token, err := auth.GenerateAccessToken()
+ if err != nil {
+ httputil.LogThenError(req, err)
+ }
+
+ dev, err := getDevice(req.Context(), r, deviceDB, acc, localpart, token)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: loginResponse{
+ UserID: dev.UserID,
+ AccessToken: dev.AccessToken,
+ HomeServer: cfg.Matrix.ServerName,
+ DeviceID: dev.ID,
+ },
+ }
+ }
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+}
+
+// check if device exists else create one
+func getDevice(
+ ctx context.Context,
+ r passwordRequest,
+ deviceDB *devices.Database,
+ acc *authtypes.Account,
+ localpart, token string,
+) (dev *authtypes.Device, err error) {
+ dev, err = deviceDB.GetDeviceByID(ctx, localpart, r.DeviceID)
+ if err == sql.ErrNoRows {
+ // device doesn't exist, create one
+ dev, err = deviceDB.CreateDevice(
+ ctx, acc.Localpart, nil, token, r.InitialDisplayName,
+ )
+ }
+ return
+}
diff --git a/clientapi/routing/logout.go b/clientapi/routing/logout.go
new file mode 100644
index 00000000..d2013853
--- /dev/null
+++ b/clientapi/routing/logout.go
@@ -0,0 +1,71 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// Logout handles POST /logout
+func Logout(
+ req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
+) util.JSONResponse {
+ if req.Method != http.MethodPost {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := deviceDB.RemoveDevice(req.Context(), device.ID, localpart); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// LogoutAll handles POST /logout/all
+func LogoutAll(
+ req *http.Request, deviceDB *devices.Database, device *authtypes.Device,
+) util.JSONResponse {
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := deviceDB.RemoveAllDevices(req.Context(), localpart); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/membership.go b/clientapi/routing/membership.go
new file mode 100644
index 00000000..b308de79
--- /dev/null
+++ b/clientapi/routing/membership.go
@@ -0,0 +1,217 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "context"
+ "errors"
+ "net/http"
+ "time"
+
+ appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/clientapi/threepid"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrixserverlib"
+
+ "github.com/matrix-org/util"
+)
+
+var errMissingUserID = errors.New("'user_id' must be supplied")
+
+// SendMembership implements PUT /rooms/{roomID}/(join|kick|ban|unban|leave|invite)
+// by building a m.room.member event then sending it to the room server
+func SendMembership(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+ roomID string, membership string, cfg config.Dendrite,
+ queryAPI roomserverAPI.RoomserverQueryAPI, asAPI appserviceAPI.AppServiceQueryAPI,
+ producer *producers.RoomserverProducer,
+) util.JSONResponse {
+ var body threepid.MembershipRequest
+ if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
+ return *reqErr
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+
+ inviteStored, err := threepid.CheckAndProcessInvite(
+ req.Context(), device, &body, cfg, queryAPI, accountDB, producer,
+ membership, roomID, evTime,
+ )
+ if err == threepid.ErrMissingParameter {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(err.Error()),
+ }
+ } else if err == threepid.ErrNotTrusted {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.NotTrusted(body.IDServer),
+ }
+ } else if err == common.ErrRoomNoExists {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound(err.Error()),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // If an invite has been stored on an identity server, it means that a
+ // m.room.third_party_invite event has been emitted and that we shouldn't
+ // emit a m.room.member one.
+ if inviteStored {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+
+ event, err := buildMembershipEvent(
+ req.Context(), body, accountDB, device, membership, roomID, cfg, evTime, queryAPI, asAPI,
+ )
+ if err == errMissingUserID {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(err.Error()),
+ }
+ } else if err == common.ErrRoomNoExists {
+ return util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound(err.Error()),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if _, err := producer.SendEvents(
+ req.Context(), []gomatrixserverlib.Event{*event}, cfg.Matrix.ServerName, nil,
+ ); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+func buildMembershipEvent(
+ ctx context.Context,
+ body threepid.MembershipRequest, accountDB *accounts.Database,
+ device *authtypes.Device,
+ membership, roomID string,
+ cfg config.Dendrite, evTime time.Time,
+ queryAPI roomserverAPI.RoomserverQueryAPI, asAPI appserviceAPI.AppServiceQueryAPI,
+) (*gomatrixserverlib.Event, error) {
+ stateKey, reason, err := getMembershipStateKey(body, device, membership)
+ if err != nil {
+ return nil, err
+ }
+
+ profile, err := loadProfile(ctx, stateKey, cfg, accountDB, asAPI)
+ if err != nil {
+ return nil, err
+ }
+
+ builder := gomatrixserverlib.EventBuilder{
+ Sender: device.UserID,
+ RoomID: roomID,
+ Type: "m.room.member",
+ StateKey: &stateKey,
+ }
+
+ // "unban" or "kick" isn't a valid membership value, change it to "leave"
+ if membership == "unban" || membership == "kick" {
+ membership = "leave"
+ }
+
+ content := common.MemberContent{
+ Membership: membership,
+ DisplayName: profile.DisplayName,
+ AvatarURL: profile.AvatarURL,
+ Reason: reason,
+ }
+
+ if err = builder.SetContent(content); err != nil {
+ return nil, err
+ }
+
+ return common.BuildEvent(ctx, &builder, cfg, evTime, queryAPI, nil)
+}
+
+// loadProfile lookups the profile of a given user from the database and returns
+// it if the user is local to this server, or returns an empty profile if not.
+// Returns an error if the retrieval failed or if the first parameter isn't a
+// valid Matrix ID.
+func loadProfile(
+ ctx context.Context,
+ userID string,
+ cfg config.Dendrite,
+ accountDB *accounts.Database,
+ asAPI appserviceAPI.AppServiceQueryAPI,
+) (*authtypes.Profile, error) {
+ _, serverName, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return nil, err
+ }
+
+ var profile *authtypes.Profile
+ if serverName == cfg.Matrix.ServerName {
+ profile, err = appserviceAPI.RetreiveUserProfile(ctx, userID, asAPI, accountDB)
+ } else {
+ profile = &authtypes.Profile{}
+ }
+
+ return profile, err
+}
+
+// getMembershipStateKey extracts the target user ID of a membership change.
+// For "join" and "leave" this will be the ID of the user making the change.
+// For "ban", "unban", "kick" and "invite" the target user ID will be in the JSON request body.
+// In the latter case, if there was an issue retrieving the user ID from the request body,
+// returns a JSONResponse with a corresponding error code and message.
+func getMembershipStateKey(
+ body threepid.MembershipRequest, device *authtypes.Device, membership string,
+) (stateKey string, reason string, err error) {
+ if membership == "ban" || membership == "unban" || membership == "kick" || membership == "invite" {
+ // If we're in this case, the state key is contained in the request body,
+ // possibly along with a reason (for "kick" and "ban") so we need to parse
+ // it
+ if body.UserID == "" {
+ err = errMissingUserID
+ return
+ }
+
+ stateKey = body.UserID
+ reason = body.Reason
+ } else {
+ stateKey = device.UserID
+ }
+
+ return
+}
diff --git a/clientapi/routing/memberships.go b/clientapi/routing/memberships.go
new file mode 100644
index 00000000..5b890328
--- /dev/null
+++ b/clientapi/routing/memberships.go
@@ -0,0 +1,60 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+type response struct {
+ Chunk []gomatrixserverlib.ClientEvent `json:"chunk"`
+}
+
+// GetMemberships implements GET /rooms/{roomId}/members
+func GetMemberships(
+ req *http.Request, device *authtypes.Device, roomID string, joinedOnly bool,
+ _ config.Dendrite,
+ queryAPI api.RoomserverQueryAPI,
+) util.JSONResponse {
+ queryReq := api.QueryMembershipsForRoomRequest{
+ JoinedOnly: joinedOnly,
+ RoomID: roomID,
+ Sender: device.UserID,
+ }
+ var queryRes api.QueryMembershipsForRoomResponse
+ if err := queryAPI.QueryMembershipsForRoom(req.Context(), &queryReq, &queryRes); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if !queryRes.HasBeenInRoom {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("You aren't a member of the room and weren't previously a member of the room."),
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: response{queryRes.JoinEvents},
+ }
+}
diff --git a/clientapi/routing/profile.go b/clientapi/routing/profile.go
new file mode 100644
index 00000000..e57d16fb
--- /dev/null
+++ b/clientapi/routing/profile.go
@@ -0,0 +1,292 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "context"
+ "net/http"
+ "time"
+
+ appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrixserverlib"
+
+ "github.com/matrix-org/util"
+)
+
+// GetProfile implements GET /profile/{userID}
+func GetProfile(
+ req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI,
+) util.JSONResponse {
+ if req.Method != http.MethodGet {
+ return util.JSONResponse{
+ Code: http.StatusMethodNotAllowed,
+ JSON: jsonerror.NotFound("Bad method"),
+ }
+ }
+ profile, err := appserviceAPI.RetreiveUserProfile(req.Context(), userID, asAPI, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ res := common.ProfileResponse{
+ AvatarURL: profile.AvatarURL,
+ DisplayName: profile.DisplayName,
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: res,
+ }
+}
+
+// GetAvatarURL implements GET /profile/{userID}/avatar_url
+func GetAvatarURL(
+ req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI,
+) util.JSONResponse {
+ profile, err := appserviceAPI.RetreiveUserProfile(req.Context(), userID, asAPI, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ res := common.AvatarURL{
+ AvatarURL: profile.AvatarURL,
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: res,
+ }
+}
+
+// SetAvatarURL implements PUT /profile/{userID}/avatar_url
+func SetAvatarURL(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+ userID string, producer *producers.UserUpdateProducer, cfg *config.Dendrite,
+ rsProducer *producers.RoomserverProducer, queryAPI api.RoomserverQueryAPI,
+) util.JSONResponse {
+ if userID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("userID does not match the current user"),
+ }
+ }
+
+ changedKey := "avatar_url"
+
+ var r common.AvatarURL
+ if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
+ return *resErr
+ }
+ if r.AvatarURL == "" {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("'avatar_url' must be supplied."),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+
+ oldProfile, err := accountDB.GetProfileByLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err = accountDB.SetAvatarURL(req.Context(), localpart, r.AvatarURL); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ memberships, err := accountDB.GetMembershipsByLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ newProfile := authtypes.Profile{
+ Localpart: localpart,
+ DisplayName: oldProfile.DisplayName,
+ AvatarURL: r.AvatarURL,
+ }
+
+ events, err := buildMembershipEvents(
+ req.Context(), memberships, newProfile, userID, cfg, evTime, queryAPI,
+ )
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if _, err := rsProducer.SendEvents(req.Context(), events, cfg.Matrix.ServerName, nil); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := producer.SendUpdate(userID, changedKey, oldProfile.AvatarURL, r.AvatarURL); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// GetDisplayName implements GET /profile/{userID}/displayname
+func GetDisplayName(
+ req *http.Request, accountDB *accounts.Database, userID string, asAPI appserviceAPI.AppServiceQueryAPI,
+) util.JSONResponse {
+ profile, err := appserviceAPI.RetreiveUserProfile(req.Context(), userID, asAPI, accountDB)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+ res := common.DisplayName{
+ DisplayName: profile.DisplayName,
+ }
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: res,
+ }
+}
+
+// SetDisplayName implements PUT /profile/{userID}/displayname
+func SetDisplayName(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+ userID string, producer *producers.UserUpdateProducer, cfg *config.Dendrite,
+ rsProducer *producers.RoomserverProducer, queryAPI api.RoomserverQueryAPI,
+) util.JSONResponse {
+ if userID != device.UserID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("userID does not match the current user"),
+ }
+ }
+
+ changedKey := "displayname"
+
+ var r common.DisplayName
+ if resErr := httputil.UnmarshalJSONRequest(req, &r); resErr != nil {
+ return *resErr
+ }
+ if r.DisplayName == "" {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("'displayname' must be supplied."),
+ }
+ }
+
+ localpart, _, err := gomatrixserverlib.SplitID('@', userID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+
+ oldProfile, err := accountDB.GetProfileByLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err = accountDB.SetDisplayName(req.Context(), localpart, r.DisplayName); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ memberships, err := accountDB.GetMembershipsByLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ newProfile := authtypes.Profile{
+ Localpart: localpart,
+ DisplayName: r.DisplayName,
+ AvatarURL: oldProfile.AvatarURL,
+ }
+
+ events, err := buildMembershipEvents(
+ req.Context(), memberships, newProfile, userID, cfg, evTime, queryAPI,
+ )
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if _, err := rsProducer.SendEvents(req.Context(), events, cfg.Matrix.ServerName, nil); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err := producer.SendUpdate(userID, changedKey, oldProfile.DisplayName, r.DisplayName); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+func buildMembershipEvents(
+ ctx context.Context,
+ memberships []authtypes.Membership,
+ newProfile authtypes.Profile, userID string, cfg *config.Dendrite,
+ evTime time.Time, queryAPI api.RoomserverQueryAPI,
+) ([]gomatrixserverlib.Event, error) {
+ evs := []gomatrixserverlib.Event{}
+
+ for _, membership := range memberships {
+ builder := gomatrixserverlib.EventBuilder{
+ Sender: userID,
+ RoomID: membership.RoomID,
+ Type: "m.room.member",
+ StateKey: &userID,
+ }
+
+ content := common.MemberContent{
+ Membership: "join",
+ }
+
+ content.DisplayName = newProfile.DisplayName
+ content.AvatarURL = newProfile.AvatarURL
+
+ if err := builder.SetContent(content); err != nil {
+ return nil, err
+ }
+
+ event, err := common.BuildEvent(ctx, &builder, *cfg, evTime, queryAPI, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ evs = append(evs, *event)
+ }
+
+ return evs, nil
+}
diff --git a/clientapi/routing/register.go b/clientapi/routing/register.go
new file mode 100644
index 00000000..b1522e82
--- /dev/null
+++ b/clientapi/routing/register.go
@@ -0,0 +1,958 @@
+// Copyright 2017 Vector Creations Ltd
+// Copyright 2017 New Vector Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "context"
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "regexp"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/matrix-org/dendrite/common/config"
+
+ "github.com/matrix-org/dendrite/clientapi/auth"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/userutil"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+ "github.com/prometheus/client_golang/prometheus"
+ log "github.com/sirupsen/logrus"
+)
+
+var (
+ // Prometheus metrics
+ amtRegUsers = prometheus.NewCounter(
+ prometheus.CounterOpts{
+ Name: "dendrite_clientapi_reg_users_total",
+ Help: "Total number of registered users",
+ },
+ )
+)
+
+const (
+ minPasswordLength = 8 // http://matrix.org/docs/spec/client_server/r0.2.0.html#password-based
+ maxPasswordLength = 512 // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
+ maxUsernameLength = 254 // http://matrix.org/speculator/spec/HEAD/intro.html#user-identifiers TODO account for domain
+ sessionIDLength = 24
+)
+
+func init() {
+ // Register prometheus metrics. They must be registered to be exposed.
+ prometheus.MustRegister(amtRegUsers)
+}
+
+// sessionsDict keeps track of completed auth stages for each session.
+type sessionsDict struct {
+ sessions map[string][]authtypes.LoginType
+}
+
+// GetCompletedStages returns the completed stages for a session.
+func (d sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType {
+ if completedStages, ok := d.sessions[sessionID]; ok {
+ return completedStages
+ }
+ // Ensure that a empty slice is returned and not nil. See #399.
+ return make([]authtypes.LoginType, 0)
+}
+
+// AddCompletedStage records that a session has completed an auth stage.
+func (d *sessionsDict) AddCompletedStage(sessionID string, stage authtypes.LoginType) {
+ d.sessions[sessionID] = append(d.GetCompletedStages(sessionID), stage)
+}
+
+func newSessionsDict() *sessionsDict {
+ return &sessionsDict{
+ sessions: make(map[string][]authtypes.LoginType),
+ }
+}
+
+var (
+ // TODO: Remove old sessions. Need to do so on a session-specific timeout.
+ // sessions stores the completed flow stages for all sessions. Referenced using their sessionID.
+ sessions = newSessionsDict()
+ validUsernameRegex = regexp.MustCompile(`^[0-9a-z_\-./]+$`)
+)
+
+// registerRequest represents the submitted registration request.
+// It can be broken down into 2 sections: the auth dictionary and registration parameters.
+// Registration parameters vary depending on the request, and will need to remembered across
+// sessions. If no parameters are supplied, the server should use the parameters previously
+// remembered. If ANY parameters are supplied, the server should REPLACE all knowledge of
+// previous parameters with the ones supplied. This mean you cannot "build up" request params.
+type registerRequest struct {
+ // registration parameters
+ Password string `json:"password"`
+ Username string `json:"username"`
+ Admin bool `json:"admin"`
+ // user-interactive auth params
+ Auth authDict `json:"auth"`
+
+ InitialDisplayName *string `json:"initial_device_display_name"`
+
+ // Prevent this user from logging in
+ InhibitLogin common.WeakBoolean `json:"inhibit_login"`
+
+ // Application Services place Type in the root of their registration
+ // request, whereas clients place it in the authDict struct.
+ Type authtypes.LoginType `json:"type"`
+}
+
+type authDict struct {
+ Type authtypes.LoginType `json:"type"`
+ Session string `json:"session"`
+ Mac gomatrixserverlib.HexString `json:"mac"`
+
+ // Recaptcha
+ Response string `json:"response"`
+ // TODO: Lots of custom keys depending on the type
+}
+
+// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#user-interactive-authentication-api
+type userInteractiveResponse struct {
+ Flows []authtypes.Flow `json:"flows"`
+ Completed []authtypes.LoginType `json:"completed"`
+ Params map[string]interface{} `json:"params"`
+ Session string `json:"session"`
+}
+
+// legacyRegisterRequest represents the submitted registration request for v1 API.
+type legacyRegisterRequest struct {
+ Password string `json:"password"`
+ Username string `json:"user"`
+ Admin bool `json:"admin"`
+ Type authtypes.LoginType `json:"type"`
+ Mac gomatrixserverlib.HexString `json:"mac"`
+}
+
+// newUserInteractiveResponse will return a struct to be sent back to the client
+// during registration.
+func newUserInteractiveResponse(
+ sessionID string,
+ fs []authtypes.Flow,
+ params map[string]interface{},
+) userInteractiveResponse {
+ return userInteractiveResponse{
+ fs, sessions.GetCompletedStages(sessionID), params, sessionID,
+ }
+}
+
+// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
+type registerResponse struct {
+ UserID string `json:"user_id"`
+ AccessToken string `json:"access_token,omitempty"`
+ HomeServer gomatrixserverlib.ServerName `json:"home_server"`
+ DeviceID string `json:"device_id,omitempty"`
+}
+
+// recaptchaResponse represents the HTTP response from a Google Recaptcha server
+type recaptchaResponse struct {
+ Success bool `json:"success"`
+ ChallengeTS time.Time `json:"challenge_ts"`
+ Hostname string `json:"hostname"`
+ ErrorCodes []int `json:"error-codes"`
+}
+
+// validateUsername returns an error response if the username is invalid
+func validateUsername(username string) *util.JSONResponse {
+ // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
+ if len(username) > maxUsernameLength {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)),
+ }
+ } else if !validUsernameRegex.MatchString(username) {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./'"),
+ }
+ } else if username[0] == '_' { // Regex checks its not a zero length string
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidUsername("Username cannot start with a '_'"),
+ }
+ }
+ return nil
+}
+
+// validateApplicationServiceUsername returns an error response if the username is invalid for an application service
+func validateApplicationServiceUsername(username string) *util.JSONResponse {
+ if len(username) > maxUsernameLength {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(fmt.Sprintf("'username' >%d characters", maxUsernameLength)),
+ }
+ } else if !validUsernameRegex.MatchString(username) {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidUsername("Username can only contain characters a-z, 0-9, or '_-./'"),
+ }
+ }
+ return nil
+}
+
+// validatePassword returns an error response if the password is invalid
+func validatePassword(password string) *util.JSONResponse {
+ // https://github.com/matrix-org/synapse/blob/v0.20.0/synapse/rest/client/v2_alpha/register.py#L161
+ if len(password) > maxPasswordLength {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON(fmt.Sprintf("'password' >%d characters", maxPasswordLength)),
+ }
+ } else if len(password) > 0 && len(password) < minPasswordLength {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.WeakPassword(fmt.Sprintf("password too weak: min %d chars", minPasswordLength)),
+ }
+ }
+ return nil
+}
+
+// validateRecaptcha returns an error response if the captcha response is invalid
+func validateRecaptcha(
+ cfg *config.Dendrite,
+ response string,
+ clientip string,
+) *util.JSONResponse {
+ if !cfg.Matrix.RecaptchaEnabled {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Captcha registration is disabled"),
+ }
+ }
+
+ if response == "" {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("Captcha response is required"),
+ }
+ }
+
+ // Make a POST request to Google's API to check the captcha response
+ resp, err := http.PostForm(cfg.Matrix.RecaptchaSiteVerifyAPI,
+ url.Values{
+ "secret": {cfg.Matrix.RecaptchaPrivateKey},
+ "response": {response},
+ "remoteip": {clientip},
+ },
+ )
+
+ if err != nil {
+ return &util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.BadJSON("Error in requesting validation of captcha response"),
+ }
+ }
+
+ // Close the request once we're finishing reading from it
+ defer resp.Body.Close() // nolint: errcheck
+
+ // Grab the body of the response from the captcha server
+ var r recaptchaResponse
+ body, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return &util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.BadJSON("Error in contacting captcha server" + err.Error()),
+ }
+ }
+ err = json.Unmarshal(body, &r)
+ if err != nil {
+ return &util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.BadJSON("Error in unmarshaling captcha server's response: " + err.Error()),
+ }
+ }
+
+ // Check that we received a "success"
+ if !r.Success {
+ return &util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: jsonerror.BadJSON("Invalid captcha response. Please try again."),
+ }
+ }
+ return nil
+}
+
+// UserIDIsWithinApplicationServiceNamespace checks to see if a given userID
+// falls within any of the namespaces of a given Application Service. If no
+// Application Service is given, it will check to see if it matches any
+// Application Service's namespace.
+func UserIDIsWithinApplicationServiceNamespace(
+ cfg *config.Dendrite,
+ userID string,
+ appservice *config.ApplicationService,
+) bool {
+ if appservice != nil {
+ // Loop through given application service's namespaces and see if any match
+ for _, namespace := range appservice.NamespaceMap["users"] {
+ // AS namespaces are checked for validity in config
+ if namespace.RegexpObject.MatchString(userID) {
+ return true
+ }
+ }
+ return false
+ }
+
+ // Loop through all known application service's namespaces and see if any match
+ for _, knownAppService := range cfg.Derived.ApplicationServices {
+ for _, namespace := range knownAppService.NamespaceMap["users"] {
+ // AS namespaces are checked for validity in config
+ if namespace.RegexpObject.MatchString(userID) {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// UsernameMatchesMultipleExclusiveNamespaces will check if a given username matches
+// more than one exclusive namespace. More than one is not allowed
+func UsernameMatchesMultipleExclusiveNamespaces(
+ cfg *config.Dendrite,
+ username string,
+) bool {
+ userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
+
+ // Check namespaces and see if more than one match
+ matchCount := 0
+ for _, appservice := range cfg.Derived.ApplicationServices {
+ if appservice.IsInterestedInUserID(userID) {
+ if matchCount++; matchCount > 1 {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+// UsernameMatchesExclusiveNamespaces will check if a given username matches any
+// application service's exclusive users namespace
+func UsernameMatchesExclusiveNamespaces(
+ cfg *config.Dendrite,
+ username string,
+) bool {
+ userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
+ return cfg.Derived.ExclusiveApplicationServicesUsernameRegexp.MatchString(userID)
+}
+
+// validateApplicationService checks if a provided application service token
+// corresponds to one that is registered. If so, then it checks if the desired
+// username is within that application service's namespace. As long as these
+// two requirements are met, no error will be returned.
+func validateApplicationService(
+ cfg *config.Dendrite,
+ username string,
+ accessToken string,
+) (string, *util.JSONResponse) {
+ // Check if the token if the application service is valid with one we have
+ // registered in the config.
+ var matchedApplicationService *config.ApplicationService
+ for _, appservice := range cfg.Derived.ApplicationServices {
+ if appservice.ASToken == accessToken {
+ matchedApplicationService = &appservice
+ break
+ }
+ }
+ if matchedApplicationService == nil {
+ return "", &util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: jsonerror.UnknownToken("Supplied access_token does not match any known application service"),
+ }
+ }
+
+ userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
+
+ // Ensure the desired username is within at least one of the application service's namespaces.
+ if !UserIDIsWithinApplicationServiceNamespace(cfg, userID, matchedApplicationService) {
+ // If we didn't find any matches, return M_EXCLUSIVE
+ return "", &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.ASExclusive(fmt.Sprintf(
+ "Supplied username %s did not match any namespaces for application service ID: %s", username, matchedApplicationService.ID)),
+ }
+ }
+
+ // Check this user does not fit multiple application service namespaces
+ if UsernameMatchesMultipleExclusiveNamespaces(cfg, userID) {
+ return "", &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.ASExclusive(fmt.Sprintf(
+ "Supplied username %s matches multiple exclusive application service namespaces. Only 1 match allowed", username)),
+ }
+ }
+
+ // Check username application service is trying to register is valid
+ if err := validateApplicationServiceUsername(username); err != nil {
+ return "", err
+ }
+
+ // No errors, registration valid
+ return matchedApplicationService.ID, nil
+}
+
+// Register processes a /register request.
+// http://matrix.org/speculator/spec/HEAD/client_server/unstable.html#post-matrix-client-unstable-register
+func Register(
+ req *http.Request,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+ cfg *config.Dendrite,
+) util.JSONResponse {
+
+ var r registerRequest
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return *resErr
+ }
+
+ // Retrieve or generate the sessionID
+ sessionID := r.Auth.Session
+ if sessionID == "" {
+ // Generate a new, random session ID
+ sessionID = util.RandomString(sessionIDLength)
+ }
+
+ // Don't allow numeric usernames less than MAX_INT64.
+ if _, err := strconv.ParseInt(r.Username, 10, 64); err == nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidUsername("Numeric user IDs are reserved"),
+ }
+ }
+ // Auto generate a numeric username if r.Username is empty
+ if r.Username == "" {
+ id, err := accountDB.GetNewNumericLocalpart(req.Context())
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ r.Username = strconv.FormatInt(id, 10)
+ }
+
+ // Squash username to all lowercase letters
+ r.Username = strings.ToLower(r.Username)
+
+ if resErr = validateUsername(r.Username); resErr != nil {
+ return *resErr
+ }
+ if resErr = validatePassword(r.Password); resErr != nil {
+ return *resErr
+ }
+
+ // Make sure normal user isn't registering under an exclusive application
+ // service namespace. Skip this check if no app services are registered.
+ if r.Auth.Type != authtypes.LoginTypeApplicationService &&
+ len(cfg.Derived.ApplicationServices) != 0 &&
+ UsernameMatchesExclusiveNamespaces(cfg, r.Username) {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.ASExclusive("This username is reserved by an application service."),
+ }
+ }
+
+ logger := util.GetLogger(req.Context())
+ logger.WithFields(log.Fields{
+ "username": r.Username,
+ "auth.type": r.Auth.Type,
+ "session_id": r.Auth.Session,
+ }).Info("Processing registration request")
+
+ return handleRegistrationFlow(req, r, sessionID, cfg, accountDB, deviceDB)
+}
+
+// handleRegistrationFlow will direct and complete registration flow stages
+// that the client has requested.
+// nolint: gocyclo
+func handleRegistrationFlow(
+ req *http.Request,
+ r registerRequest,
+ sessionID string,
+ cfg *config.Dendrite,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+) util.JSONResponse {
+ // TODO: Shared secret registration (create new user scripts)
+ // TODO: Enable registration config flag
+ // TODO: Guest account upgrading
+
+ // TODO: Handle loading of previous session parameters from database.
+ // TODO: Handle mapping registrationRequest parameters into session parameters
+
+ // TODO: email / msisdn auth types.
+
+ if cfg.Matrix.RegistrationDisabled && r.Auth.Type != authtypes.LoginTypeSharedSecret {
+ return util.MessageResponse(http.StatusForbidden, "Registration has been disabled")
+ }
+
+ switch r.Auth.Type {
+ case authtypes.LoginTypeRecaptcha:
+ // Check given captcha response
+ resErr := validateRecaptcha(cfg, r.Auth.Response, req.RemoteAddr)
+ if resErr != nil {
+ return *resErr
+ }
+
+ // Add Recaptcha to the list of completed registration stages
+ sessions.AddCompletedStage(sessionID, authtypes.LoginTypeRecaptcha)
+
+ case authtypes.LoginTypeSharedSecret:
+ // Check shared secret against config
+ valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Auth.Mac)
+
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ } else if !valid {
+ return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
+ }
+
+ // Add SharedSecret to the list of completed registration stages
+ sessions.AddCompletedStage(sessionID, authtypes.LoginTypeSharedSecret)
+
+ case "":
+ // Extract the access token from the request, if there's one to extract
+ // (which we can know by checking whether the error is nil or not).
+ accessToken, err := auth.ExtractAccessToken(req)
+
+ // A missing auth type can mean either the registration is performed by
+ // an AS or the request is made as the first step of a registration
+ // using the User-Interactive Authentication API. This can be determined
+ // by whether the request contains an access token.
+ if err == nil {
+ return handleApplicationServiceRegistration(
+ accessToken, err, req, r, cfg, accountDB, deviceDB,
+ )
+ }
+
+ case authtypes.LoginTypeApplicationService:
+ // Extract the access token from the request.
+ accessToken, err := auth.ExtractAccessToken(req)
+ // Let the AS registration handler handle the process from here. We
+ // don't need a condition on that call since the registration is clearly
+ // stated as being AS-related.
+ return handleApplicationServiceRegistration(
+ accessToken, err, req, r, cfg, accountDB, deviceDB,
+ )
+
+ case authtypes.LoginTypeDummy:
+ // there is nothing to do
+ // Add Dummy to the list of completed registration stages
+ sessions.AddCompletedStage(sessionID, authtypes.LoginTypeDummy)
+
+ default:
+ return util.JSONResponse{
+ Code: http.StatusNotImplemented,
+ JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
+ }
+ }
+
+ // Check if the user's registration flow has been completed successfully
+ // A response with current registration flow and remaining available methods
+ // will be returned if a flow has not been successfully completed yet
+ return checkAndCompleteFlow(sessions.GetCompletedStages(sessionID),
+ req, r, sessionID, cfg, accountDB, deviceDB)
+}
+
+// handleApplicationServiceRegistration handles the registration of an
+// application service's user by validating the AS from its access token and
+// registering the user. Its two first parameters must be the two return values
+// of the auth.ExtractAccessToken function.
+// Returns an error if the access token couldn't be extracted from the request
+// at an earlier step of the registration workflow, or if the provided access
+// token doesn't belong to a valid AS, or if there was an issue completing the
+// registration process.
+func handleApplicationServiceRegistration(
+ accessToken string,
+ tokenErr error,
+ req *http.Request,
+ r registerRequest,
+ cfg *config.Dendrite,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+) util.JSONResponse {
+ // Check if we previously had issues extracting the access token from the
+ // request.
+ if tokenErr != nil {
+ return util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: jsonerror.MissingToken(tokenErr.Error()),
+ }
+ }
+
+ // Check application service register user request is valid.
+ // The application service's ID is returned if so.
+ appserviceID, err := validateApplicationService(
+ cfg, r.Username, accessToken,
+ )
+ if err != nil {
+ return *err
+ }
+
+ // If no error, application service was successfully validated.
+ // Don't need to worry about appending to registration stages as
+ // application service registration is entirely separate.
+ return completeRegistration(
+ req.Context(), accountDB, deviceDB, r.Username, "", appserviceID,
+ r.InhibitLogin, r.InitialDisplayName,
+ )
+}
+
+// checkAndCompleteFlow checks if a given registration flow is completed given
+// a set of allowed flows. If so, registration is completed, otherwise a
+// response with
+func checkAndCompleteFlow(
+ flow []authtypes.LoginType,
+ req *http.Request,
+ r registerRequest,
+ sessionID string,
+ cfg *config.Dendrite,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+) util.JSONResponse {
+ if checkFlowCompleted(flow, cfg.Derived.Registration.Flows) {
+ // This flow was completed, registration can continue
+ return completeRegistration(
+ req.Context(), accountDB, deviceDB, r.Username, r.Password, "",
+ r.InhibitLogin, r.InitialDisplayName,
+ )
+ }
+
+ // There are still more stages to complete.
+ // Return the flows and those that have been completed.
+ return util.JSONResponse{
+ Code: http.StatusUnauthorized,
+ JSON: newUserInteractiveResponse(sessionID,
+ cfg.Derived.Registration.Flows, cfg.Derived.Registration.Params),
+ }
+}
+
+// LegacyRegister process register requests from the legacy v1 API
+func LegacyRegister(
+ req *http.Request,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+ cfg *config.Dendrite,
+) util.JSONResponse {
+ var r legacyRegisterRequest
+ resErr := parseAndValidateLegacyLogin(req, &r)
+ if resErr != nil {
+ return *resErr
+ }
+
+ logger := util.GetLogger(req.Context())
+ logger.WithFields(log.Fields{
+ "username": r.Username,
+ "auth.type": r.Type,
+ }).Info("Processing registration request")
+
+ if cfg.Matrix.RegistrationDisabled && r.Type != authtypes.LoginTypeSharedSecret {
+ return util.MessageResponse(http.StatusForbidden, "Registration has been disabled")
+ }
+
+ switch r.Type {
+ case authtypes.LoginTypeSharedSecret:
+ if cfg.Matrix.RegistrationSharedSecret == "" {
+ return util.MessageResponse(http.StatusBadRequest, "Shared secret registration is disabled")
+ }
+
+ valid, err := isValidMacLogin(cfg, r.Username, r.Password, r.Admin, r.Mac)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if !valid {
+ return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
+ }
+
+ return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil)
+ case authtypes.LoginTypeDummy:
+ // there is nothing to do
+ return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil)
+ default:
+ return util.JSONResponse{
+ Code: http.StatusNotImplemented,
+ JSON: jsonerror.Unknown("unknown/unimplemented auth type"),
+ }
+ }
+}
+
+// parseAndValidateLegacyLogin parses the request into r and checks that the
+// request is valid (e.g. valid user names, etc)
+func parseAndValidateLegacyLogin(req *http.Request, r *legacyRegisterRequest) *util.JSONResponse {
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return resErr
+ }
+
+ // Squash username to all lowercase letters
+ r.Username = strings.ToLower(r.Username)
+
+ if resErr = validateUsername(r.Username); resErr != nil {
+ return resErr
+ }
+ if resErr = validatePassword(r.Password); resErr != nil {
+ return resErr
+ }
+
+ // All registration requests must specify what auth they are using to perform this request
+ if r.Type == "" {
+ return &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("invalid type"),
+ }
+ }
+
+ return nil
+}
+
+func completeRegistration(
+ ctx context.Context,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+ username, password, appserviceID string,
+ inhibitLogin common.WeakBoolean,
+ displayName *string,
+) util.JSONResponse {
+ if username == "" {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("missing username"),
+ }
+ }
+ // Blank passwords are only allowed by registered application services
+ if password == "" && appserviceID == "" {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.BadJSON("missing password"),
+ }
+ }
+
+ acc, err := accountDB.CreateAccount(ctx, username, password, appserviceID)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
+ }
+ } else if acc == nil {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.UserInUse("Desired user ID is already taken."),
+ }
+ }
+
+ // Check whether inhibit_login option is set. If so, don't create an access
+ // token or a device for this user
+ if inhibitLogin {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: registerResponse{
+ UserID: userutil.MakeUserID(username, acc.ServerName),
+ HomeServer: acc.ServerName,
+ },
+ }
+ }
+
+ token, err := auth.GenerateAccessToken()
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.Unknown("Failed to generate access token"),
+ }
+ }
+
+ // TODO: Use the device ID in the request.
+ dev, err := deviceDB.CreateDevice(ctx, username, nil, token, displayName)
+ if err != nil {
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.Unknown("failed to create device: " + err.Error()),
+ }
+ }
+
+ // Increment prometheus counter for created users
+ amtRegUsers.Inc()
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: registerResponse{
+ UserID: dev.UserID,
+ AccessToken: dev.AccessToken,
+ HomeServer: acc.ServerName,
+ DeviceID: dev.ID,
+ },
+ }
+}
+
+// Used for shared secret registration.
+// Checks if the username, password and isAdmin flag matches the given mac.
+func isValidMacLogin(
+ cfg *config.Dendrite,
+ username, password string,
+ isAdmin bool,
+ givenMac []byte,
+) (bool, error) {
+ sharedSecret := cfg.Matrix.RegistrationSharedSecret
+
+ // Check that shared secret registration isn't disabled.
+ if cfg.Matrix.RegistrationSharedSecret == "" {
+ return false, errors.New("Shared secret registration is disabled")
+ }
+
+ // Double check that username/password don't contain the HMAC delimiters. We should have
+ // already checked this.
+ if strings.Contains(username, "\x00") {
+ return false, errors.New("Username contains invalid character")
+ }
+ if strings.Contains(password, "\x00") {
+ return false, errors.New("Password contains invalid character")
+ }
+ if sharedSecret == "" {
+ return false, errors.New("Shared secret registration is disabled")
+ }
+
+ adminString := "notadmin"
+ if isAdmin {
+ adminString = "admin"
+ }
+ joined := strings.Join([]string{username, password, adminString}, "\x00")
+
+ mac := hmac.New(sha1.New, []byte(sharedSecret))
+ _, err := mac.Write([]byte(joined))
+ if err != nil {
+ return false, err
+ }
+ expectedMAC := mac.Sum(nil)
+
+ return hmac.Equal(givenMac, expectedMAC), nil
+}
+
+// checkFlows checks a single completed flow against another required one. If
+// one contains at least all of the stages that the other does, checkFlows
+// returns true.
+func checkFlows(
+ completedStages []authtypes.LoginType,
+ requiredStages []authtypes.LoginType,
+) bool {
+ // Create temporary slices so they originals will not be modified on sorting
+ completed := make([]authtypes.LoginType, len(completedStages))
+ required := make([]authtypes.LoginType, len(requiredStages))
+ copy(completed, completedStages)
+ copy(required, requiredStages)
+
+ // Sort the slices for simple comparison
+ sort.Slice(completed, func(i, j int) bool { return completed[i] < completed[j] })
+ sort.Slice(required, func(i, j int) bool { return required[i] < required[j] })
+
+ // Iterate through each slice, going to the next required slice only once
+ // we've found a match.
+ i, j := 0, 0
+ for j < len(required) {
+ // Exit if we've reached the end of our input without being able to
+ // match all of the required stages.
+ if i >= len(completed) {
+ return false
+ }
+
+ // If we've found a stage we want, move on to the next required stage.
+ if completed[i] == required[j] {
+ j++
+ }
+ i++
+ }
+ return true
+}
+
+// checkFlowCompleted checks if a registration flow complies with any allowed flow
+// dictated by the server. Order of stages does not matter. A user may complete
+// extra stages as long as the required stages of at least one flow is met.
+func checkFlowCompleted(
+ flow []authtypes.LoginType,
+ allowedFlows []authtypes.Flow,
+) bool {
+ // Iterate through possible flows to check whether any have been fully completed.
+ for _, allowedFlow := range allowedFlows {
+ if checkFlows(flow, allowedFlow.Stages) {
+ return true
+ }
+ }
+ return false
+}
+
+type availableResponse struct {
+ Available bool `json:"available"`
+}
+
+// RegisterAvailable checks if the username is already taken or invalid.
+func RegisterAvailable(
+ req *http.Request,
+ cfg config.Dendrite,
+ accountDB *accounts.Database,
+) util.JSONResponse {
+ username := req.URL.Query().Get("username")
+
+ // Squash username to all lowercase letters
+ username = strings.ToLower(username)
+
+ if err := validateUsername(username); err != nil {
+ return *err
+ }
+
+ // Check if this username is reserved by an application service
+ userID := userutil.MakeUserID(username, cfg.Matrix.ServerName)
+ for _, appservice := range cfg.Derived.ApplicationServices {
+ if appservice.IsInterestedInUserID(userID) {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.UserInUse("Desired user ID is reserved by an application service."),
+ }
+ }
+ }
+
+ availability, availabilityErr := accountDB.CheckAccountAvailability(req.Context(), username)
+ if availabilityErr != nil {
+ return util.JSONResponse{
+ Code: http.StatusInternalServerError,
+ JSON: jsonerror.Unknown("failed to check availability: " + availabilityErr.Error()),
+ }
+ }
+ if !availability {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.UserInUse("Desired User ID is already taken."),
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: availableResponse{
+ Available: true,
+ },
+ }
+}
diff --git a/clientapi/routing/register_test.go b/clientapi/routing/register_test.go
new file mode 100644
index 00000000..6fcf0bc3
--- /dev/null
+++ b/clientapi/routing/register_test.go
@@ -0,0 +1,209 @@
+// Copyright 2017 Andrew Morgan <andrew@amorgan.xyz>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "regexp"
+ "testing"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/common/config"
+)
+
+var (
+ // Registration Flows that the server allows.
+ allowedFlows = []authtypes.Flow{
+ {
+ Stages: []authtypes.LoginType{
+ authtypes.LoginType("stage1"),
+ authtypes.LoginType("stage2"),
+ },
+ },
+ {
+ Stages: []authtypes.LoginType{
+ authtypes.LoginType("stage1"),
+ authtypes.LoginType("stage3"),
+ },
+ },
+ }
+)
+
+// Should return true as we're completing all the stages of a single flow in
+// order.
+func TestFlowCheckingCompleteFlowOrdered(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage1"),
+ authtypes.LoginType("stage3"),
+ }
+
+ if !checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be true.")
+ }
+}
+
+// Should return false as all stages in a single flow need to be completed.
+func TestFlowCheckingStagesFromDifferentFlows(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage2"),
+ authtypes.LoginType("stage3"),
+ }
+
+ if checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be false.")
+ }
+}
+
+// Should return true as we're completing all the stages from a single flow, as
+// well as some extraneous stages.
+func TestFlowCheckingCompleteOrderedExtraneous(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage1"),
+ authtypes.LoginType("stage3"),
+ authtypes.LoginType("stage4"),
+ authtypes.LoginType("stage5"),
+ }
+ if !checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be true.")
+ }
+}
+
+// Should return false as we're submitting an empty flow.
+func TestFlowCheckingEmptyFlow(t *testing.T) {
+ testFlow := []authtypes.LoginType{}
+ if checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be false.")
+ }
+}
+
+// Should return false as we've completed a stage that isn't in any allowed flow.
+func TestFlowCheckingInvalidStage(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage8"),
+ }
+ if checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be false.")
+ }
+}
+
+// Should return true as we complete all stages of an allowed flow, though out
+// of order, as well as extraneous stages.
+func TestFlowCheckingExtraneousUnordered(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage5"),
+ authtypes.LoginType("stage4"),
+ authtypes.LoginType("stage3"),
+ authtypes.LoginType("stage2"),
+ authtypes.LoginType("stage1"),
+ }
+ if !checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be true.")
+ }
+}
+
+// Should return false as we're providing fewer stages than are required.
+func TestFlowCheckingShortIncorrectInput(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage8"),
+ }
+ if checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be false.")
+ }
+}
+
+// Should return false as we're providing different stages than are required.
+func TestFlowCheckingExtraneousIncorrectInput(t *testing.T) {
+ testFlow := []authtypes.LoginType{
+ authtypes.LoginType("stage8"),
+ authtypes.LoginType("stage9"),
+ authtypes.LoginType("stage10"),
+ authtypes.LoginType("stage11"),
+ }
+ if checkFlowCompleted(testFlow, allowedFlows) {
+ t.Error("Incorrect registration flow verification: ", testFlow, ", from allowed flows: ", allowedFlows, ". Should be false.")
+ }
+}
+
+// Completed flows stages should always be a valid slice header.
+// TestEmptyCompletedFlows checks that sessionsDict returns a slice & not nil.
+func TestEmptyCompletedFlows(t *testing.T) {
+ fakeEmptySessions := newSessionsDict()
+ fakeSessionID := "aRandomSessionIDWhichDoesNotExist"
+ ret := fakeEmptySessions.GetCompletedStages(fakeSessionID)
+
+ // check for []
+ if ret == nil || len(ret) != 0 {
+ t.Error("Empty Completed Flow Stages should be a empty slice: returned ", ret, ". Should be []")
+ }
+}
+
+// This method tests validation of the provided Application Service token and
+// username that they're registering
+func TestValidationOfApplicationServices(t *testing.T) {
+ // Set up application service namespaces
+ regex := "@_appservice_.*"
+ regexp, err := regexp.Compile(regex)
+ if err != nil {
+ t.Errorf("Error compiling regex: %s", regex)
+ }
+
+ fakeNamespace := config.ApplicationServiceNamespace{
+ Exclusive: true,
+ Regex: regex,
+ RegexpObject: regexp,
+ }
+
+ // Create a fake application service
+ fakeID := "FakeAS"
+ fakeSenderLocalpart := "_appservice_bot"
+ fakeApplicationService := config.ApplicationService{
+ ID: fakeID,
+ URL: "null",
+ ASToken: "1234",
+ HSToken: "4321",
+ SenderLocalpart: fakeSenderLocalpart,
+ NamespaceMap: map[string][]config.ApplicationServiceNamespace{
+ "users": {fakeNamespace},
+ },
+ }
+
+ // Set up a config
+ fakeConfig := config.Dendrite{}
+ fakeConfig.Matrix.ServerName = "localhost"
+ fakeConfig.Derived.ApplicationServices = []config.ApplicationService{fakeApplicationService}
+
+ // Access token is correct, user_id omitted so we are acting as SenderLocalpart
+ asID, resp := validateApplicationService(&fakeConfig, fakeSenderLocalpart, "1234")
+ if resp != nil || asID != fakeID {
+ t.Errorf("appservice should have validated and returned correct ID: %s", resp.JSON)
+ }
+
+ // Access token is incorrect, user_id omitted so we are acting as SenderLocalpart
+ asID, resp = validateApplicationService(&fakeConfig, fakeSenderLocalpart, "xxxx")
+ if resp == nil || asID == fakeID {
+ t.Errorf("access_token should have been marked as invalid")
+ }
+
+ // Access token is correct, acting as valid user_id
+ asID, resp = validateApplicationService(&fakeConfig, "_appservice_bob", "1234")
+ if resp != nil || asID != fakeID {
+ t.Errorf("access_token and user_id should've been valid: %s", resp.JSON)
+ }
+
+ // Access token is correct, acting as invalid user_id
+ asID, resp = validateApplicationService(&fakeConfig, "_something_else", "1234")
+ if resp == nil || asID == fakeID {
+ t.Errorf("user_id should not have been valid: @_something_else:localhost")
+ }
+}
diff --git a/clientapi/routing/routing.go b/clientapi/routing/routing.go
new file mode 100644
index 00000000..b0ced79e
--- /dev/null
+++ b/clientapi/routing/routing.go
@@ -0,0 +1,413 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "encoding/json"
+ "net/http"
+ "strings"
+
+ "github.com/gorilla/mux"
+ appserviceAPI "github.com/matrix-org/dendrite/appservice/api"
+ "github.com/matrix-org/dendrite/clientapi/auth"
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/devices"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/dendrite/common/transactions"
+ roomserverAPI "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+const pathPrefixV1 = "/_matrix/client/api/v1"
+const pathPrefixR0 = "/_matrix/client/r0"
+const pathPrefixUnstable = "/_matrix/client/unstable"
+
+// 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(
+ apiMux *mux.Router, cfg config.Dendrite,
+ producer *producers.RoomserverProducer,
+ queryAPI roomserverAPI.RoomserverQueryAPI,
+ aliasAPI roomserverAPI.RoomserverAliasAPI,
+ asAPI appserviceAPI.AppServiceQueryAPI,
+ accountDB *accounts.Database,
+ deviceDB *devices.Database,
+ federation *gomatrixserverlib.FederationClient,
+ keyRing gomatrixserverlib.KeyRing,
+ userUpdateProducer *producers.UserUpdateProducer,
+ syncProducer *producers.SyncAPIProducer,
+ typingProducer *producers.TypingServerProducer,
+ transactionsCache *transactions.Cache,
+) {
+
+ apiMux.Handle("/_matrix/client/versions",
+ common.MakeExternalAPI("versions", func(req *http.Request) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct {
+ Versions []string `json:"versions"`
+ }{[]string{
+ "r0.0.1",
+ "r0.1.0",
+ "r0.2.0",
+ "r0.3.0",
+ }},
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux := apiMux.PathPrefix(pathPrefixR0).Subrouter()
+ v1mux := apiMux.PathPrefix(pathPrefixV1).Subrouter()
+ unstableMux := apiMux.PathPrefix(pathPrefixUnstable).Subrouter()
+
+ authData := auth.Data{
+ AccountDB: accountDB,
+ DeviceDB: deviceDB,
+ AppServices: cfg.Derived.ApplicationServices,
+ }
+
+ r0mux.Handle("/createRoom",
+ common.MakeAuthAPI("createRoom", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return CreateRoom(req, device, cfg, producer, accountDB, aliasAPI, asAPI)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+ r0mux.Handle("/join/{roomIDOrAlias}",
+ common.MakeAuthAPI("join", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return JoinRoomByIDOrAlias(
+ req, device, vars["roomIDOrAlias"], cfg, federation, producer, queryAPI, aliasAPI, keyRing, accountDB,
+ )
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+ r0mux.Handle("/rooms/{roomID}/{membership:(?:join|kick|ban|unban|leave|invite)}",
+ common.MakeAuthAPI("membership", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SendMembership(req, accountDB, device, vars["roomID"], vars["membership"], cfg, queryAPI, asAPI, producer)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+ r0mux.Handle("/rooms/{roomID}/send/{eventType}",
+ common.MakeAuthAPI("send_message", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SendEvent(req, device, vars["roomID"], vars["eventType"], nil, nil, cfg, queryAPI, producer, nil)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+ r0mux.Handle("/rooms/{roomID}/send/{eventType}/{txnID}",
+ common.MakeAuthAPI("send_message", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ txnID := vars["txnID"]
+ return SendEvent(req, device, vars["roomID"], vars["eventType"], &txnID,
+ nil, cfg, queryAPI, producer, transactionsCache)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+ r0mux.Handle("/rooms/{roomID}/state/{eventType:[^/]+/?}",
+ common.MakeAuthAPI("send_message", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ emptyString := ""
+ eventType := vars["eventType"]
+ // If there's a trailing slash, remove it
+ if strings.HasSuffix(eventType, "/") {
+ eventType = eventType[:len(eventType)-1]
+ }
+ return SendEvent(req, device, vars["roomID"], eventType, nil, &emptyString, cfg, queryAPI, producer, nil)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+ r0mux.Handle("/rooms/{roomID}/state/{eventType}/{stateKey}",
+ common.MakeAuthAPI("send_message", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ stateKey := vars["stateKey"]
+ return SendEvent(req, device, vars["roomID"], vars["eventType"], nil, &stateKey, cfg, queryAPI, producer, nil)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/register", common.MakeExternalAPI("register", func(req *http.Request) util.JSONResponse {
+ return Register(req, accountDB, deviceDB, &cfg)
+ })).Methods(http.MethodPost, http.MethodOptions)
+
+ v1mux.Handle("/register", common.MakeExternalAPI("register", func(req *http.Request) util.JSONResponse {
+ return LegacyRegister(req, accountDB, deviceDB, &cfg)
+ })).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/register/available", common.MakeExternalAPI("registerAvailable", func(req *http.Request) util.JSONResponse {
+ return RegisterAvailable(req, cfg, accountDB)
+ })).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/directory/room/{roomAlias}",
+ common.MakeExternalAPI("directory_room", func(req *http.Request) util.JSONResponse {
+ vars := mux.Vars(req)
+ return DirectoryRoom(req, vars["roomAlias"], federation, &cfg, aliasAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/directory/room/{roomAlias}",
+ common.MakeAuthAPI("directory_room", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SetLocalAlias(req, device, vars["roomAlias"], &cfg, aliasAPI)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/directory/room/{roomAlias}",
+ common.MakeAuthAPI("directory_room", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return RemoveLocalAlias(req, device, vars["roomAlias"], aliasAPI)
+ }),
+ ).Methods(http.MethodDelete, http.MethodOptions)
+
+ r0mux.Handle("/logout",
+ common.MakeAuthAPI("logout", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return Logout(req, deviceDB, device)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/logout/all",
+ common.MakeAuthAPI("logout", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return LogoutAll(req, deviceDB, device)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/rooms/{roomID}/typing/{userID}",
+ common.MakeAuthAPI("rooms_typing", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SendTyping(req, device, vars["roomID"], vars["userID"], accountDB, typingProducer)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/account/whoami",
+ common.MakeAuthAPI("whoami", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return Whoami(req, device)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ // Stub endpoints required by Riot
+
+ r0mux.Handle("/login",
+ common.MakeExternalAPI("login", func(req *http.Request) util.JSONResponse {
+ return Login(req, accountDB, deviceDB, cfg)
+ }),
+ ).Methods(http.MethodGet, http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/pushrules/",
+ common.MakeExternalAPI("push_rules", func(req *http.Request) util.JSONResponse {
+ // TODO: Implement push rules API
+ res := json.RawMessage(`{
+ "global": {
+ "content": [],
+ "override": [],
+ "room": [],
+ "sender": [],
+ "underride": []
+ }
+ }`)
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: &res,
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/user/{userId}/filter",
+ common.MakeAuthAPI("put_filter", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return PutFilter(req, device, accountDB, vars["userId"])
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/user/{userId}/filter/{filterId}",
+ common.MakeAuthAPI("get_filter", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetFilter(req, device, accountDB, vars["userId"], vars["filterId"])
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ // Riot user settings
+
+ r0mux.Handle("/profile/{userID}",
+ common.MakeExternalAPI("profile", func(req *http.Request) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetProfile(req, accountDB, vars["userID"], asAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/profile/{userID}/avatar_url",
+ common.MakeExternalAPI("profile_avatar_url", func(req *http.Request) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetAvatarURL(req, accountDB, vars["userID"], asAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/profile/{userID}/avatar_url",
+ common.MakeAuthAPI("profile_avatar_url", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SetAvatarURL(req, accountDB, device, vars["userID"], userUpdateProducer, &cfg, producer, queryAPI)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+ // Browsers use the OPTIONS HTTP method to check if the CORS policy allows
+ // PUT requests, so we need to allow this method
+
+ r0mux.Handle("/profile/{userID}/displayname",
+ common.MakeExternalAPI("profile_displayname", func(req *http.Request) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetDisplayName(req, accountDB, vars["userID"], asAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/profile/{userID}/displayname",
+ common.MakeAuthAPI("profile_displayname", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SetDisplayName(req, accountDB, device, vars["userID"], userUpdateProducer, &cfg, producer, queryAPI)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+ // Browsers use the OPTIONS HTTP method to check if the CORS policy allows
+ // PUT requests, so we need to allow this method
+
+ r0mux.Handle("/account/3pid",
+ common.MakeAuthAPI("account_3pid", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return GetAssociated3PIDs(req, accountDB, device)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/account/3pid",
+ common.MakeAuthAPI("account_3pid", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return CheckAndSave3PIDAssociation(req, accountDB, device, cfg)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ unstableMux.Handle("/account/3pid/delete",
+ common.MakeAuthAPI("account_3pid", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return Forget3PID(req, accountDB)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/{path:(?:account/3pid|register)}/email/requestToken",
+ common.MakeExternalAPI("account_3pid_request_token", func(req *http.Request) util.JSONResponse {
+ return RequestEmailToken(req, accountDB, cfg)
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ // Riot logs get flooded unless this is handled
+ r0mux.Handle("/presence/{userID}/status",
+ common.MakeExternalAPI("presence", func(req *http.Request) util.JSONResponse {
+ // TODO: Set presence (probably the responsibility of a presence server not clientapi)
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/voip/turnServer",
+ common.MakeAuthAPI("turn_server", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return RequestTurnServer(req, device, cfg)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ unstableMux.Handle("/thirdparty/protocols",
+ common.MakeExternalAPI("thirdparty_protocols", func(req *http.Request) util.JSONResponse {
+ // TODO: Return the third party protcols
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/rooms/{roomID}/initialSync",
+ common.MakeExternalAPI("rooms_initial_sync", func(req *http.Request) util.JSONResponse {
+ // TODO: Allow people to peek into rooms.
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.GuestAccessForbidden("Guest access not implemented"),
+ }
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/user/{userID}/account_data/{type}",
+ common.MakeAuthAPI("user_account_data", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SaveAccountData(req, accountDB, device, vars["userID"], "", vars["type"], syncProducer)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/user/{userID}/rooms/{roomID}/account_data/{type}",
+ common.MakeAuthAPI("user_account_data", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return SaveAccountData(req, accountDB, device, vars["userID"], vars["roomID"], vars["type"], syncProducer)
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ r0mux.Handle("/rooms/{roomID}/members",
+ common.MakeAuthAPI("rooms_members", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetMemberships(req, device, vars["roomID"], false, cfg, queryAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/rooms/{roomID}/joined_members",
+ common.MakeAuthAPI("rooms_members", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetMemberships(req, device, vars["roomID"], true, cfg, queryAPI)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/rooms/{roomID}/read_markers",
+ common.MakeExternalAPI("rooms_read_markers", func(req *http.Request) util.JSONResponse {
+ // TODO: return the read_markers.
+ return util.JSONResponse{Code: http.StatusOK, JSON: struct{}{}}
+ }),
+ ).Methods(http.MethodPost, http.MethodOptions)
+
+ r0mux.Handle("/devices",
+ common.MakeAuthAPI("get_devices", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return GetDevicesByLocalpart(req, deviceDB, device)
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/devices/{deviceID}",
+ common.MakeAuthAPI("get_device", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return GetDeviceByID(req, deviceDB, device, vars["deviceID"])
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/devices/{deviceID}",
+ common.MakeAuthAPI("device_data", authData, func(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ vars := mux.Vars(req)
+ return UpdateDeviceByID(req, deviceDB, device, vars["deviceID"])
+ }),
+ ).Methods(http.MethodPut, http.MethodOptions)
+
+ // Stub implementations for sytest
+ r0mux.Handle("/events",
+ common.MakeExternalAPI("events", func(req *http.Request) util.JSONResponse {
+ return util.JSONResponse{Code: http.StatusOK, JSON: map[string]interface{}{
+ "chunk": []interface{}{},
+ "start": "",
+ "end": "",
+ }}
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+
+ r0mux.Handle("/initialSync",
+ common.MakeExternalAPI("initial_sync", func(req *http.Request) util.JSONResponse {
+ return util.JSONResponse{Code: http.StatusOK, JSON: map[string]interface{}{
+ "end": "",
+ }}
+ }),
+ ).Methods(http.MethodGet, http.MethodOptions)
+}
diff --git a/clientapi/routing/sendevent.go b/clientapi/routing/sendevent.go
new file mode 100644
index 00000000..e916e451
--- /dev/null
+++ b/clientapi/routing/sendevent.go
@@ -0,0 +1,153 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/common"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/dendrite/common/transactions"
+ "github.com/matrix-org/dendrite/roomserver/api"
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+// http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-send-eventtype-txnid
+// http://matrix.org/docs/spec/client_server/r0.2.0.html#put-matrix-client-r0-rooms-roomid-state-eventtype-statekey
+type sendEventResponse struct {
+ EventID string `json:"event_id"`
+}
+
+// SendEvent implements:
+// /rooms/{roomID}/send/{eventType}
+// /rooms/{roomID}/send/{eventType}/{txnID}
+// /rooms/{roomID}/state/{eventType}/{stateKey}
+func SendEvent(
+ req *http.Request,
+ device *authtypes.Device,
+ roomID, eventType string, txnID, stateKey *string,
+ cfg config.Dendrite,
+ queryAPI api.RoomserverQueryAPI,
+ producer *producers.RoomserverProducer,
+ txnCache *transactions.Cache,
+) util.JSONResponse {
+ if txnID != nil {
+ // Try to fetch response from transactionsCache
+ if res, ok := txnCache.FetchTransaction(*txnID); ok {
+ return *res
+ }
+ }
+
+ e, resErr := generateSendEvent(req, device, roomID, eventType, stateKey, cfg, queryAPI)
+ if resErr != nil {
+ return *resErr
+ }
+
+ var txnAndDeviceID *api.TransactionID
+ if txnID != nil {
+ txnAndDeviceID = &api.TransactionID{
+ TransactionID: *txnID,
+ DeviceID: device.ID,
+ }
+ }
+
+ // pass the new event to the roomserver and receive the correct event ID
+ // event ID in case of duplicate transaction is discarded
+ eventID, err := producer.SendEvents(
+ req.Context(), []gomatrixserverlib.Event{*e}, cfg.Matrix.ServerName, txnAndDeviceID,
+ )
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ res := util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: sendEventResponse{eventID},
+ }
+ // Add response to transactionsCache
+ if txnID != nil {
+ txnCache.AddTransaction(*txnID, &res)
+ }
+
+ return res
+}
+
+func generateSendEvent(
+ req *http.Request,
+ device *authtypes.Device,
+ roomID, eventType string, stateKey *string,
+ cfg config.Dendrite,
+ queryAPI api.RoomserverQueryAPI,
+) (*gomatrixserverlib.Event, *util.JSONResponse) {
+ // parse the incoming http request
+ userID := device.UserID
+ var r map[string]interface{} // must be a JSON object
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return nil, resErr
+ }
+
+ evTime, err := httputil.ParseTSParam(req)
+ if err != nil {
+ return nil, &util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.InvalidArgumentValue(err.Error()),
+ }
+ }
+
+ // create the new event and set all the fields we can
+ builder := gomatrixserverlib.EventBuilder{
+ Sender: userID,
+ RoomID: roomID,
+ Type: eventType,
+ StateKey: stateKey,
+ }
+ err = builder.SetContent(r)
+ if err != nil {
+ resErr := httputil.LogThenError(req, err)
+ return nil, &resErr
+ }
+
+ var queryRes api.QueryLatestEventsAndStateResponse
+ e, err := common.BuildEvent(req.Context(), &builder, cfg, evTime, queryAPI, &queryRes)
+ if err == common.ErrRoomNoExists {
+ return nil, &util.JSONResponse{
+ Code: http.StatusNotFound,
+ JSON: jsonerror.NotFound("Room does not exist"),
+ }
+ } else if err != nil {
+ resErr := httputil.LogThenError(req, err)
+ return nil, &resErr
+ }
+
+ // check to see if this user can perform this operation
+ stateEvents := make([]*gomatrixserverlib.Event, len(queryRes.StateEvents))
+ for i := range queryRes.StateEvents {
+ stateEvents[i] = &queryRes.StateEvents[i]
+ }
+ provider := gomatrixserverlib.NewAuthEvents(stateEvents)
+ if err = gomatrixserverlib.Allowed(*e, &provider); err != nil {
+ return nil, &util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden(err.Error()), // TODO: Is this error string comprehensible to the client?
+ }
+ }
+ return e, nil
+}
diff --git a/clientapi/routing/sendtyping.go b/clientapi/routing/sendtyping.go
new file mode 100644
index 00000000..561a2d89
--- /dev/null
+++ b/clientapi/routing/sendtyping.go
@@ -0,0 +1,80 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "database/sql"
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/producers"
+ "github.com/matrix-org/dendrite/clientapi/userutil"
+ "github.com/matrix-org/util"
+)
+
+type typingContentJSON struct {
+ Typing bool `json:"typing"`
+ Timeout int64 `json:"timeout"`
+}
+
+// SendTyping handles PUT /rooms/{roomID}/typing/{userID}
+// sends the typing events to client API typingProducer
+func SendTyping(
+ req *http.Request, device *authtypes.Device, roomID string,
+ userID string, accountDB *accounts.Database,
+ typingProducer *producers.TypingServerProducer,
+) util.JSONResponse {
+ if device.UserID != userID {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("Cannot set another user's typing state"),
+ }
+ }
+
+ localpart, err := userutil.ParseUsernameParam(userID, nil)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // Verify that the user is a member of this room
+ _, err = accountDB.GetMembershipInRoomByLocalpart(req.Context(), localpart, roomID)
+ if err == sql.ErrNoRows {
+ return util.JSONResponse{
+ Code: http.StatusForbidden,
+ JSON: jsonerror.Forbidden("User not in this room"),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ // parse the incoming http request
+ var r typingContentJSON
+ resErr := httputil.UnmarshalJSONRequest(req, &r)
+ if resErr != nil {
+ return *resErr
+ }
+
+ if err = typingProducer.Send(
+ req.Context(), userID, roomID, r.Typing, r.Timeout,
+ ); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/threepid.go b/clientapi/routing/threepid.go
new file mode 100644
index 00000000..897d13b6
--- /dev/null
+++ b/clientapi/routing/threepid.go
@@ -0,0 +1,178 @@
+// Copyright 2017 Vector Creations Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/auth/storage/accounts"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/clientapi/jsonerror"
+ "github.com/matrix-org/dendrite/clientapi/threepid"
+ "github.com/matrix-org/dendrite/common/config"
+
+ "github.com/matrix-org/gomatrixserverlib"
+ "github.com/matrix-org/util"
+)
+
+type reqTokenResponse struct {
+ SID string `json:"sid"`
+}
+
+type threePIDsResponse struct {
+ ThreePIDs []authtypes.ThreePID `json:"threepids"`
+}
+
+// RequestEmailToken implements:
+// POST /account/3pid/email/requestToken
+// POST /register/email/requestToken
+func RequestEmailToken(req *http.Request, accountDB *accounts.Database, cfg config.Dendrite) util.JSONResponse {
+ var body threepid.EmailAssociationRequest
+ if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
+ return *reqErr
+ }
+
+ var resp reqTokenResponse
+ var err error
+
+ // Check if the 3PID is already in use locally
+ localpart, err := accountDB.GetLocalpartForThreePID(req.Context(), body.Email, "email")
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if len(localpart) > 0 {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.MatrixError{
+ ErrCode: "M_THREEPID_IN_USE",
+ Err: accounts.Err3PIDInUse.Error(),
+ },
+ }
+ }
+
+ resp.SID, err = threepid.CreateSession(req.Context(), body, cfg)
+ if err == threepid.ErrNotTrusted {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.NotTrusted(body.IDServer),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: resp,
+ }
+}
+
+// CheckAndSave3PIDAssociation implements POST /account/3pid
+func CheckAndSave3PIDAssociation(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+ cfg config.Dendrite,
+) util.JSONResponse {
+ var body threepid.EmailAssociationCheckRequest
+ if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
+ return *reqErr
+ }
+
+ // Check if the association has been validated
+ verified, address, medium, err := threepid.CheckAssociation(req.Context(), body.Creds, cfg)
+ if err == threepid.ErrNotTrusted {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.NotTrusted(body.Creds.IDServer),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if !verified {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.MatrixError{
+ ErrCode: "M_THREEPID_AUTH_FAILED",
+ Err: "Failed to auth 3pid",
+ },
+ }
+ }
+
+ if body.Bind {
+ // Publish the association on the identity server if requested
+ err = threepid.PublishAssociation(body.Creds, device.UserID, cfg)
+ if err == threepid.ErrNotTrusted {
+ return util.JSONResponse{
+ Code: http.StatusBadRequest,
+ JSON: jsonerror.NotTrusted(body.Creds.IDServer),
+ }
+ } else if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+ }
+
+ // Save the association in the database
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ if err = accountDB.SaveThreePIDAssociation(req.Context(), address, localpart, medium); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
+
+// GetAssociated3PIDs implements GET /account/3pid
+func GetAssociated3PIDs(
+ req *http.Request, accountDB *accounts.Database, device *authtypes.Device,
+) util.JSONResponse {
+ localpart, _, err := gomatrixserverlib.SplitID('@', device.UserID)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ threepids, err := accountDB.GetThreePIDsForLocalpart(req.Context(), localpart)
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: threePIDsResponse{threepids},
+ }
+}
+
+// Forget3PID implements POST /account/3pid/delete
+func Forget3PID(req *http.Request, accountDB *accounts.Database) util.JSONResponse {
+ var body authtypes.ThreePID
+ if reqErr := httputil.UnmarshalJSONRequest(req, &body); reqErr != nil {
+ return *reqErr
+ }
+
+ if err := accountDB.RemoveThreePIDAssociation(req.Context(), body.Address, body.Medium); err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+}
diff --git a/clientapi/routing/voip.go b/clientapi/routing/voip.go
new file mode 100644
index 00000000..b9121633
--- /dev/null
+++ b/clientapi/routing/voip.go
@@ -0,0 +1,78 @@
+// Copyright 2017 Michael Telatysnki <7t3chguy@gmail.com>
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "crypto/hmac"
+ "crypto/sha1"
+ "encoding/base64"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/dendrite/clientapi/httputil"
+ "github.com/matrix-org/dendrite/common/config"
+ "github.com/matrix-org/gomatrix"
+ "github.com/matrix-org/util"
+)
+
+// RequestTurnServer implements:
+// GET /voip/turnServer
+func RequestTurnServer(req *http.Request, device *authtypes.Device, cfg config.Dendrite) util.JSONResponse {
+ turnConfig := cfg.TURN
+
+ // TODO Guest Support
+ if len(turnConfig.URIs) == 0 || turnConfig.UserLifetime == "" {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+
+ // Duration checked at startup, err not possible
+ duration, _ := time.ParseDuration(turnConfig.UserLifetime)
+
+ resp := gomatrix.RespTurnServer{
+ URIs: turnConfig.URIs,
+ TTL: int(duration.Seconds()),
+ }
+
+ if turnConfig.SharedSecret != "" {
+ expiry := time.Now().Add(duration).Unix()
+ mac := hmac.New(sha1.New, []byte(turnConfig.SharedSecret))
+ _, err := mac.Write([]byte(resp.Username))
+
+ if err != nil {
+ return httputil.LogThenError(req, err)
+ }
+
+ resp.Username = fmt.Sprintf("%d:%s", expiry, device.UserID)
+ resp.Password = base64.StdEncoding.EncodeToString(mac.Sum(nil))
+ } else if turnConfig.Username != "" && turnConfig.Password != "" {
+ resp.Username = turnConfig.Username
+ resp.Password = turnConfig.Password
+ } else {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: struct{}{},
+ }
+ }
+
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: resp,
+ }
+}
diff --git a/clientapi/routing/whoami.go b/clientapi/routing/whoami.go
new file mode 100644
index 00000000..840bcb5f
--- /dev/null
+++ b/clientapi/routing/whoami.go
@@ -0,0 +1,34 @@
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package routing
+
+import (
+ "net/http"
+
+ "github.com/matrix-org/dendrite/clientapi/auth/authtypes"
+ "github.com/matrix-org/util"
+)
+
+// whoamiResponse represents an response for a `whoami` request
+type whoamiResponse struct {
+ UserID string `json:"user_id"`
+}
+
+// Whoami implements `/account/whoami` which enables client to query their account user id.
+// https://matrix.org/docs/spec/client_server/r0.3.0.html#get-matrix-client-r0-account-whoami
+func Whoami(req *http.Request, device *authtypes.Device) util.JSONResponse {
+ return util.JSONResponse{
+ Code: http.StatusOK,
+ JSON: whoamiResponse{UserID: device.UserID},
+ }
+}