master
Sven Slootweg 5 years ago
parent 82af54b214
commit ba6ff51b2f

@ -27,7 +27,7 @@ module.exports = {
"no-dupe-args": [ "error" ],
"no-dupe-keys": [ "error" ],
"no-duplicate-case": [ "error" ],
"no-empty": [ "error" ],
// "no-empty": [ "error" ],
"no-empty-character-class": [ "error" ],
"no-ex-assign": [ "error" ],
"no-extra-semi": [ "error" ],

@ -0,0 +1,30 @@
'use strict';
module.exports.up = function(knex, Promise) {
return Promise.try(() => {
return knex.schema.createTable("users", (table) => {
table.bigIncrements("id").primary();
table.text("username").notNull();
table.text("password_hash");
table.enum("type", ["user", "guest"]).notNull();
});
}).then(() => {
return knex.schema.createTable("devices", (table) => {
table.bigIncrements("id").primary();
table.integer("user_id").notNull().references("users.id");
table.text("name");
table.text("device_id").notNull();
table.text("token").notNull();
table.unique(["user_id", "device_id"]);
});
});
};
module.exports.down = function(knex, Promise) {
return Promise.try(() => {
return knex.schema.dropTable("devices");
}).then(() => {
return knex.schema.dropTable("users");
});
};

@ -0,0 +1,62 @@
QUESTIONS:
- What are allowable characters for a device ID?
https://github.com/matrix-org/matrix-doc/issues/1257
----
The txn_id in a token authentication request is meant to be tied permanently to the (single-use) token by the server, *after* the first request from the client that includes the txn_id. It functions as a nonce that other clients would not be able to guess, essentially locking the token to a specific client. Ref https://github.com/matrix-org/matrix-doc/pull/69
----
Error metadata:
TooManyRequests: { retry_after_ms: 2000 }
----
Deprecated features to maybe support later:
-
----
Skipped, todo later:
GET /_matrix/static/client/login/
https://matrix.org/docs/spec/client_server/r0.4.0.html#id216
This returns an HTML and JavaScript page which can perform the entire login process. The page will attempt to call the JavaScript function window.onLogin when login has been successfully completed.
-----
GUEST ACCOUNTS
https://matrix.org/docs/spec/client_server/r0.4.0.html#id520
The following API endpoints are allowed to be accessed by guest accounts for retrieving events:
GET /rooms/:room_id/state
GET /rooms/:room_id/context/:event_id
GET /rooms/:room_id/event/:event_id
GET /rooms/:room_id/state/:event_type/:state_key
GET /rooms/:room_id/messages
GET /rooms/:room_id/initialSync
GET /sync
GET /events as used for room previews.
The following API endpoints are allowed to be accessed by guest accounts for sending events:
POST /rooms/:room_id/join
POST /rooms/:room_id/leave
PUT /rooms/:room_id/send/m.room.message/:txn_id
PUT /sendToDevice/{eventType}/{txnId}
The following API endpoints are allowed to be accessed by guest accounts for their own account maintenance:
PUT /profile/:user_id/displayname
GET /devices
GET /devices/{deviceId}
PUT /devices/{deviceId}
The following API endpoints are allowed to be accessed by guest accounts for end-to-end encryption:
POST /keys/upload
POST /keys/query
POST /keys/claim

@ -6,15 +6,20 @@
"author": "Sven Slootweg <admin@cryto.net>",
"license": "WTFPL OR CC0-1.0",
"dependencies": {
"awesome-phonenumber": "^2.9.0",
"bluebird": "^3.5.4",
"body-parser": "^1.19.0",
"chalk": "^2.4.2",
"cors": "^2.8.5",
"database-error": "^2.0.1",
"default-value": "^1.0.0",
"express": "^4.16.4",
"express-promise-router": "^3.0.3",
"knex": "^0.16.5",
"pg": "^7.10.0"
"nanoid": "^2.0.1",
"nanoid-dictionary": "^2.0.0",
"pg": "^7.10.0",
"scrypt-for-humans": "^2.0.5"
},
"devDependencies": {
"eslint": "^5.16.0",

@ -0,0 +1,16 @@
"use strict";
const errors = require("../errors");
module.exports = function decodeAccessToken(encodedAccessToken) {
if (encodedAccessToken.includes(":")) {
let [encodedDeviceId, token] = encodedAccessToken.split(":");
return {
deviceId: Buffer.from(encodedDeviceId, "base64").toString(),
token: token
};
} else {
throw new errors.InvalidAccessToken("Invalid access token provided");
}
};

@ -0,0 +1,9 @@
"use strict";
module.exports = function encodeAccessToken({ deviceId, token }) {
if (deviceId == null || token == null) {
throw new Error("Device ID and token are required");
} else {
return `${Buffer.from(deviceId).toString("base64")}:${token}`;
}
};

@ -0,0 +1,7 @@
"use strict";
module.exports = function ({ db }) {
return function passwordAuthenticator(data) {
}
};

@ -0,0 +1,7 @@
"use strict";
const PhoneNumber = require("awesome-phonenumber");
module.exports = function canonicalizePhoneNumber(isoRegionCode, phoneNumber) {
return PhoneNumber(phoneNumber, isoRegionCode).getNumber("e164").replace(/^\+/, "");
};

@ -0,0 +1,17 @@
"use strict";
/* FIXME: Complete, maybe */
const Promise = require("bluebird");
module.exports = function atLeastOne(method) {
return function methodWrapper(...args) {
return Promise.try(() => {
return method(...args);
}).then((result) => {
if (Array.isArray(result)) {
}
})
}
};

@ -0,0 +1,23 @@
"use strict";
const Promise = require("bluebird");
const mapObj = require("map-obj");
const databaseError = require("database-error");
function wrapMethod(value) {
if (typeof value === "function") {
return function wrappedDatabaseMethod(...args) {
return Promise.try(() => {
return value(...args);
}).catch(databaseError.rethrow);
};
} else {
return value;
}
}
module.exports = function createDatabaseModule(methods) {
return mapObj(methods, (key, value) => {
return [key, wrapMethod(value)];
});
};

@ -0,0 +1,39 @@
"use strict";
const Promise = require("bluebird");
const scryptForHumans = require("scrypt-for-humans");
const errors = require("../errors");
const extractLocalPart = require("../extract-local-part");
const createDatabaseModule = require("../db-module");
module.exports = function ({ knex }) {
return createDatabaseModule({
create: function ({ username, password, type }, tx = knex) {
return Promise.try(() => {
if (password != null) {
return scryptForHumans.hash(password);
}
}).then((hash) => {
return tx("users").insert({
username: username,
password_hash: hash,
type: type
}).returning("*");
});
},
byUsername: function ({ username }, tx = knex) {
return Promise.try(() => {
/* TODO: Validate that, if a fully-qualified ID is passed in, it's for the correct homeserver? */
return tx("users").first().where({ username: extractLocalPart(username) });
}).then((user) => {
if (user != null) {
return user;
} else {
throw new errors.InvalidUsername("No such user exists");
/* FIXME: Map to 403 error in login route */
}
});
}
});
};

@ -1,7 +1,91 @@
"use strict";
module.exports = function ({db}) {
return {
const Promise = require("bluebird");
const crypto = require("crypto");
const nanoid = require("nanoid");
const defaultValue = require("default-value");
const databaseError = require("database-error");
};
const errors = require("../errors");
const createDatabaseModule = require("../db-module");
function tokensMatch(a, b) {
return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
}
module.exports = function ({ knex }) {
return createDatabaseModule({
create: function ({ name, deviceId, userId, token }, tx = knex) {
return Promise.try(() => {
let token_ = defaultValue(token, nanoid());
let deviceId_ = defaultValue(deviceId, nanoid());
return tx("devices").insert({
device_id: deviceId_,
user_id: userId,
name: name,
token: token_,
}).returning("*");
}).then((results) => {
return results[0];
});
},
byDeviceId: function ({ deviceId }, tx = knex) {
return Promise.try(() => {
return tx("devices").first().where({ device_id: deviceId });
}).then((result) => {
if (result != null) {
return result;
} else {
throw new errors.NotFound("The specified device ID does not exist");
}
});
},
byDeviceIdAndToken: function ({ deviceId, token }, _tx = knex) {
/* NOTE: This is intentionally a separate method as opposed to conditional logic in byDeviceId, to prevent cases where a bug results in a user-specified token not being passed in - which would fail unsafely by allowing any token, if conditional logic were used. */
return Promise.try(() => {
if (token != null) {
return this.byDeviceId({ deviceId });
} else {
throw new errors.MissingAccessToken("No access token was specified");
}
}).then((device) => {
if (tokensMatch(token, device.token)) {
return device;
} else {
throw new errors.InvalidAccessToken("Specified token was invalid");
}
});
},
updateToken: function ({ userId, deviceId, token }, tx = knex) {
return Promise.try(() => {
return tx("devices")
.update({ token: token })
.where({
device_id: deviceId,
user_id: userId
})
.returning("*");
}).then((results) => {
if (results.length > 0) {
return results[0];
} else {
throw new errors.NotFound("Specified Device ID does not exist");
}
});
},
createDeviceSession: function ({ name, deviceId, userId }, _tx = knex) {
return Promise.try(() => {
let token = nanoid();
return Promise.try(() => {
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 */
});
});
}
});
};

@ -42,8 +42,9 @@ let ExternalIdentifierExists = ResourceExists.extend("ExternalIdentifierExists",
let RequestTooLarge = HttpError.extend("RequestTooLarge", { statusCode: 413, errorCode: "M_TOO_LARGE" });
/*** HTTP 422: Unprocessable Entity (Invalid Payload) ***/
/* NOTE: Currently set to 400 instead, because that seems to be what the specification requires, even though 422 would be more appropriate. */
let InvalidData = HttpError.extend("InvalidData", { statusCode: 422 });
let InvalidData = HttpError.extend("InvalidData", { statusCode: /*422*/ 400 });
let InvalidUsername = InvalidData.extend("InvalidUsername", { errorCode: "M_INVALID_USERNAME" });
let InvalidRoomVersion = InvalidData.extend("InvalidRoomVersion", { errorCode: "M_UNSUPPORTED_ROOM_VERSION" });
let InvalidRoomState = InvalidData.extend("InvalidRoomState", { errorCode: "M_INVALID_ROOM_STATE" });
@ -52,6 +53,7 @@ let InvalidExternalIdentifier = InvalidData.extend("InvalidExternalIdentifier",
let InvalidParameter = InvalidData.extend("InvalidParameter", { errorCode: "M_INVALID_PARAM" });
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" });
/*** HTTP 429: Too Many Requests ***/
@ -61,11 +63,12 @@ let TooManyRequests = HttpError.extend("TooManyRequests", { statusCode: 429, err
let InternalServerError = HttpError.extend("InternalServerError", { statusCode: 500 });
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
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
};

@ -0,0 +1,9 @@
"use strict";
module.exports = function extractLocalPart(id) {
if (id.includes(":")) {
return id.split(":")[0];
} else {
return id;
}
};

@ -6,22 +6,41 @@ const bodyParser = require("body-parser");
const cors = require("cors");
const url = require("url");
const debugMiddleware = require("./src/debug-middleware");
const accessTokenMiddleware = require("./src/middlewares/access-token");
const debugMiddleware = require("./debug-middleware");
const accessTokenMiddleware = require("./middlewares/access-token");
const clientApiRouter = require("./src/routers/client-api");
const clientApiRouter = require("./routers/client-api");
const configuration = require("./config.json");
const knex = require("knex")(require("../knexfile"));
const configuration = require("../config.json");
function fullyQualify(prefix, value) {
if (value.includes(":")) {
return value;
} else {
return `${prefix}${value}:${configuration.hostname}`;
}
}
let state = {
configuration, knex,
fullyQualifyUser: fullyQualify.bind(null, "@"),
fullyQualifyRoom: fullyQualify.bind(null, "!"),
fullyQualifyRoomAlias: fullyQualify.bind(null, "#"),
fullyQualifyEvent: fullyQualify.bind(null, "$"),
fullyQualifyGroup: fullyQualify.bind(null, "+"),
};
state.db = {
devices: require("./db/devices")(state),
accounts: require("./db/accounts")(state)
};
let baseUrl = url.format({
protocol: (configuration.tls) ? "https" : "http",
host: configuration.hostname
});
let state = {
configuration
};
let app = express();
let router = expressPromiseRouter();

@ -0,0 +1,15 @@
"use strict";
module.exports = function matchValue(value, arms) {
for (let [candidateValue, arm] of Object.entries(arms)) {
if (candidateValue !== "_" && value === candidateValue) {
return arm();
}
}
if (arms._ != null) {
return arms._();
} else {
throw new Error("No matching arm found");
}
};

@ -0,0 +1,20 @@
"use strict";
function applyNormalizations(payload, normalizations) {
return normalizations.reduce((current, normalization) => {
let changes = normalization(current);
if (changes == null) {
return current;
} else {
return Object.assign(current, changes);
}
}, Object.assign({}, payload));
}
module.exports = function createNormalizationMiddleware(normalizations) {
return function normalizePayload(req, _res, next) {
req.body = applyNormalizations(req.body, normalizations);
next();
}
};

@ -1,11 +1,14 @@
"use strict";
const errors = require("../errors");
const Promise = require("bluebird");
module.exports = function requireAccessToken(req, res, next) {
if (req.matrixAccessToken == null) {
throw new errors.UnauthorizedError("An access token is required, but none was provided", {
errorCode: "M_MISSING_TOKEN"
module.exports = function ({ db }) {
return function requireAccessToken(req, _res, next) {
return Promise.try(() => {
return db.devices.byToken(req.matrixAccessToken);
}).then((device) => {
req.device = device;
next();
});
}
};
};
};

@ -0,0 +1,36 @@
"use strict";
// const Joi = require("@hapi/joi");
const errors = require("../errors");
const {validate, ValidationError} = require("../validate");
module.exports = function createPayloadValidator(validator) {
return function validatePayload(req, _res, next) {
try {
validate(validator, req.body);
} catch (error) {
if (error instanceof ValidationError) {
console.log(require("util").inspect(error, {colors: true, depth: null}));
throw new errors.MissingParameter("Something in the validation was wrong");
} else {
throw error;
}
}
next();
// let result = Joi.validate(req.body, schema);
// if (result.error != null) {
// /* FIXME: Actually produce meaningful output based on the Joi output */
// console.log(require("util").inspect(result.error, {colors: true, depth: null}));
// throw new errors.MissingParameter("Something in the validation was wrong");
// } else {
// next();
// }
}
};

@ -0,0 +1,14 @@
"use strict";
const nanoidGenerate = require("nanoid/generate");
const uppercaseAlphabet = require("nanoid-dictionary/uppercase");
module.exports = function processDeviceId(deviceId) {
/* TODO: Add validation here, once it becomes clear what the formatting requirements for a Device ID are */
if (deviceId != null && deviceId.length > 0) {
return deviceId;
} else {
return nanoidGenerate(uppercaseAlphabet, 16);
}
};

@ -0,0 +1,27 @@
"use strict";
const expressPromiseRouter = require("express-promise-router");
module.exports = function(state) {
let {db} = state;
let router = expressPromiseRouter();
router.get("/r0/account/3pid", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#get-matrix-client-r0-account-3pid
// Gets a list of the third party identifiers that the homeserver has associated with the user's account. This is not the same as the list of third party identifiers bound to the user's Matrix ID in identity servers.
});
router.post("/r0/account/3pid", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-3pid
// Adds contact information to the user's account.
});
router.post("/r0/account/3pid/delete", (req, res) => {
// https://matrix.org/docs/spec/client_server/r0.4.0.html#post-matrix-client-r0-account-3pid-delete
// Removes a third party identifier from the user's account. This might not cause an unbind of the identifier from the identity server.
});
// MARKER: https://matrix.org/docs/spec/client_server/r0.4.0.html#id231
return router;
}

@ -0,0 +1,221 @@
"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;
}

@ -1,10 +1,16 @@
"use strict";
const expressPromiseRouter = require("express-promise-router");
module.exports = function(state) {
let router = require("express-promise-router")();
const requireAccessToken = require("../../middlewares/require-access-token")(state);
const authenticationRouter = require("./authentication")(state);
const accountRouter = require("./account")(state);
let router = expressPromiseRouter();
router.get("/versions", (req, res) => {
/* TODO: Support more specification versions? */
/* FIXME: Support more specification versions? */
res.json({
versions: [
"r0.4.0"
@ -12,5 +18,13 @@ module.exports = function(state) {
});
});
/* Various routes in the authentication router are freely accessible, so access token requirements are implemented there on a per-route basis. */
router.use(authenticationRouter);
/* From this point onwards, every route requires an access token. */
router.use(requireAccessToken);
router.use(accountRouter);
return router;
}

@ -0,0 +1,119 @@
"use strict";
const errorChain = require("error-chain");
let ValidationError = errorChain("ValidationError");
function isPresent(value) {
if (value == null) {
throw new ValidationError("Required value is missing", { errorType: "isPresent" });
}
}
function isString(value) {
if (value != null && typeof value !== "string") {
throw new ValidationError("Value is not a string", { errorType: "isString" });
}
}
function isOneOf(...acceptableValues) {
let valueSet = new Set(acceptableValues);
return function (value) {
if (value != null && !valueSet.has(value)) {
throw new ValidationError("Value is not one of the specified acceptable values", {
errorType: "isOneOf",
acceptableValues: acceptableValues
});
}
};
}
/* NOTE: Extremely sketchy way of tracking validation state via a module-global object, but we can get away with this because validation is completely synchronous */
let context;
function assertProperties(object, properties) {
for (let [property, assertions] of Object.entries(properties)) {
context.path.push(property);
let value = object[property];
try {
for (let assertion of assertions) {
assertion(value);
}
} catch (error) {
if (error.input == null) {
error.input = value;
}
throw error;
}
context.path.pop();
}
}
function arrayOf(assertions) {
return function (value) {
if (value != null) {
if (!Array.isArray(value)) {
throw new ValidationError("Value is not an array", { errorType: "arrayOf" });
} else {
for (let [i, item] of value.entries()) {
context.path.push(i);
try {
for (let assertion of assertions) {
assertion(item);
}
} catch (error) {
if (error.input == null) {
error.input = value;
}
throw error;
}
context.path.pop();
}
}
}
};
}
function validate(validator, value) {
context = {
path: []
};
try {
validator(value);
} catch (error) {
if (error.path == null) {
error.path = context.path;
}
throw error;
}
context = undefined;
}
module.exports = {
validate, assertProperties, ValidationError,
isPresent, isString, isOneOf, arrayOf
};
// try {
// validate(validateLoginPayload, {
// type: 'm.login.password',
// password: 'test',
// identifier: { type: 'm.id.user', user: 'joepie91' },
// initial_device_display_name: 'http://localhost:3001/index.html via Firefox on Linux',
// user: 'joepie91',
// arrayStuff: [ 42 ]
// });
// } catch (error) {
// console.log(require("util").inspect(error, {colors: true, depth: null}));
// }

@ -146,6 +146,11 @@ atob@^2.1.1:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==
awesome-phonenumber@^2.9.0:
version "2.9.0"
resolved "https://registry.yarnpkg.com/awesome-phonenumber/-/awesome-phonenumber-2.9.0.tgz#8b3a0eea4ab5f508876c0061c45ab655559068cb"
integrity sha512-iKaiK6BXxpd9mpFgu1XkcnT3TffaDYT+E76iAaKGsgsI/zLObi0c3FFbmcl7jgmRAb7ldPTo0rWYROUkIBzP1Q==
balanced-match@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767"
@ -164,6 +169,11 @@ base@^0.11.1:
mixin-deep "^1.2.0"
pascalcase "^0.1.1"
bluebird@^2.6.4:
version "2.11.0"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
integrity sha1-U0uQM8AiyVecVro7Plpcqvu2UOE=
bluebird@^3.5.4:
version "3.5.4"
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.4.tgz#d6cc661595de30d5b3af5fcedd3c0b3ef6ec5714"
@ -374,6 +384,11 @@ cors@^2.8.5:
object-assign "^4"
vary "^1"
create-error@^0.3.1:
version "0.3.1"
resolved "https://registry.yarnpkg.com/create-error/-/create-error-0.3.1.tgz#69810245a629e654432bf04377360003a5351a23"
integrity sha1-aYECRaYp5lRDK/BDdzYAA6U1GiM=
cross-spawn@^6.0.5:
version "6.0.5"
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4"
@ -385,6 +400,14 @@ cross-spawn@^6.0.5:
shebang-command "^1.2.0"
which "^1.2.9"
database-error@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/database-error/-/database-error-2.0.1.tgz#d91745b01281caa73a5603f7178ddf694b8d18ba"
integrity sha1-2RdFsBKByqc6VgP3F43faUuNGLo=
dependencies:
create-error "^0.3.1"
pg-error-codes "^1.0.0"
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
@ -489,6 +512,11 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
errors@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/errors/-/errors-0.2.0.tgz#0f51e889daa3e11b19e7186d11f104aa66eb2403"
integrity sha1-D1Hoidqj4RsZ5xhtEfEEqmbrJAM=
es-abstract@^1.11.0, es-abstract@^1.7.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@ -1492,6 +1520,21 @@ mute-stream@0.0.7:
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
nan@^2.0.8:
version "2.13.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.13.2.tgz#f51dc7ae66ba7d5d55e1e6d4d8092e802c9aefe7"
integrity sha512-TghvYc72wlMGMVMluVo9WRJc0mB8KxxF/gZ4YYFy7V2ZQX9l7rgbPg7vjS9mt6U5HXODVFVI2bOduCzwOMv/lw==
nanoid-dictionary@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/nanoid-dictionary/-/nanoid-dictionary-2.0.0.tgz#4e04562622db78171dda7aed0dd808071c7868ac"
integrity sha512-mjMNB0yg2lH6stvxeZs2sBFhwhMwoYdbevPT5DYMxk8iDTI3vAZ6YAvHm+uBFT4VvGITpPpBX1U0sWVRTlPzOA==
nanoid@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.0.1.tgz#deb55cac196e3f138071911dabbc3726eb048864"
integrity sha512-k1u2uemjIGsn25zmujKnotgniC/gxQ9sdegdezeDiKdkDW56THUMqlz3urndKCXJxA6yPzSZbXx/QCMe/pxqsA==
nanomatch@^1.2.9:
version "1.2.13"
resolved "https://registry.yarnpkg.com/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119"
@ -1711,6 +1754,11 @@ pg-connection-string@2.0.0:
resolved "https://registry.yarnpkg.com/pg-connection-string/-/pg-connection-string-2.0.0.tgz#3eefe5997e06d94821e4d502e42b6a1c73f8df82"
integrity sha1-Pu/lmX4G2Ugh5NUC5CtqHHP434I=
pg-error-codes@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/pg-error-codes/-/pg-error-codes-1.1.0.tgz#6d331d8797954ad55b4c3b631eccc5ca9c8ed563"
integrity sha512-WUN3AlYo7927nyvMEtCXevLRE23E2N3yx8nTb5WWABneG59FEDCzOms+JGki/UvQ6iMsbOZTm32WVp/4ef19eQ==
pg-int8@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/pg-int8/-/pg-int8-1.0.1.tgz#943bd463bf5b71b4170115f80f8efc9a0c0eb78c"
@ -1962,6 +2010,22 @@ safe-regex@^1.1.0:
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
scrypt-for-humans@^2.0.5:
version "2.0.5"
resolved "https://registry.yarnpkg.com/scrypt-for-humans/-/scrypt-for-humans-2.0.5.tgz#a5c5dceffe20d62cbb4e972054f63cac65a9926e"
integrity sha1-pcXc7/4g1iy7TpcgVPY8rGWpkm4=
dependencies:
bluebird "^2.6.4"
errors "^0.2.0"
scrypt "^6.0.1"
scrypt@^6.0.1:
version "6.0.3"
resolved "https://registry.yarnpkg.com/scrypt/-/scrypt-6.0.3.tgz#04e014a5682b53fa50c2d5cce167d719c06d870d"
integrity sha1-BOAUpWgrU/pQwtXM4WfXGcBthw0=
dependencies:
nan "^2.0.8"
semver@4.3.2:
version "4.3.2"
resolved "https://registry.yarnpkg.com/semver/-/semver-4.3.2.tgz#c7a07158a80bedd052355b770d82d6640f803be7"

Loading…
Cancel
Save