From 4cce6bab0991552dfa9ec8bcb4c8a8e9080f05ca Mon Sep 17 00:00:00 2001 From: Sven Slootweg Date: Thu, 2 May 2019 23:36:48 +0200 Subject: [PATCH] WIP --- src/errors.js | 5 +- .../client-api/authentication/index.js | 7 +- .../client-api/authentication/login.js | 4 +- .../client-api/authentication/register.js | 85 +++++++++++++++++++ src/validator-lib.js | 8 +- 5 files changed, 96 insertions(+), 13 deletions(-) create mode 100644 src/routers/client-api/authentication/register.js diff --git a/src/errors.js b/src/errors.js index 40a8fa1..09b67a5 100644 --- a/src/errors.js +++ b/src/errors.js @@ -54,6 +54,7 @@ let InvalidParameter = InvalidData.extend("InvalidParameter", { errorCode: "M_IN let MissingParameter = InvalidData.extend("MissingParameter", { errorCode: "M_MISSING_PARAM" }); let IncompatibleRoomVersion = InvalidData.extend("IncompatibleRoomVersion", { errorCode: "M_INCOMPATIBLE_ROOM_VERSION" }); let WeakPassword = InvalidData.extend("WeakPassword", { errorCode: "M_WEAK_PASSWORD" }); +let ExclusiveResource = InvalidData.extend("ExclusiveResource", { errorCode: "M_EXCLUSIVE" }); /*** HTTP 429: Too Many Requests ***/ @@ -65,10 +66,6 @@ let InternalServerError = HttpError.extend("InternalServerError", { statusCode: let UnknownError = InternalServerError.extend("UnknownError", { errorCode: "M_UNKNOWN" }); let Unreachable = InternalServerError.extend("Unreachable"); /* FIXME: How to represent this with an error code? This should crash the process anyway. */ -/*** HTTP 503: Service Unavailable ***/ -// This describes reserved namespaces and such (eg. for registration), so probably should be a client error instead -let ExclusiveResource = HttpError.extend("ExclusiveResource", { statusCode: 503, errorCode: "M_EXCLUSIVE" }); - module.exports = { HttpError, Unauthorized, MissingAccessToken, InvalidAccessToken, NotJson, BadJson, NotFound, TooManyRequests, MissingCaptcha, InvalidCaptcha, ServerNotTrusted, NoGuestAccess, UserExists, RoomExists, RequestTooLarge, InvalidUsername, InvalidRoomVersion, InvalidRoomState, InvalidStateChange, InvalidParameter, MissingParameter, IncompatibleRoomVersion, InternalServerError, UnknownError, ExclusiveResource, ResourceExists, Forbidden, BadRequest, ExternalIdentifierNotFound, ExternalIdentifierExists, ExternalIdentifierAuthenticationFailed, InvalidExternalIdentifier, WeakPassword, Unreachable }; \ No newline at end of file diff --git a/src/routers/client-api/authentication/index.js b/src/routers/client-api/authentication/index.js index 22ebd9c..f42991a 100644 --- a/src/routers/client-api/authentication/index.js +++ b/src/routers/client-api/authentication/index.js @@ -9,6 +9,7 @@ module.exports = function(state) { let router = expressPromiseRouter(); router.use(require("./login")(state)); + router.use(require("./register")(state)) router.post("/r0/logout", requireAccessToken, (req, res) => { // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout @@ -25,12 +26,6 @@ module.exports = function(state) { // Checks to see if a username is available, and valid, for the server. }); - router.post("/r0/register", (req, res) => { - // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register - // Register for an account on this homeserver. This API endpoint uses the User-Interactive Authentication API. - // Optionally allows user-interactive auth - }); - router.post("/r0/register/:source/requestToken", (req, res) => { // source === "email" // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register-email-requesttoken diff --git a/src/routers/client-api/authentication/login.js b/src/routers/client-api/authentication/login.js index 8d47bc4..9495b22 100644 --- a/src/routers/client-api/authentication/login.js +++ b/src/routers/client-api/authentication/login.js @@ -57,7 +57,7 @@ module.exports = function(state) { }); function validateLoginPayload(payload) { - let {assertProperties, isPresent, isString, isOneOf} = require("../../../validator-lib.js"); + let {assertProperties, isPresent, isString, isOneOf} = require("../../../validator-lib"); assertProperties(payload, { type: [ isPresent, isOneOf("m.login.password", "m.login.token") ], @@ -118,7 +118,7 @@ module.exports = function(state) { access_token: device.token, user_id: fullyQualifyUser(user.username), home_server: configuration.hostname, - device_id: device.deviceId + device_id: device.device_id }); }); }); diff --git a/src/routers/client-api/authentication/register.js b/src/routers/client-api/authentication/register.js new file mode 100644 index 0000000..742c6f0 --- /dev/null +++ b/src/routers/client-api/authentication/register.js @@ -0,0 +1,85 @@ +"use strict"; + +const Promise = require("bluebird"); +const defaultValue = require("default-value"); + +const errors = require("../../../errors"); +const validate = require("../../../middlewares/validate-payload"); +const processDeviceID = require("../../../process-device-id"); + +module.exports = function({ db, knex, configuration, fullyQualifyUser }) { + let router = require("express-promise-router")(); + + function validateRegisterPayload(payload) { + let {assertProperties, isPresent, isString, isOneOf, isBoolean} = require("../../../validator-lib"); + + assertProperties(payload, { + kind: [ isOneOf("user", "guest") ], + device_id: [ isString ], + initial_device_display_name: [ isString ], + username: [ isPresent, isString, (username) => { + if (!/^[a-z0-9._=/-]+$/.test(username)) { + throw new errors.InvalidUsername("Usernames may only contain a-z, 0-9, and . _ = - /"); + } else if (username.startsWith("_")) { + throw new errors.ExclusiveResource("Usernames starting with _ are reserved for bridges"); + } + } ], + password: [ isPresent, isString ], /* FIXME: Password strength */ + inhibit_login: [ isBoolean ], + bind_email: [ isBoolean ] + }); + } + + router.post("/r0/register", validate(validateRegisterPayload), (req, res) => { + // https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register + // Register for an account on this homeserver. This API endpoint uses the User-Interactive Authentication API. + + /* FIXME: Support User-Interactive Authentication (eg. for registration CAPTCHAs) */ + /* FIXME: Rate-limit */ + + if (req.body.kind === "guest") { + /* TODO: Re-evaluate this, once it becomes clearer how to deal with guest users and peeking across federation (ref. https://github.com/matrix-org/matrix-doc/pull/1777) */ + throw new errors.Forbidden("Guest registrations are disabled on this server"); + } else { + let inhibitLogin = defaultValue(req.body.inhibit_login, false); + /* FILEBUG: Default for bind_email is not clearly specified in spec */ + /* FIXME: Actually implement e-mail binding on identity server */ + let bindEmail = defaultValue(req.body.bind_email, false); + let deviceId = processDeviceID(req.body.deviceID); + + return knex.transaction((transaction) => { + return Promise.try(() => { + return db.users.create({ + type: "user", + username: req.body.username, + password: req.body.password + }, transaction); + }).then((user) => { + return Promise.try(() => { + if (!inhibitLogin) { + return db.devices.createDeviceSession({ + name: req.body.initial_device_display_name, + deviceId: deviceId, + userId: user.id + }, transaction); + } + }).then((device) => { + let sessionDetails = (device != null) + ? { access_token: device.token, device_id: device.device_id } + : {}; + + res.send({ + user_id: fullyQualifyUser(user.username), + home_server: configuration.hostname, + ...sessionDetails + }); + }); + }); + }).catch({ name: "UniqueConstraintViolationError", table: "users", column: "username" }, (_error) => { + throw new errors.UserExists("That username is already taken"); + }); + } + }); + + return router; +} \ No newline at end of file diff --git a/src/validator-lib.js b/src/validator-lib.js index 75a5c8b..08959a7 100644 --- a/src/validator-lib.js +++ b/src/validator-lib.js @@ -16,6 +16,12 @@ function isString(value) { } } +function isBoolean(value) { + if (value != null && typeof value !== "boolean") { + throw new ValidationError("Value is not a boolean", { errorType: "isBoolean" }); + } +} + function isOneOf(...acceptableValues) { let valueSet = new Set(acceptableValues); @@ -104,7 +110,7 @@ function validate(validator, value) { module.exports = { validate, assertProperties, ValidationError, - isPresent, isString, isOneOf, arrayOf + isPresent, isString, isOneOf, arrayOf, isBoolean }; // try {