mirror of
https://github.com/matrix-org/dendrite
synced 2024-11-10 15:14:36 +00:00
a97b8eafd4
* Use a fork of pq which supports userCurrent on wasm * Use sqlite3_js driver when running in JS * Add cmd/dendritejs to pull in sqlite3_js driver for wasm only * Update to latest go-sqlite-js version * Replace prometheus with a stub. sigh * Hard-code a config and don't use opentracing * Latest go-sqlite3-js version * Generate a key for now * Listen for fetch traffic rather than HTTP * Latest hacks for js * libp2p support * More libp2p * Fork gjson to allow us to enforce auth checks as before Previously, all events would come down redacted because the hash checks would fail. They would fail because sjson.DeleteBytes didn't remove keys not used for hashing. This didn't work because of a build tag which included a file which no-oped the index returned. See https://github.com/tidwall/gjson/issues/157 When it's resolved, let's go back to mainline. * Use gjson@1.6.0 as it fixes https://github.com/tidwall/gjson/issues/157 * Use latest gomatrixserverlib for sig checks * Fix a bug which could cause exclude_from_sync to not be set Caused when sending events over federation. * Use query variadic to make lookups actually work! * Latest gomatrixserverlib * Add notes on getting p2p up and running Partly so I don't forget myself! * refactor: Move p2p specific stuff to cmd/dendritejs This is important or else the normal build of dendrite will fail because the p2p libraries depend on syscall/js which doesn't work on normal builds. Also, clean up main.go to read a bit better. * Update ho-http-js-libp2p to return errors from RoundTrip * Add an LRU cache around the key DB We actually need this for P2P because otherwise we can *segfault* with things like: "runtime: unexpected return pc for runtime.handleEvent" where the event is a `syscall/js` event, caused by spamming sql.js caused by "Checking event signatures for 14 events of room state" which hammers the key DB repeatedly in quick succession. Using a cache fixes this, though the underlying cause is probably a bug in the version of Go I'm on (1.13.7) * breaking: Add Tracing.Enabled to toggle whether we do opentracing Defaults to false, which is why this is a breaking change. We need this flag because WASM builds cannot do opentracing. * Start adding conditional builds for wasm to handle lib/pq The general idea here is to have the wasm build have a `NewXXXDatabase` that doesn't import any postgres package and hence we never import `lib/pq`, which doesn't work under WASM (undefined `userCurrent`). * Remove lib/pq for wasm for syncapi * Add conditional building to remaining storage APIs * Update build script to set env vars correctly for dendritejs * sqlite bug fixes * Docs * Add a no-op main for dendritejs when not building under wasm * Use the real prometheus, even for WASM Instead, the dendrite-sw.js must mock out `process.pid` and `fs.stat` - which must invoke the callback with an error (e.g `EINVAL`) in order for it to work: ``` global.process = { pid: 1, }; global.fs.stat = function(path, cb) { cb({ code: "EINVAL", }); } ``` * Linting
1040 lines
33 KiB
Go
1040 lines
33 KiB
Go
// 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"
|
|
"sync"
|
|
"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/gomatrixserverlib/tokens"
|
|
"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.
|
|
// It shouldn't be passed by value because it contains a mutex.
|
|
type sessionsDict struct {
|
|
sync.Mutex
|
|
sessions map[string][]authtypes.LoginType
|
|
}
|
|
|
|
// GetCompletedStages returns the completed stages for a session.
|
|
func (d *sessionsDict) GetCompletedStages(sessionID string) []authtypes.LoginType {
|
|
d.Lock()
|
|
defer d.Unlock()
|
|
|
|
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)
|
|
}
|
|
|
|
func newSessionsDict() *sessionsDict {
|
|
return &sessionsDict{
|
|
sessions: make(map[string][]authtypes.LoginType),
|
|
}
|
|
}
|
|
|
|
// AddCompletedSessionStage records that a session has completed an auth stage.
|
|
func AddCompletedSessionStage(sessionID string, stage authtypes.LoginType) {
|
|
sessions.Lock()
|
|
defer sessions.Unlock()
|
|
|
|
for _, completedStage := range sessions.sessions[sessionID] {
|
|
if completedStage == stage {
|
|
return
|
|
}
|
|
}
|
|
sessions.sessions[sessionID] = append(sessions.sessions[sessionID], stage)
|
|
}
|
|
|
|
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"`
|
|
|
|
// Both DeviceID and InitialDisplayName can be omitted, or empty strings ("")
|
|
// Thus a pointer is needed to differentiate between the two
|
|
InitialDisplayName *string `json:"initial_device_display_name"`
|
|
DeviceID *string `json:"device_id"`
|
|
|
|
// 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.StatusConflict,
|
|
JSON: jsonerror.Unknown("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.StatusGatewayTimeout,
|
|
JSON: jsonerror.Unknown("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
|
|
}
|
|
if req.URL.Query().Get("kind") == "guest" {
|
|
return handleGuestRegistration(req, r, cfg, accountDB, deviceDB)
|
|
}
|
|
|
|
// 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 {
|
|
util.GetLogger(req.Context()).WithError(err).Error("accountDB.GetNewNumericLocalpart failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func handleGuestRegistration(
|
|
req *http.Request,
|
|
r registerRequest,
|
|
cfg *config.Dendrite,
|
|
accountDB accounts.Database,
|
|
deviceDB devices.Database,
|
|
) util.JSONResponse {
|
|
|
|
//Generate numeric local part for guest user
|
|
id, err := accountDB.GetNewNumericLocalpart(req.Context())
|
|
if err != nil {
|
|
util.GetLogger(req.Context()).WithError(err).Error("accountDB.GetNewNumericLocalpart failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
localpart := strconv.FormatInt(id, 10)
|
|
acc, err := accountDB.CreateAccount(req.Context(), localpart, "", "")
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: jsonerror.Unknown("failed to create account: " + err.Error()),
|
|
}
|
|
}
|
|
token, err := tokens.GenerateLoginToken(tokens.TokenOptions{
|
|
ServerPrivateKey: cfg.Matrix.PrivateKey.Seed(),
|
|
ServerName: string(acc.ServerName),
|
|
UserID: acc.UserID,
|
|
})
|
|
|
|
if err != nil {
|
|
return util.JSONResponse{
|
|
Code: http.StatusInternalServerError,
|
|
JSON: jsonerror.Unknown("Failed to generate access token"),
|
|
}
|
|
}
|
|
//we don't allow guests to specify their own device_id
|
|
dev, err := deviceDB.CreateDevice(req.Context(), acc.Localpart, nil, token, r.InitialDisplayName)
|
|
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: registerResponse{
|
|
UserID: dev.UserID,
|
|
AccessToken: dev.AccessToken,
|
|
HomeServer: acc.ServerName,
|
|
DeviceID: dev.ID,
|
|
},
|
|
}
|
|
}
|
|
|
|
// 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
|
|
AddCompletedSessionStage(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 {
|
|
util.GetLogger(req.Context()).WithError(err).Error("isValidMacLogin failed")
|
|
return jsonerror.InternalServerError()
|
|
} else if !valid {
|
|
return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
|
|
}
|
|
|
|
// Add SharedSecret to the list of completed registration stages
|
|
AddCompletedSessionStage(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
|
|
AddCompletedSessionStage(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, r.DeviceID,
|
|
)
|
|
}
|
|
|
|
// 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, r.DeviceID,
|
|
)
|
|
}
|
|
|
|
// 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 {
|
|
util.GetLogger(req.Context()).WithError(err).Error("isValidMacLogin failed")
|
|
return jsonerror.InternalServerError()
|
|
}
|
|
|
|
if !valid {
|
|
return util.MessageResponse(http.StatusForbidden, "HMAC incorrect")
|
|
}
|
|
|
|
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil, nil)
|
|
case authtypes.LoginTypeDummy:
|
|
// there is nothing to do
|
|
return completeRegistration(req.Context(), accountDB, deviceDB, r.Username, r.Password, "", false, nil, 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
|
|
}
|
|
|
|
// completeRegistration runs some rudimentary checks against the submitted
|
|
// input, then if successful creates an account and a newly associated device
|
|
// We pass in each individual part of the request here instead of just passing a
|
|
// registerRequest, as this function serves requests encoded as both
|
|
// registerRequests and legacyRegisterRequests, which share some attributes but
|
|
// not all
|
|
func completeRegistration(
|
|
ctx context.Context,
|
|
accountDB accounts.Database,
|
|
deviceDB devices.Database,
|
|
username, password, appserviceID string,
|
|
inhibitLogin common.WeakBoolean,
|
|
displayName, deviceID *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."),
|
|
}
|
|
}
|
|
|
|
// Increment prometheus counter for created users
|
|
amtRegUsers.Inc()
|
|
|
|
// 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"),
|
|
}
|
|
}
|
|
|
|
dev, err := deviceDB.CreateDevice(ctx, username, deviceID, token, displayName)
|
|
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: 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,
|
|
},
|
|
}
|
|
}
|