2021-01-21 21:06:52 +00:00
|
|
|
'use strict';
|
2011-08-03 19:31:25 +01:00
|
|
|
/**
|
2021-01-21 21:06:52 +00:00
|
|
|
* The Session Manager provides functions to manage session in the database,
|
|
|
|
* it only provides session management for sessions created by the API
|
2011-08-03 19:31:25 +01:00
|
|
|
*/
|
|
|
|
|
|
|
|
/*
|
2011-08-11 15:26:41 +01:00
|
|
|
* 2011 Peter 'Pita' Martischka (Primary Technology Ltd)
|
2011-08-03 19:31:25 +01:00
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*/
|
2012-02-28 21:19:10 +01:00
|
|
|
|
2021-01-21 21:06:52 +00:00
|
|
|
const CustomError = require('../utils/customError');
|
2020-09-10 12:47:59 -04:00
|
|
|
const promises = require('../utils/promises');
|
2020-11-23 13:24:19 -05:00
|
|
|
const randomString = require('../utils/randomstring');
|
|
|
|
const db = require('./DB');
|
|
|
|
const groupManager = require('./GroupManager');
|
|
|
|
const authorManager = require('./AuthorManager');
|
2019-02-08 23:20:57 +01:00
|
|
|
|
2020-09-10 12:47:59 -04:00
|
|
|
/**
|
|
|
|
* Finds the author ID for a session with matching ID and group.
|
|
|
|
*
|
|
|
|
* @param groupID identifies the group the session is bound to.
|
|
|
|
* @param sessionCookie contains a comma-separated list of IDs identifying the sessions to search.
|
|
|
|
* @return If there is a session that is not expired, has an ID matching one of the session IDs in
|
|
|
|
* sessionCookie, and is bound to a group with the given ID, then this returns the author ID
|
|
|
|
* bound to the session. Otherwise, returns undefined.
|
|
|
|
*/
|
|
|
|
exports.findAuthorID = async (groupID, sessionCookie) => {
|
|
|
|
if (!sessionCookie) return undefined;
|
|
|
|
/*
|
|
|
|
* Sometimes, RFC 6265-compliant web servers may send back a cookie whose
|
|
|
|
* value is enclosed in double quotes, such as:
|
|
|
|
*
|
2021-01-21 21:06:52 +00:00
|
|
|
* Set-Cookie: sessionCookie="s.37cf5299fbf981e14121fba3a588c02b,
|
|
|
|
* s.2b21517bf50729d8130ab85736a11346"; Version=1; Path=/; Domain=localhost; Discard
|
2020-09-10 12:47:59 -04:00
|
|
|
*
|
|
|
|
* Where the double quotes at the start and the end of the header value are
|
|
|
|
* just delimiters. This is perfectly legal: Etherpad parsing logic should
|
|
|
|
* cope with that, and remove the quotes early in the request phase.
|
|
|
|
*
|
|
|
|
* Somehow, this does not happen, and in such cases the actual value that
|
|
|
|
* sessionCookie ends up having is:
|
|
|
|
*
|
|
|
|
* sessionCookie = '"s.37cf5299fbf981e14121fba3a588c02b,s.2b21517bf50729d8130ab85736a11346"'
|
|
|
|
*
|
|
|
|
* As quick measure, let's strip the double quotes (when present).
|
|
|
|
* Note that here we are being minimal, limiting ourselves to just removing
|
|
|
|
* quotes at the start and the end of the string.
|
|
|
|
*
|
|
|
|
* Fixes #3819.
|
|
|
|
* Also, see #3820.
|
|
|
|
*/
|
|
|
|
const sessionIDs = sessionCookie.replace(/^"|"$/g, '').split(',');
|
|
|
|
const sessionInfoPromises = sessionIDs.map(async (id) => {
|
|
|
|
try {
|
|
|
|
return await exports.getSessionInfo(id);
|
|
|
|
} catch (err) {
|
|
|
|
if (err.message === 'sessionID does not exist') {
|
|
|
|
console.debug(`SessionManager getAuthorID: no session exists with ID ${id}`);
|
|
|
|
} else {
|
|
|
|
throw err;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
const now = Math.floor(Date.now() / 1000);
|
2020-09-19 15:51:55 -04:00
|
|
|
const isMatch = (si) => (si != null && si.groupID === groupID && now < si.validUntil);
|
2020-09-10 12:47:59 -04:00
|
|
|
const sessionInfo = await promises.firstSatisfies(sessionInfoPromises, isMatch);
|
|
|
|
if (sessionInfo == null) return undefined;
|
|
|
|
return sessionInfo.authorID;
|
|
|
|
};
|
|
|
|
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.doesSessionExist = async (sessionID) => {
|
2020-11-23 13:24:19 -05:00
|
|
|
// check if the database entry of this session exists
|
|
|
|
const session = await db.get(`session:${sessionID}`);
|
2021-01-21 21:06:52 +00:00
|
|
|
return (session != null);
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2019-02-08 23:20:57 +01:00
|
|
|
|
2011-08-09 16:45:49 +01:00
|
|
|
/**
|
|
|
|
* Creates a new session between an author and a group
|
|
|
|
*/
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.createSession = async (groupID, authorID, validUntil) => {
|
2019-01-25 14:53:24 +00:00
|
|
|
// check if the group exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const groupExists = await groupManager.doesGroupExist(groupID);
|
2019-01-25 14:53:24 +00:00
|
|
|
if (!groupExists) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('groupID does not exist', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if the author exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const authorExists = await authorManager.doesAuthorExist(authorID);
|
2019-01-25 14:53:24 +00:00
|
|
|
if (!authorExists) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('authorID does not exist', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// try to parse validUntil if it's not a number
|
2020-11-23 13:24:19 -05:00
|
|
|
if (typeof validUntil !== 'number') {
|
2019-01-25 14:53:24 +00:00
|
|
|
validUntil = parseInt(validUntil);
|
|
|
|
}
|
|
|
|
|
|
|
|
// check it's a valid number
|
|
|
|
if (isNaN(validUntil)) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('validUntil is not a number', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ensure this is not a negative number
|
|
|
|
if (validUntil < 0) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('validUntil is a negative number', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// ensure this is not a float value
|
2021-01-21 21:06:52 +00:00
|
|
|
if (!isInt(validUntil)) {
|
|
|
|
throw new CustomError('validUntil is a float value', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// check if validUntil is in the future
|
|
|
|
if (validUntil < Math.floor(Date.now() / 1000)) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('validUntil is in the past', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// generate sessionID
|
2020-11-23 13:24:19 -05:00
|
|
|
const sessionID = `s.${randomString(16)}`;
|
2019-01-25 14:53:24 +00:00
|
|
|
|
|
|
|
// set the session into the database
|
2020-11-23 13:24:19 -05:00
|
|
|
await db.set(`session:${sessionID}`, {groupID, authorID, validUntil});
|
2019-01-25 14:53:24 +00:00
|
|
|
|
2021-11-12 21:26:01 -05:00
|
|
|
// Add the session ID to the group2sessions and author2sessions records after creating the session
|
|
|
|
// so that the state is consistent.
|
|
|
|
await Promise.all([
|
|
|
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
|
|
|
// property, and writes the result.
|
|
|
|
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], 1),
|
|
|
|
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], 1),
|
|
|
|
]);
|
2019-01-25 14:53:24 +00:00
|
|
|
|
2020-11-23 13:24:19 -05:00
|
|
|
return {sessionID};
|
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.getSessionInfo = async (sessionID) => {
|
2019-02-08 23:20:57 +01:00
|
|
|
// check if the database entry of this session exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const session = await db.get(`session:${sessionID}`);
|
2019-01-28 14:44:36 +00:00
|
|
|
|
|
|
|
if (session == null) {
|
|
|
|
// session does not exist
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('sessionID does not exist', 'apierror');
|
2019-01-28 14:44:36 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// everything is fine, return the sessioninfos
|
|
|
|
return session;
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes a session
|
|
|
|
*/
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.deleteSession = async (sessionID) => {
|
2019-01-25 14:53:24 +00:00
|
|
|
// ensure that the session exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const session = await db.get(`session:${sessionID}`);
|
2019-01-25 14:53:24 +00:00
|
|
|
if (session == null) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('sessionID does not exist', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// everything is fine, use the sessioninfos
|
2020-11-23 13:24:19 -05:00
|
|
|
const groupID = session.groupID;
|
|
|
|
const authorID = session.authorID;
|
2019-01-25 14:53:24 +00:00
|
|
|
|
2021-11-12 21:26:01 -05:00
|
|
|
await Promise.all([
|
|
|
|
// UeberDB's setSub() method atomically reads the record, updates the appropriate (sub)object
|
|
|
|
// property, and writes the result. Setting a property to `undefined` deletes that property
|
|
|
|
// (JSON.stringify() ignores such properties).
|
|
|
|
db.setSub(`group2sessions:${groupID}`, ['sessionIDs', sessionID], undefined),
|
|
|
|
db.setSub(`author2sessions:${authorID}`, ['sessionIDs', sessionID], undefined),
|
|
|
|
]);
|
2021-11-12 23:16:20 +01:00
|
|
|
|
2021-11-12 21:26:01 -05:00
|
|
|
// Delete the session record after updating group2sessions and author2sessions so that the state
|
|
|
|
// is consistent.
|
2021-11-12 23:16:20 +01:00
|
|
|
await db.remove(`session:${sessionID}`);
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.listSessionsOfGroup = async (groupID) => {
|
2019-01-25 14:53:24 +00:00
|
|
|
// check that the group exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const exists = await groupManager.doesGroupExist(groupID);
|
2019-01-25 14:53:24 +00:00
|
|
|
if (!exists) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('groupID does not exist', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
2020-11-23 13:24:19 -05:00
|
|
|
const sessions = await listSessionsWithDBKey(`group2sessions:${groupID}`);
|
2019-01-25 14:53:24 +00:00
|
|
|
return sessions;
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
2021-01-21 21:06:52 +00:00
|
|
|
exports.listSessionsOfAuthor = async (authorID) => {
|
2019-01-25 14:53:24 +00:00
|
|
|
// check that the author exists
|
2020-11-23 13:24:19 -05:00
|
|
|
const exists = await authorManager.doesAuthorExist(authorID);
|
2019-01-25 14:53:24 +00:00
|
|
|
if (!exists) {
|
2021-01-21 21:06:52 +00:00
|
|
|
throw new CustomError('authorID does not exist', 'apierror');
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
2020-11-23 13:24:19 -05:00
|
|
|
const sessions = await listSessionsWithDBKey(`author2sessions:${authorID}`);
|
2019-01-25 14:53:24 +00:00
|
|
|
return sessions;
|
2020-11-23 13:24:19 -05:00
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
2019-02-08 23:20:57 +01:00
|
|
|
// this function is basically the code listSessionsOfAuthor and listSessionsOfGroup has in common
|
2019-01-25 14:53:24 +00:00
|
|
|
// required to return null rather than an empty object if there are none
|
2021-01-21 21:06:52 +00:00
|
|
|
const listSessionsWithDBKey = async (dbkey) => {
|
2019-01-25 14:53:24 +00:00
|
|
|
// get the group2sessions entry
|
2020-11-23 13:24:19 -05:00
|
|
|
const sessionObject = await db.get(dbkey);
|
|
|
|
const sessions = sessionObject ? sessionObject.sessionIDs : null;
|
2019-01-25 14:53:24 +00:00
|
|
|
|
|
|
|
// iterate through the sessions and get the sessioninfos
|
2021-05-11 16:56:06 -04:00
|
|
|
for (const sessionID of Object.keys(sessions || {})) {
|
2019-01-25 14:53:24 +00:00
|
|
|
try {
|
2020-11-23 13:24:19 -05:00
|
|
|
const sessionInfo = await exports.getSessionInfo(sessionID);
|
2019-01-25 14:53:24 +00:00
|
|
|
sessions[sessionID] = sessionInfo;
|
|
|
|
} catch (err) {
|
2021-11-12 22:58:49 +01:00
|
|
|
if (err.name === 'apierror') {
|
2019-01-25 14:53:24 +00:00
|
|
|
console.warn(`Found bad session ${sessionID} in ${dbkey}`);
|
|
|
|
sessions[sessionID] = null;
|
|
|
|
} else {
|
|
|
|
throw err;
|
2011-08-09 20:14:32 +01:00
|
|
|
}
|
|
|
|
}
|
2019-01-25 14:53:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return sessions;
|
2021-01-21 21:06:52 +00:00
|
|
|
};
|
2011-08-09 16:45:49 +01:00
|
|
|
|
2019-02-08 23:20:57 +01:00
|
|
|
// checks if a number is an int
|
2021-01-21 21:06:52 +00:00
|
|
|
const isInt = (value) => (parseFloat(value) === parseInt(value)) && !isNaN(value);
|