master
Sven Slootweg 5 years ago
parent ba6ff51b2f
commit e3ac9edfe4

@ -16,6 +16,7 @@
"express": "^4.16.4",
"express-promise-router": "^3.0.3",
"knex": "^0.16.5",
"map-obj": "^3.1.0",
"nanoid": "^2.0.1",
"nanoid-dictionary": "^2.0.0",
"pg": "^7.10.0",

@ -82,8 +82,6 @@ module.exports = function ({ knex }) {
return this.create({ name, deviceId, userId, token });
}).catch({ name: "UniqueConstraintViolationError", table: "devices", column: "device_id" }, (_error) => {
return this.updateToken({ userId, deviceId, token });
}).then((device) => {
/* FIXME */
});
});
}

@ -8,6 +8,7 @@ const url = require("url");
const debugMiddleware = require("./debug-middleware");
const accessTokenMiddleware = require("./middlewares/access-token");
const errorHandlerMiddleware = require("./middlewares/error-handler");
const clientApiRouter = require("./routers/client-api");
@ -49,7 +50,7 @@ router.use(accessTokenMiddleware);
router.use(debugMiddleware());
router.options("*", cors({
router.use(cors({
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allowedHeaders: ["Origin", "X-Requested-With", "Content-Type", "Accept", "Authorization"],
optionsSuccessStatus: 200
@ -78,6 +79,7 @@ router.get("/.well-known/matrix/client", (req, res) => {
});
app.use(router);
app.use(errorHandlerMiddleware);
app.listen(3000, () => {
console.log("listening on 3000");
});

@ -2,26 +2,37 @@
const defaultValue = require("default-value");
const http = require("http");
const chalk = require("chalk");
const util = require("util");
module.exports = function errorHandler(error, _req, res, _next) {
let statusCode = (error.isHttpError && error.statusCode != null)
? error.statusCode
console.log(chalk.bold.red("Unhandled error:"), error.stack);
/* This extracts any (nested) metadata from chained errors */
let errorContext = (error.getAllContext != null)
? error.getAllContext()
: error;
console.log(chalk.red("Error metadata:"), util.inspect(errorContext, { colors: true, depth: 5 }));
let statusCode = (errorContext.isHttpError && errorContext.statusCode != null)
? errorContext.statusCode
: 500;
let errorCode = (error.isHttpError && error.errorCode != null)
? error.errorCode
let errorCode = (errorContext.isHttpError && errorContext.errorCode != null)
? errorContext.errorCode
: "M_UNKNOWN";
let errorMessage = (error.errorCode !== 500)
? defaultValue(error.message, http.STATUS_CODES[statusCode])
let errorMessage = (errorContext.errorCode !== 500)
? defaultValue(errorContext.message, http.STATUS_CODES[statusCode])
: "An internal server error occurred. Please contact the server administrator for more information."
/* TODO: Add contact details? */
let errorMeta = (error.errorMeta != null)
? error.errorMeta
let errorMeta = (errorContext.errorMeta != null)
? errorContext.errorMeta
: {};
res.json(Object.assign({}, errorMeta, {
res.status(statusCode).json(Object.assign({}, errorMeta, {
errcode: errorCode,
error: errorMessage
}));

@ -4,7 +4,7 @@
const errors = require("../errors");
const {validate, ValidationError} = require("../validate");
const {validate, ValidationError} = require("../validator-lib");
module.exports = function createPayloadValidator(validator) {
return function validatePayload(req, _res, next) {

@ -1,221 +0,0 @@
"use strict";
const Promise = require("bluebird");
const expressPromiseRouter = require("express-promise-router");
const scryptForHumans = require("scrypt-for-humans");
const errors = require("../../errors");
const normalize = require("../../middlewares/normalize-payload");
const validate = require("../../middlewares/validate-payload");
const processDeviceId = require("../../process-device-id");
const matchValue = require("../../match-value");
const canonicalizePhoneNumber = require("../../canonicalize-phone-number");
function normalizeIdentifier(payload) {
if (payload.identifier != null) {
/* Never overwrite the `identifier` key if it already exists */
return null;
} else if (payload.user != null) {
return {
identifier: {
type: "m.id.user",
user: payload.user
}
};
} else if (payload.address != null) {
return {
identifier: {
type: "m.id.thirdparty",
medium: payload.medium,
address: payload.address
}
};
}
}
function normalizePhoneNumber(payload) {
if (payload.identifier != null && payload.identifier.type === "m.id.phone") {
return {
identifier: {
type: "m.id.thirdparty",
medium: "msisdn",
address: canonicalizePhoneNumber(payload.identifier.country, payload.identifier.phone)
}
};
}
}
function validateUserIdentifier(userIdentifier) {
let {assertProperties, isPresent, isString, isOneOf} = require("../../validate.js");
assertProperties(userIdentifier, {
type: [ isPresent, isOneOf("m.id.user", "m.id.thirdparty", "m.id.phone") ]
});
matchValue(userIdentifier.type, {
"m.id.user": () => {
assertProperties(userIdentifier, {
user: [ isPresent, isString ]
});
},
"m.id.thirdparty": () => {
assertProperties(userIdentifier, {
medium: [ isPresent, isString ],
address: [ isPresent, isString ]
});
},
"m.id.phone": () => {
assertProperties(userIdentifier, {
country: [ isPresent, isString ],
phone: [ isPresent, isString ]
});
}
});
}
module.exports = function(state) {
const requireAccessToken = require("../../middlewares/require-access-token")(state);
let {db, configuration, fullyQualifyUser} = state;
let router = expressPromiseRouter();
router.get("/r0/login", (req, res) => {
res.json({
flows: [{ type: "m.login.password" }]
});
});
function validateLoginPayload(payload) {
let {assertProperties, isPresent, isString, isOneOf} = require("../../validate.js");
assertProperties(payload, {
type: [ isPresent, isOneOf("m.login.password", "m.login.token") ],
device_id: [ isString ],
initial_device_display_name: [ isString ],
});
matchValue(payload.type, {
"m.login.password": () => {
assertProperties(payload, {
identifier: [ isPresent, validateUserIdentifier ],
password: [ isPresent, isString ]
});
},
"m.login.token": () => {
assertProperties(payload, {
token: [ isPresent, isString ]
});
}
});
}
router.post("/r0/login", normalize([normalizeIdentifier, normalizePhoneNumber]), validate(validateLoginPayload), (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-login
// Authenticates the user, and issues an access token they can use to authorize themself in subsequent requests.
/* FIXME: Rate-limit */
return Promise.try(() => {
if (req.body.identifier.type === "m.id.user") {
return db.accounts.byUsername({
username: req.body.identifier.user
});
} else if (req.body.identifier.type === "m.id.thirdparty") {
// FIXME
} else {
/* NOTE: We're not handling m.id.phone here, as that's normalized into m.id.thirdparty in normalizePhoneNumber */
throw new errors.Unreachable("Invalid identifier type");
}
}).then((user) => {
let deviceId = processDeviceId(req.body.device_id);
return Promise.try(() => {
if (req.body.type === "m.login.password") {
return scryptForHumans.verifyHash(req.body.password, user.password_hash);
} else if (req.body.type === "m.login.token") {
// FIXME
} else {
throw new errors.Unreachable("Invalid login type");
}
}).then(() => {
return db.devices.createDeviceSession({
name: req.body.initial_device_display_name,
deviceId: deviceId,
userId: user.id
}).then((device) => {
res.send({
access_token: device.token,
user_id: fullyQualifyUser(user.username),
home_server: configuration.hostname,
device_id: device.deviceId
});
});
});
}).catch(errors.InvalidUsername, errors.Forbidden.chain("Invalid username specified"))
.catch(scryptForHumans.PasswordError, errors.Forbidden.chain("Invalid password specified"));
/* FIXME: Formatting above */
});
router.post("/r0/logout", requireAccessToken, (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout
// Invalidates an existing access token, so that it can no longer be used for authorization.
});
router.post("/r0/logout/all", requireAccessToken, (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout-all
// Invalidates all access tokens for a user, so that they can no longer be used for authorization. This includes the access token that made this request.
});
router.post("/r0/register/available", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-register-available
// 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
// Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is not already associated with an account on this homeserver. See the Identity Service API for further information.
// source === "msisdn"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register-msisdn-requesttoken
// Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is not already associated with an account on this homeserver. See the Identity Service API for further information.
// NOTE: Unclear whether it requires authentication or not.
});
router.post("/r0/account/password", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password
// Changes the password for an account on this homeserver.
// NOTE: Requires access token *or* interactive auth.
});
router.post("/r0/account/password/:source/requestToken", (req, res) => {
// source === "email"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-email-requesttoken
// Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint.
// source === "msisdn"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-msisdn-requesttoken
// Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint.
// NOTE: Unclear whether it requires authentication or not.
});
router.post("/r0/account/deactivate", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-deactivate
// Deactivate the user's account, removing all ability for the user to login again. This API endpoint uses the User-Interactive Authentication API.
// NOTE: Requires access token *or* interactive auth.
});
// FIXME: Modularize out 'create session for device' logic for both login and register endpoints
return router;
}

@ -0,0 +1,75 @@
"use strict";
const Promise = require("bluebird");
const expressPromiseRouter = require("express-promise-router");
module.exports = function(state) {
const requireAccessToken = require("../../../middlewares/require-access-token")(state);
let router = expressPromiseRouter();
router.use(require("./login")(state));
router.post("/r0/logout", requireAccessToken, (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout
// Invalidates an existing access token, so that it can no longer be used for authorization.
});
router.post("/r0/logout/all", requireAccessToken, (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-logout-all
// Invalidates all access tokens for a user, so that they can no longer be used for authorization. This includes the access token that made this request.
});
router.post("/r0/register/available", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-register-available
// 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
// Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is not already associated with an account on this homeserver. See the Identity Service API for further information.
// source === "msisdn"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-register-msisdn-requesttoken
// Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is not already associated with an account on this homeserver. See the Identity Service API for further information.
// NOTE: Unclear whether it requires authentication or not.
});
router.post("/r0/account/password", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password
// Changes the password for an account on this homeserver.
// NOTE: Requires access token *or* interactive auth.
});
router.post("/r0/account/password/:source/requestToken", (req, res) => {
// source === "email"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-email-requesttoken
// Proxies the Identity Service API validate/email/requestToken, but first checks that the given email address is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint.
// source === "msisdn"
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-password-msisdn-requesttoken
// Proxies the Identity Service API validate/msisdn/requestToken, but first checks that the given phone number is associated with an account on this homeserver. This API should be used to request validation tokens when authenticating for the account/password endpoint.
// NOTE: Unclear whether it requires authentication or not.
});
router.post("/r0/account/deactivate", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-deactivate
// Deactivate the user's account, removing all ability for the user to login again. This API endpoint uses the User-Interactive Authentication API.
// NOTE: Requires access token *or* interactive auth.
});
// FIXME: Modularize out 'create session for device' logic for both login and register endpoints
return router;
}

@ -0,0 +1,133 @@
"use strict";
const Promise = require("bluebird");
const scryptForHumans = require("scrypt-for-humans");
const errors = require("../../../errors");
const normalize = require("../../../middlewares/normalize-payload");
const validate = require("../../../middlewares/validate-payload");
const processDeviceId = require("../../../process-device-id");
const matchValue = require("../../../match-value");
const canonicalizePhoneNumber = require("../../../canonicalize-phone-number");
const validateUserIdentifier = require("../../../validate/user-identifier");
function normalizeIdentifier(payload) {
if (payload.identifier != null) {
/* Never overwrite the `identifier` key if it already exists */
return null;
} else if (payload.user != null) {
return {
identifier: {
type: "m.id.user",
user: payload.user
}
};
} else if (payload.address != null) {
return {
identifier: {
type: "m.id.thirdparty",
medium: payload.medium,
address: payload.address
}
};
}
}
function normalizePhoneNumber(payload) {
if (payload.identifier != null && payload.identifier.type === "m.id.phone") {
return {
identifier: {
type: "m.id.thirdparty",
medium: "msisdn",
address: canonicalizePhoneNumber(payload.identifier.country, payload.identifier.phone)
}
};
}
}
module.exports = function(state) {
let {db, configuration, fullyQualifyUser} = state;
let router = require("express-promise-router")();
router.get("/r0/login", (req, res) => {
res.json({
flows: [{ type: "m.login.password" }]
});
});
function validateLoginPayload(payload) {
let {assertProperties, isPresent, isString, isOneOf} = require("../../../validator-lib.js");
assertProperties(payload, {
type: [ isPresent, isOneOf("m.login.password", "m.login.token") ],
device_id: [ isString ],
initial_device_display_name: [ isString ],
});
matchValue(payload.type, {
"m.login.password": () => {
assertProperties(payload, {
identifier: [ isPresent, validateUserIdentifier ],
password: [ isPresent, isString ]
});
},
"m.login.token": () => {
assertProperties(payload, {
token: [ isPresent, isString ]
});
}
});
}
router.post("/r0/login", normalize([normalizeIdentifier, normalizePhoneNumber]), validate(validateLoginPayload), (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-login
// Authenticates the user, and issues an access token they can use to authorize themself in subsequent requests.
/* FIXME: Rate-limit */
return Promise.try(() => {
if (req.body.identifier.type === "m.id.user") {
return db.accounts.byUsername({
username: req.body.identifier.user
});
} else if (req.body.identifier.type === "m.id.thirdparty") {
// FIXME
} else {
/* NOTE: We're not handling m.id.phone here, as that's normalized into m.id.thirdparty in normalizePhoneNumber */
throw new errors.Unreachable("Invalid identifier type");
}
}).then((user) => {
let deviceId = processDeviceId(req.body.device_id);
return Promise.try(() => {
if (req.body.type === "m.login.password") {
return scryptForHumans.verifyHash(req.body.password, user.password_hash);
} else if (req.body.type === "m.login.token") {
// FIXME
} else {
throw new errors.Unreachable("Invalid login type");
}
}).then(() => {
return db.devices.createDeviceSession({
name: req.body.initial_device_display_name,
deviceId: deviceId,
userId: user.id
}).then((device) => {
res.send({
access_token: device.token,
user_id: fullyQualifyUser(user.username),
home_server: configuration.hostname,
device_id: device.deviceId
});
});
});
}).catch(errors.InvalidUsername, (error) => {
throw errors.Forbidden.chain(error, "Invalid username specified");
}).catch(scryptForHumans.PasswordError, (error) => {
throw errors.Forbidden.chain(error, "Invalid password specified");
});
});
return router;
}

@ -0,0 +1,31 @@
"use strict";
const matchValue = require("../match-value");
module.exports = function validateUserIdentifier(userIdentifier) {
let {assertProperties, isPresent, isString, isOneOf} = require("../validator-lib");
assertProperties(userIdentifier, {
type: [ isPresent, isOneOf("m.id.user", "m.id.thirdparty", "m.id.phone") ]
});
matchValue(userIdentifier.type, {
"m.id.user": () => {
assertProperties(userIdentifier, {
user: [ isPresent, isString ]
});
},
"m.id.thirdparty": () => {
assertProperties(userIdentifier, {
medium: [ isPresent, isString ],
address: [ isPresent, isString ]
});
},
"m.id.phone": () => {
assertProperties(userIdentifier, {
country: [ isPresent, isString ],
phone: [ isPresent, isString ]
});
}
});
};

@ -54,6 +54,8 @@ function assertProperties(object, properties) {
}
}
/* FIXME: Abstract out the path and value assignment for errors into a `withPath(...)` abstraction */
function arrayOf(assertions) {
return function (value) {
if (value != null) {

@ -1415,6 +1415,11 @@ map-cache@^0.2.0, map-cache@^0.2.2:
resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf"
integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=
map-obj@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-3.1.0.tgz#3be9810d926db2f8612c728a2e95e03b7f109241"
integrity sha512-Xg1iyYz/+iIW6YoMldux47H/e5QZyDSB41Kb0ev+YYHh3FJnyyzY0vTk/WbVeWcCvdXd70cOriUBmhP8alUFBA==
map-visit@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/map-visit/-/map-visit-1.0.0.tgz#ecdca8f13144e660f1b5bd41f12f3479d98dfb8f"

Loading…
Cancel
Save