diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index bb8de749b..000000000 --- a/lib/api.js +++ /dev/null @@ -1,234 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const WebSocketServer = require('ws').Server; -const NetfluxSrv = require('chainpad-server'); -const Decrees = require("./decrees"); - -const nThen = require("nthen"); -const Fs = require("fs"); -const Fse = require("fs-extra"); -const Path = require("path"); -const Nacl = require("tweetnacl/nacl-fast"); -const Hash = require('./common-hash'); -const Util = require('./common-util'); - -module.exports.create = function (Env) { - var log = Env.Log; - -nThen(function (w) { - Decrees.load(Env, w(function (err) { - Env.flushCache(); - if (err) { - log.error('DECREES_LOADING', { - error: err.code || err, - message: err.message, - }); - console.error(err); - } - })); -}).nThen(function () { - if (Env.proofsMigrated) { return; } - const { Worker } = require('node:worker_threads'); - const Admin = require("./commands/admin-rpc"); - - const worker = new Worker('./scripts/migrations/migrate-blob-proofs.js'); - - worker.on('message', message => { - if (message === 'READY') { - log.info('BLOB_PROOFS_MIGRATION'); - return void worker.postMessage({ - start: 1, - }); - } - if (message === 'MIGRATED') { - return void log.info('BLOB_PROOFS_DELETION'); - } - if (message === 'CLEANED') { - log.info('BLOB_PROOFS_MIGRATED'); - Admin.sendDecree(Env, null, function (err) { - if (err) { return void log.error('BLOB_PROOF', err); } - Env.flushCache(); - }, ['PROOFS_MIGRATED', ['PROOFS_MIGRATED', 1]], 'server'); - } - }); -}).nThen(function (w) { - let admins = Env.admins || []; - - // If we don't have any admin on this instance, print an onboarding link - if (Array.isArray(admins) && admins.length) { return; } - let token = Env.installToken; - let printLink = () => { - let url = `${Env.httpUnsafeOrigin}/install/#${token}`; - console.log('============================='); - console.log('Create your first admin account and customize your instance by visiting'); - console.log(url); - console.log('============================='); - - }; - - // If we already have a token, print it - if (token) { return void printLink(); } - - // Otherwise create a new token - let decreeName = Path.join(Env.paths.decree, 'decree.ndjson'); - token = Hash.createChannelId() + Hash.createChannelId(); - let decree = ["ADD_INSTALL_TOKEN",[token],"",+new Date()]; - Fs.appendFile(decreeName, JSON.stringify(decree) + '\n', w(function (err) { - if (err) { console.log(err); return; } - Env.installToken = token; - Env.envUpdated.fire(); - printLink(); - })); -}).nThen(function () { - if (!Env.admins.length) { - Env.Log.info('NO_ADMIN_CONFIGURED', { - message: `Your instance is not correctly configured for production usage. Review its checkup page for more information.`, - details: new URL('/checkup/', Env.httpUnsafeOrigin).href, - }); - } -}).nThen(function (w) { - // we assume the server has generated a secret used to validate JWT tokens - if (typeof(Env.bearerSecret) === 'string') { return; } - // if one does not exist, then create one and remember it - // 256 bits - var bearerSecret = Util.encodeBase64(Nacl.randomBytes(32)); - Env.bearerSecret = bearerSecret; - Env.Log.info("GENERATING_BEARER_SECRET", {}); - Decrees.write(Env, [ - 'SET_BEARER_SECRET', - [bearerSecret], - 'INTERNAL', - +new Date() - ], w(function (err) { - if (err) { throw err; } - })); -}).nThen(function (w) { - Fse.mkdirp(Env.paths.block, w(function (err) { - if (err) { - log.error("BLOCK_FOLDER_CREATE_FAILED", err); - } - })); -}).nThen(function (w) { - var fullPath = Path.join(Env.paths.block, 'placeholder.txt'); - Fs.writeFile(fullPath, 'PLACEHOLDER\n', w(function (err) { - if (err) { - log.error('BLOCK_PLACEHOLDER_CREATE_FAILED', err); - } - })); -}).nThen(function () { - // asynchronously create a historyKeeper and RPC together - require('./historyKeeper.js').create(Env, function (err, historyKeeper) { - if (err) { throw err; } - - - var noop = function () {}; - - var special_errors = {}; - ['EPIPE', 'ECONNRESET'].forEach(function (k) { special_errors[k] = noop; }); - special_errors.NF_ENOENT = function (error, label, info) { - delete info.stack; - log.error(label, { - info: info, - }); - }; - - // spawn ws server and attach netflux event handlers - let Server = Env.Server = NetfluxSrv.create(new WebSocketServer({ server: Env.httpServer})) - .on('channelClose', historyKeeper.channelClose) - .on('channelMessage', historyKeeper.channelMessage) - .on('channelOpen', historyKeeper.channelOpen) - .on('sessionClose', historyKeeper.sessionClose) - .on('sessionOpen', historyKeeper.sessionOpen) - .on('error', function (error, label, info) { - if (!error) { return; } - var code = error && (error.code || error.message); - if (code) { - /* EPIPE,ECONNERESET, NF_ENOENT */ - if (typeof(special_errors[code]) === 'function') { - return void special_errors[code](error, label, info); - } - } - - /* labels: - SEND_MESSAGE_FAIL, SEND_MESSAGE_FAIL_2, FAIL_TO_DISCONNECT, - FAIL_TO_TERMINATE, HANDLE_CHANNEL_LEAVE, NETFLUX_BAD_MESSAGE, - NETFLUX_WEBSOCKET_ERROR - */ - log.error(label, { - code: error.code, - message: error.message, - stack: error.stack, - info: info, - }); - }) - .register(historyKeeper.id, historyKeeper.directMessage); - // Store max active WS during the last day (reset when sending ping if enabled) - setInterval(() => { - try { - // Concurrent usage data - let oldWs = Env.maxConcurrentWs || 0; - let oldUniqueWs = Env.maxConcurrentUniqueWs || 0; - let oldChans = Env.maxActiveChannels || 0; - let oldUsers = Env.maxConcurrentRegUsers || 0; - let stats = Server.getSessionStats(); - let chans = Server.getActiveChannelCount(); - - const map = Env.netfluxUsers; - // Extract public key from each ws connection - let regKeys = Object.keys(map).map(id => { - return Object.keys(map[id] || {})[0]; - }).filter(Boolean); - // Convert to set to get only unique values - let regSet = new Set(regKeys); - const reg = regSet.size; - - Env.maxConcurrentWs = Math.max(oldWs, stats.total); - Env.maxConcurrentUniqueWs = Math.max(oldUniqueWs, stats.unique); - Env.maxConcurrentRegUsers = Math.max(oldUsers, reg); - Env.maxActiveChannels = Math.max(oldChans, chans); - } catch (e) {} - }, 10000); - // Clean up active registered users and channels (possible memory leak) - setInterval(() => { - try { - let users = Env.netfluxUsers || {}; - let online = Server.getOnlineUsers() || []; - let onlineSet = new Set(online); - let removed = 0; - Object.keys(users).forEach(id => { - if (!onlineSet.has(id)) { - delete users[id]; - removed++; - } - }); - if (removed) { - Env.Log.info("CLEANED_ACTIVE_USERS_MAP", {removed}); - } - } catch (e) {} - try { - let HK = require('./hk-utils'); - let chans = Env.channel_cache || {}; - let active = Server.getActiveChannels() || []; - let activeSet = new Set(active); - let removed = 0; - Object.keys(chans).forEach(id => { - if (!activeSet.has(id)) { - HK.dropChannel(Env, id); - removed++; - } - }); - if (Env.store) { - Env.store.closeInactiveChannels(activeSet); - } - if (removed) { - Env.Log.info("CLEANED_ACTIVE_CHANNELS_MAP", {removed}); - } - } catch (e) {} - }, 30000); - - }); -}); - -}; diff --git a/lib/archive-account.js b/lib/archive-account.js deleted file mode 100644 index 222b2421f..000000000 --- a/lib/archive-account.js +++ /dev/null @@ -1,356 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const nThen = require('nthen'); -const Pins = require('./pins'); -const Util = require("./common-util"); -const Store = require('./storage/file.js'); -const BlobStore = require("./storage/blob"); -const BlockStore = require("./storage/block"); -const Core = require("./commands/core"); -const Metadata = require("./commands/metadata"); -const Meta = require("./metadata"); -const Logger = require("./log"); -const plugins = require("./plugin-manager"); -const HK = require('./hk-util'); -const MFA = require("./storage/mfa"); - -let SSOUtils = plugins.SSO && plugins.SSO.utils; - -const Path = require("path"); -const Fse = require("fs-extra"); - -const { parentPort } = require('node:worker_threads'); - -const COMMANDS = {}; -let Log; - -const mkReportPath = function (Env, safeKey) { - return Path.join(Env.paths.archive, 'accounts', safeKey); -}; -const storeReport = (Env, report, cb) => { - let path = mkReportPath(Env, report.key); - let s_data; - try { - s_data = JSON.stringify(report); - Fse.outputFile(path, s_data, cb); - } catch (err) { - return void cb(err); - } -}; -const readReport = (Env, key, cb) => { - let path = mkReportPath(Env, key); - Fse.readJson(path, cb); -}; -const deleteReport = (Env, key, cb) => { - let path = mkReportPath(Env, key); - Fse.remove(path, cb); -}; - -const init = (cb) => { - const Environment = require("./env"); - const config = require('./load-config'); - const Env = Environment.create(config); - Env.computeMetadata = function (channel, cb) { - const ref = {}; - const lineHandler = Meta.createLineHandler(ref, (err) => { console.log(err); }); - - let f = Env.store.readChannelMetadata; - if (channel.length === HK.BLOB_ID_LENGTH) { - f = Env.blobStore.readMetadata; - } - - return void f(channel, lineHandler, function (err) { - if (err) { - // stream errors? - return void cb(err); - } - cb(void 0, ref.meta); - }); - }; - - nThen((waitFor) => { - Logger.create(config, waitFor(function (_) { - Log = Env.Log = _; - })); - Store.create(config, waitFor(function (err, _store) { - if (err) { - waitFor.abort(); - return void cb(err); - } - Env.store = _store; - })); - Store.create({ - filePath: config.pinPath, - archivePath: config.archivePath, - // archive pin logs to their own subpath - volumeId: 'pins', - }, waitFor(function (err, _) { - if (err) { - waitFor.abort(); - throw err; - } - Env.pinStore = _; - })); - BlobStore.create({ - blobPath: config.blobPath, - blobStagingPath: config.blobStagingPath, - archivePath: config.archivePath, - getSession: function () {}, - }, waitFor(function (err, blob) { - if (err) { - waitFor.abort(); - return void cb(err); - } - Env.blobStore = blob; - })); - }).nThen(() => { - cb(Env); - }); -}; - -COMMANDS.start = (edPublic, blockId, reason) => { - const safeKey = Util.escapeKeyCharacters(edPublic); - const archiveReason = { - code: 'MODERATION_ACCOUNT', - txt: reason - }; - - let ref = {}; - let blobsToArchive = []; - let channelsToArchive = []; - let deletedChannels = []; - let deletedBlobs = []; - let Env; - nThen((waitFor) => { - init(waitFor((_Env) => { - Env = _Env; - })); - }).nThen((waitFor) => { - let lineHandler = Pins.createLineHandler(ref, (err) => { console.log(err); }); - Env.pinStore.readMessagesBin(safeKey, 0, (msgObj, readMore) => { - lineHandler(msgObj.buff.toString('utf8')); - readMore(); - }, waitFor()); - }).nThen((waitFor) => { - Log.info('MODERATION_ACCOUNT_ARCHIVAL_START', edPublic, waitFor()); - var n = nThen; - Object.keys(ref.pins || {}).forEach((chanId) => { - n = n((w) => { - // Blobs - if (Env.blobStore.isFileId(chanId)) { - return Env.computeMetadata(chanId, w((e, md) => { - if (e || !md) { return; } - if (md && md.owners - && md.owners.includes(edPublic)) { - blobsToArchive.push(chanId); - } - })); - } - // Pads - Metadata.getMetadata(Env, chanId, w((err, metadata) => { - if (err) { return; } // Can't read metadata? Don't archive - if (!Core.hasOwners(metadata)) { return; } // No owner, don't archive - if (Core.isOwner(metadata, edPublic) && metadata.owners.length === 1) { - channelsToArchive.push(chanId); // Only owner: archive - } - })); - }).nThen; - }); - n(waitFor()); - }).nThen((waitFor) => { - Log.info('MODERATION_ACCOUNT_ARCHIVAL_LISTED', JSON.stringify({ - pads: channelsToArchive.length, - blobs: blobsToArchive.length - }), waitFor()); - - var n = nThen; - // Archive the pads - channelsToArchive.forEach((chanId) => { - n = n((w) => { - Env.store.archiveChannel(chanId, archiveReason, w(function (err) { - if (err) { - return Log.error('MODERATION_CHANNEL_ARCHIVAL_ERROR', { - error: err, - channel: chanId, - }, w()); - } - deletedChannels.push(chanId); - Log.info('MODERATION_CHANNEL_ARCHIVAL', chanId, w()); - })); - }).nThen; - }); - // Archive the blobs - blobsToArchive.forEach((blobId) => { - n = n((w) => { - Env.blobStore.archive.blob(blobId, archiveReason, w(function (err) { - if (err) { - return Log.error('MODERATION_BLOB_ARCHIVAL_ERROR', { - error: err, - item: blobId, - }, w()); - } - deletedBlobs.push(blobId); - Log.info('MODERATION_BLOB_ARCHIVAL', blobId, w()); - })); - }).nThen; - }); - n(waitFor(() => { - // Archive the pin log - Env.pinStore.archiveChannel(safeKey, undefined, waitFor(function (err) { - if (err) { - return Log.error('MODERATION_ACCOUNT_PIN_LOG', err, waitFor()); - } - Log.info('MODERATION_ACCOUNT_LOG', safeKey, waitFor()); - })); - blockId = blockId || ref.block; - if (!blockId) { return; } - BlockStore.archive(Env, blockId, archiveReason, waitFor(function (err) { - if (err) { - blockId = undefined; - return Log.error('MODERATION_ACCOUNT_BLOCK', err, waitFor()); - } - Log.info('MODERATION_ACCOUNT_BLOCK', safeKey, waitFor()); - })); - MFA.delete(Env, blockId, waitFor()); - if (!SSOUtils) { return; } - SSOUtils.deleteAccount(Env, blockId, waitFor((err) => { - if (err) { - return Log.error('MODERATION_ACCOUNT_BLOCK_SSO', err, waitFor()); - } - })); - })); - }).nThen((waitFor) => { - var report = { - key: safeKey, - channels: deletedChannels, - blobs: deletedBlobs, - blockId: blockId, - reason: reason - }; - storeReport(Env, report, waitFor((err) => { - if (err) { - return Log.error('MODERATION_ACCOUNT_REPORT', report, waitFor()); - } - })); - }).nThen(() => { - parentPort.postMessage(JSON.stringify(deletedChannels)); - process.exit(0); - }); -}; - - -COMMANDS.restore = (edPublic) => { - const safeKey = Util.escapeKeyCharacters(edPublic); - let pads, blobs; - let blockId; - let errors = []; - let Env; - nThen((waitFor) => { - init(waitFor((_Env) => { - Env = _Env; - })); - }).nThen((waitFor) => { - Log.info('MODERATION_ACCOUNT_RESTORE_START', edPublic, waitFor()); - readReport(Env, safeKey, waitFor((err, report) => { - if (err) { throw new Error(err); } - pads = report.channels; - blobs = report.blobs; - blockId = report.blockId; - })); - }).nThen((waitFor) => { - Log.info('MODERATION_ACCOUNT_RESTORE_LISTED', JSON.stringify({ - pads: pads.length, - blobs: blobs.length - }), waitFor()); - var n = nThen; - pads.forEach((chanId) => { - n = n((w) => { - Env.store.restoreArchivedChannel(chanId, w(function (err) { - if (err) { - errors.push(chanId); - return Log.error('MODERATION_CHANNEL_RESTORE_ERROR', { - error: err, - channel: chanId, - }, w()); - } - Log.info('MODERATION_CHANNEL_RESTORE', chanId, w()); - })); - }).nThen; - }); - blobs.forEach((blobId) => { - n = n((w) => { - Env.blobStore.restore.blob(blobId, w(function (err) { - if (err) { - errors.push(blobId); - return Log.error('MODERATION_BLOB_RESTORE_ERROR', { - error: err, - item: blobId, - }, w()); - } - Log.info('MODERATION_BLOB_RESTORE', blobId, w()); - })); - }).nThen; - }); - n(waitFor(() => { - // remove the pin logs of inactive accounts if inactive account removal is configured - Env.pinStore.restoreArchivedChannel(safeKey, waitFor(function (err) { - if (err) { - return Log.error('MODERATION_ACCOUNT_PIN_LOG_RESTORE', err, waitFor()); - } - Log.info('MODERATION_ACCOUNT_LOG_RESTORE', safeKey, waitFor()); - })); - if (!blockId) { return; } - BlockStore.restore(Env, blockId, waitFor(function (err) { - if (err) { - blockId = undefined; - return Log.error('MODERATION_ACCOUNT_BLOCK_RESTORE', err, waitFor()); - } - Log.info('MODERATION_ACCOUNT_BLOCK_RESTORE', safeKey, waitFor()); - })); - if (!SSOUtils) { return; } - SSOUtils.restoreAccount(Env, blockId, waitFor(function (err) { - if (err) { - return Log.error('MODERATION_ACCOUNT_BLOCK_RESTORE_SSO', err, waitFor()); - } - })); - })); - }).nThen((waitFor) => { - deleteReport(Env, safeKey, waitFor((err) => { - if (err) { - return Log.error('MODERATION_ACCOUNT_REPORT_DELETE', safeKey, waitFor()); - } - })); - }).nThen(() => { - parentPort.postMessage(JSON.stringify(errors)); - process.exit(0); - }); - -}; - -const getStatus = (Env, edPublic, cb) => { - const safeKey = Util.escapeKeyCharacters(edPublic); - readReport(Env, safeKey, (err, report) => { - if (err) { return void cb(err); } - cb(void 0, report); - }); -}; - -if (parentPort) { - parentPort.on('message', (message) => { - let parsed = message; //JSON.parse(message); - let command = parsed.command; - let content = parsed.content; - let block = parsed.block; - let reason = parsed.reason; - COMMANDS[command](content, block, reason); - }); - - parentPort.postMessage('READY'); -} else { - - module.exports = { - getStatus: getStatus - }; -} diff --git a/lib/batch-read.js b/lib/batch-read.js deleted file mode 100644 index 3e7a104c4..000000000 --- a/lib/batch-read.js +++ /dev/null @@ -1,68 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -/* - -## Purpose - -To avoid running expensive IO or computation concurrently. - -If the result of IO or computation is requested while an identical request -is already in progress, wait until the first one completes and provide its -result to every routine that requested it. - -Asynchrony is guaranteed. - -## Usage - -Provide: - -1. a named key for the computation or resource, -2. a callback to handle the result -3. an implementation which calls back with the result - -``` -var batch = Batch(); - -var read = function (path, cb) { - batch(path, cb, function (done) { - console.log("reading %s", path); - fs.readFile(path, 'utf8', done); - }); -}; - -read('./pewpew.txt', function (err, data) { - if (err) { return void console.error(err); } - console.log(data); -}); - -read('./pewpew.txt', function (err, data) { - if (err) { return void console.error(err); } - console.log(data); -}); -``` - -*/ - -module.exports = function (/* task */) { - var map = {}; - return function (id, cb, impl) { - if (typeof(cb) !== 'function' || typeof(impl) !== 'function') { - throw new Error("expected callback and implementation"); - } - if (map[id]) { return void map[id].push(cb); } - map[id] = [cb]; - impl(function () { - var args = Array.prototype.slice.call(arguments); - - //if (map[id] && map[id].length > 1) { console.log("BATCH-READ DID ITS JOB for [%s][%s]", task, id); } - setTimeout(function () { - map[id].forEach(function (h) { - h.apply(null, args); - }); - delete map[id]; - }); - }); - }; -}; diff --git a/lib/challenge-commands/base.js b/lib/challenge-commands/base.js deleted file mode 100644 index 0af6ead56..000000000 --- a/lib/challenge-commands/base.js +++ /dev/null @@ -1,113 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Block = require("../commands/block"); -const MFA = require("../storage/mfa"); -const Util = require("../common-util"); -const Sessions = require("../storage/sessions"); - -const Commands = module.exports; - -var isValidBlockId = Block.isValidBlockId; - -// Read the MFA settings for the given public key -const checkMFA = (Env, publicKey, cb) => { - // Success if we can't get the MFA settings - MFA.read(Env, publicKey, function (err, content) { - if (err) { - if (err.code !== "ENOENT") { - Env.Log.error('TOTP_VALIDATE_MFA_READ', { - error: err, - publicKey: publicKey, - }); - } - return void cb(); - } - - var parsed = Util.tryParse(content); - if (!parsed) { return void cb(); } - - cb("NOT_ALLOWED"); - }); -}; - -// Make sure the block is not protected by MFA but don't do anything else -const check = Commands.MFA_CHECK = function (Env, body, cb) { - var { publicKey } = body; - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - checkMFA(Env, publicKey, cb); -}; -check.complete = function (Env, body, cb) { cb(); }; - -// Write a login block IFF -// 1. You can sign for the block's public key -// 2. the block is not protected by MFA -// Note: the internal WRITE_LOGIN_BLOCK will check is you're allowed to create this block -const writeBlock = Commands.WRITE_BLOCK = function (Env, body, cb) { - const { publicKey, content } = body; - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); } - - // check MFA - checkMFA(Env, publicKey, cb); -}; - -writeBlock.complete = function (Env, body, cb) { - const { publicKey, content, session } = body; - Block.writeLoginBlock(Env, content, (err) => { - if (err) { return void cb(err); } - - if (!session) { return void cb(); } - - const proof = Util.tryParse(content.registrationProof); - const oldKey = proof && proof[0]; - Sessions.update(Env, publicKey, oldKey, session, "", cb); - }); -}; - -// Remove a login block IFF -// 1. You can sign for the block's public key -// 2. the block is not protected by MFA -const removeBlock = Commands.REMOVE_BLOCK = function (Env, body, cb) { - const { publicKey } = body; - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - - // check MFA - checkMFA(Env, publicKey, cb); -}; - -removeBlock.complete = function (Env, body, cb) { - const { publicKey, edPublic, reason } = body; - Block.removeLoginBlock(Env, publicKey, reason, edPublic, cb); -}; - -// Get an upload cookie -// Get a cookie allowing you to upload to the blobstage of your user -const uploadCookie = Commands.UPLOAD_COOKIE = function (Env, body, cb) { - const { publicKey } = body; - - // they must provide a valid public key - if (publicKey && typeof(publicKey) === "string" - && publicKey.length === 44) { - return cb(); - } - - cb("INVALID_KEY"); -}; - -uploadCookie.complete = function (Env, body, cb) { - const { publicKey } = body; - - const safeKey = Util.escapeKeyCharacters(publicKey); - Env.blobStore.uploadCookie(safeKey, (err, cookie) => { - if (err) { return void cb(err); } - cb(void 0, {cookie}); - }); -}; - - diff --git a/lib/challenge-commands/totp.js b/lib/challenge-commands/totp.js deleted file mode 100644 index a4ecfaa06..000000000 --- a/lib/challenge-commands/totp.js +++ /dev/null @@ -1,529 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const B32 = require("thirty-two"); -const OTP = require("notp"); -const nThen = require("nthen"); -const Util = require("../common-util"); - -const MFA = require("../storage/mfa"); -const Sessions = require("../storage/sessions"); -const BlockStore = require("../storage/block"); -const Block = require("../commands/block"); -const config = require("../load-config"); - -const Commands = module.exports; - -var isString = s => typeof(s) === 'string'; - -// basic definition of what we'll accept as an OTP code -// exactly six numerical digits -var isValidOTP = otp => { - return isString(otp) && - // in the future this could be updated to support 8 digits - otp.length === 6 && - // \D is non-digit characters, so this tests that it is exclusively numeric - !/\D/.test(otp); -}; - -// basic definition of what we'll accept as a recovery key -// 24 bytes encoded as b64 ==> 32 characters -var isValidRecoveryKey = otp => { - return isString(otp) && - // in the future this could be updated to support 8 digits - otp.length === 32 && - // \D is non-digit characters, so this tests that it is exclusively numeric - /[A-Za-z0-9+\/]{32}/.test(otp); -}; - -// we'll only allow users to set up multi-factor auth -// for keypairs they control which already have blocks -// this check doesn't confirm that their id is valid base64 -// any attempt relying on this should fail when we can't decode -// the id they provided. -var isValidBlockId = Block.isValidBlockId; - -// the base32 library can throw when decoding under various conditions. -// we have some basic requirements for the length of base32 as well, -// so we just do all the validation here. It either returns a buffer -// of length 20 or undefined, so the caller can just check whether it's -// falsey and otherwise assume it was well-formed -// Length === 20 comes from the recommendation of 160 bits of entropy -// in RFC4226 (https://www.rfc-editor.org/rfc/rfc4226#section-4) -var decode32 = S => { - let decoded; - try { - decoded = B32.decode(S); - } catch (err) { return; } - if (!(decoded instanceof Buffer) || decoded.length !== 20) { return; } - return decoded; -}; - - -// Allow user settings? -var EXPIRATION = (config.otpSessionExpiration || 7 * 24) * 3600 * 1000; - -// Create a session with a token for the given public key -const makeSession = (Env, publicKey, oldKey, ssoSession, cb) => { - const sessionId = ssoSession || Sessions.randomId(); - let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils; - - // For password change, we need to get the sso session associated to the old block key - // In other cases (login and totp_setup), the sso session is associated to the current block - oldKey = oldKey || publicKey; // use the current block if no old key - - let isUpdate = false; - nThen(function (w) { - if (!ssoSession || !SSOUtils) { return; } - // If we have an session token, confirm this is an sso account - SSOUtils.readBlock(Env, oldKey, w((err) => { - if (err === 'ENOENT') { return; } // No sso block, no need to update the session - if (err) { - w.abort(); - return void cb('TOTP_VALIDATE_READ_SSO'); - } - // We have an existing session for an SSO account: update the existing session - isUpdate = true; - })); - }).nThen(function (w) { - // store the token - let sessionData = { - mfa: { - type: 'otp', - exp: (+new Date()) + EXPIRATION - } - }; - var then = w(function (err) { - if (err) { - Env.Log.error("TOTP_VALIDATE_SESSION_WRITE", { - error: Util.serializeError(err), - publicKey: publicKey, - sessionId: sessionId, - }); - w.abort(); - return void cb("SESSION_WRITE_ERROR"); - } - // else continue - }); - if (isUpdate) { - Sessions.update(Env, publicKey, oldKey, sessionId, JSON.stringify(sessionData), then); - } else { - Sessions.write(Env, publicKey, sessionId, JSON.stringify(sessionData), then); - } - }).nThen(function () { - cb(void 0, { - bearer: sessionId, - }); - }); - -}; -// Read the MFA settings for the given public key -const readMFA = (Env, publicKey, cb) => { - // check that there is an MFA configuration for the given account - MFA.read(Env, publicKey, function (err, content) { - if (err) { - Env.Log.error('TOTP_VALIDATE_MFA_READ', { - error: err, - publicKey: publicKey, - }); - return void cb('NO_MFA_CONFIGURED'); - } - - var parsed = Util.tryParse(content); - if (!parsed) { return void cb("INVALID_CONFIGURATION"); } - cb(undefined, parsed); - }); -}; -// Check if an OTP code is valid against the provided secret -const checkCode = (Env, secret, code, publicKey, _cb) => { - const cb = Util.mkAsync(_cb); - - let decoded = decode32(secret); - if (!decoded) { - Env.Log.error("TOTP_VALIDATE_INVALID_SECRET", { - publicKey, // log the public key so the admin can investigate further - // don't log the problematic secret directly as - // logs are likely to be pasted in random places - }); - return void cb("E_INVALID_SECRET"); - } - - // validate the code - var validated = OTP.totp.verify(code, decoded, { - window: 1, - }); - - if (!validated) { - // I won't worry about logging these OTPs as they shouldn't leak any useful information - Env.Log.error("TOTP_VALIDATE_BAD_OTP", { - code, - }); - return void cb("INVALID_OTP"); - } - - // call back to indicate that their request was well-formed and valid - cb(); -}; - - -// This command allows clients to configure TOTP as a second factor protecting -// their login block IFF they: -// 1. provide a sufficiently strong TOTP secret -// 2. are able to produce a valid OTP code for that secret (indicating that their clock is sufficiently close to ours) -// 3. such a login block actually exists -// 4. are able to sign an arbitrary message for the login block's public key -// 5. have not already configured TOTP protection for this account -// (changing to a new secret can be done by disabling and re-enabling TOTP 2FA) -const TOTP_SETUP = Commands.TOTP_SETUP = function (Env, body, cb) { - const { publicKey, secret, code, contact } = body; - - - // the client MUST provide an OTP code of the expected format - // this doesn't check if it matches the secret and time, just that it's well-formed - if (!isValidOTP(code)) { return void cb("E_INVALID"); } - - // if they provide an (optional) point of contact as a recovery mechanism then it should be a string. - // the intent is to allow to specify some side channel for those who inevitably lock themselves out - // we should be able to use that to validate their identity. - // I don't want to assume email, but limiting its length to 254 (the maximum email length) seems fair. - if (contact && (!isString(contact) || contact.length > 254)) { return void cb("INVALID_CONTACT"); } - - // Check that the provided public key is the expected format for a block - if (!isValidBlockId(publicKey)) { - return void cb("INVALID_KEY"); - } - - // decode32 checks whether the secret decodes to a sufficiently long buffer - var decoded = decode32(secret); - if (!decoded) { return void cb('INVALID_SECRET'); } - - // Reject attempts to setup TOTP if a record of their preferences already exists - MFA.read(Env, publicKey, function (err) { - // There **should be** an error here, because anything else - // means that a record already exists - // This may need to be adjusted as other methods of MFA are added - if (!err) { return void cb("EEXISTS"); } - - // if no MFA settings exist then we expect ENOENT - // anything else indicates a problem and should result in rejection - if (err.code !== 'ENOENT') { return void cb(err); } - try { - // allow for 30s of clock drift in either direction - // returns an object ({ delta: 0 }) indicating the amount of clock drift - // if successful, otherwise `null` - var validated = OTP.totp.verify(code, decoded, { - window: 1, - }); - if (!validated) { return void cb("INVALID_OTP"); } - cb(); - } catch (err2) { - Env.Log.error('TOTP_SETUP_VERIFICATION_ERROR', { - error: err2, - }); - return void cb("INTERNAL_ERROR"); - } - }); -}; - -// The 'complete' step for TOTP_SETUP will only be called if the client -// passed earlier validation and successfully signed the server's challenge. -// There's still a little bit more to do and it could still fail. -TOTP_SETUP.complete = function (Env, body, cb) { - // the OTP code should have already been validated - var { publicKey, secret, contact, session } = body; - - // the device from which they configure MFA settings - // is assumed to be safe, so we'll respond with a JWT token - // the remainder of the setup is successfully completed. - // Otherwise they would have to reauthenticate. - // The session id is used as a reference to this particular session. - nThen(function (w) { - // confirm that the block exists - BlockStore.check(Env, publicKey, w(function (err) { - if (err) { - Env.Log.error("TOTP_SETUP_NO_BLOCK", { - publicKey, - }); - w.abort(); - return void cb("NO_BLOCK"); - } - // otherwise the block exists, continue - })); - }).nThen(function (w) { - // store the data you'll need in the future - var data = { - method: 'TOTP', // specify this so it's easier to add other methods later? - secret: secret, // the 160 bit, base32-encoded secret that is used for OTP validation - creation: new Date(), // the moment at which the MFA was configured - }; - - if (isString(contact)) { - // 'contact' is an arbitary (and optional) string for manual recovery from 2FA auth fails - // it should already be validated - data.contact = contact; - } - - // We attempt to store a record of the above preferences - // if it fails then we abort and inform the client of an error. - MFA.write(Env, publicKey, JSON.stringify(data), w(function (err) { - if (err) { - w.abort(); - Env.Log.error("TOTP_SETUP_STORAGE_FAILURE", { - publicKey: publicKey, - error: err, - }); - return void cb('STORAGE_FAILURE'); - } - // otherwise continue - })); - }).nThen(function () { - // we have already stored the MFA data, which will cause access to the resource to be restricted to the provided TOTP secret. - // we attempt to create a session as a matter of convenience - so if it fails - // that just means they'll be forced to authenticate - makeSession(Env, publicKey, null, session, cb); - }); -}; - -// This command is somewhat simpler than TOTP_SETUP -// Issue a client a JWT which will allow them to access a login block IFF: -// 1. That login block exists -// 2. That login block is protected by TOTP 2FA -// 3. They can produce a valid OTP for that block's TOTP secret -// 4. They can sign for the block's public key -const validate = Commands.TOTP_VALIDATE = function (Env, body, cb) { - var { publicKey, code } = body; - - // they must provide a valid OTP code - if (!isValidOTP(code)) { return void cb('E_INVALID'); } - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - - var secret; - nThen(function (w) { - // check that there is an MFA configuration for the given account - readMFA(Env, publicKey, w(function (err, content) { - if (err) { - w.abort(); - return void cb(err); - } - secret = content.secret; - })); - }).nThen(function () { - checkCode(Env, secret, code, publicKey, cb); - }); -}; - -validate.complete = function (Env, body, cb) { -/* -if they are here then they: - -1. have a valid block configured with TOTP-based 2FA -2. were able to provide a valid TOTP for that block's secret -3. were able to sign their messages for the block's public key - -So, we should: - -1. instanciate a session for them by generating and storing a token for their public key -2. send them the token - -*/ - var { publicKey, session } = body; - makeSession(Env, publicKey, null, session, cb); -}; - -// Same as TOTP_VALIDATE but without making a session at the end -const check = Commands.TOTP_MFA_CHECK = function (Env, body, cb) { - var { publicKey, auth } = body; - const code = auth; - if (!isValidOTP(code)) { return void cb('E_INVALID'); } - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - var secret; - nThen(function (w) { - readMFA(Env, publicKey, w(function (err, content) { - if (err) { - w.abort(); - return void cb(err); - } - secret = content.secret; - })); - }).nThen(function () { - checkCode(Env, secret, code, publicKey, cb); - }); -}; -check.complete = function (Env, body, cb) { cb(); }; - - -// Revoke a client TOTP secret which will allow them to disable TOTP for a login block IFF: -// 1. That login block exists -// 2. That login block is protected by TOTP 2FA -// 3. They can produce a valid OTP for that block's TOTP secret -// 4. They can sign for the block's public key -const revoke = Commands.TOTP_REVOKE = function (Env, body, cb) { - var { publicKey, code, recoveryKey } = body; - - // they must provide a valid OTP code - if (!isValidOTP(code) && !isValidRecoveryKey(recoveryKey)) { return void cb('E_INVALID'); } - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - - var secret, recoveryStored; - nThen(function (w) { - // check that there is an MFA configuration for the given account - readMFA(Env, publicKey, w(function (err, content) { - if (err) { - w.abort(); - return void cb(err); - } - secret = content.secret; - recoveryStored = content.contact; - })); - }).nThen(function (w) { - if (!recoveryKey) { return; } - w.abort(); - if (!/^secret:/.test(recoveryStored)) { - return void cb("E_NO_RECOVERY_KEY"); - } - recoveryStored = recoveryStored.slice(7); - if (recoveryKey !== recoveryStored) { - return void cb("E_WRONG_RECOVERY_KEY"); - } - cb(); - }).nThen(function () { - checkCode(Env, secret, code, publicKey, cb); - }); -}; - -revoke.complete = function (Env, body, cb) { -/* -if they are here then they: - -1. have a valid block configured with TOTP-based 2FA -2. were able to provide a valid TOTP for that block's secret -3. were able to sign their messages for the block's public key - -So, we should: - -1. Revoke the TOTP authentication for their block -2. Remove all existing sessions -*/ - var { publicKey } = body; - MFA.revoke(Env, publicKey, cb); -}; - - - -// Write a login block using an existing OTP block IFF -// 1. You can sign for the block's public key -// 2. You have a proof for the old block -// 3. The old block is OTP protected -// 4. The OTP code is valid -// Note: this is used when users change their password -const writeBlock = Commands.TOTP_WRITE_BLOCK = function (Env, body, cb) { - const { publicKey, content } = body; - const code = content.auth; - const registrationProof = content.registrationProof; - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - if (publicKey !== content.publicKey) { return void cb("INVALID_KEY"); } - if (!isValidOTP(code)) { return void cb('E_INVALID'); } - if (!registrationProof) { return void cb('MISSING_ANCESTOR'); } - - let secret; - let oldKey; - nThen(function (w) { - Block.validateAncestorProof(Env, registrationProof, w((err, provenKey) => { - if (err || !provenKey) { - w.abort(); - return void cb('INVALID_ANCESTOR'); - } - oldKey = provenKey; - })); - }).nThen(function (w) { - // check that there is an MFA configuration for the ancestor account - readMFA(Env, oldKey, w(function (err, content) { - if (err) { - w.abort(); - return void cb(err); - } - secret = content.secret; - })); - }).nThen(function () { - // check that the OTP code is valid - checkCode(Env, secret, code, oldKey, cb); - }); -}; - - - -writeBlock.complete = function (Env, body, cb) { - const { publicKey, content, session } = body; - let oldKey; - nThen(function (w) { - // Write new block - Block.writeLoginBlock(Env, content, w((err) => { - if (err) { - w.abort(); - return void cb("BLOCK_WRITE_ERROR"); - } - })); - }).nThen(function (w) { - // Copy MFA settings - const proof = Util.tryParse(content.registrationProof); - oldKey = proof && proof[0]; - if (!oldKey) { - w.abort(); - return void cb('INVALID_ANCESTOR'); - } - MFA.copy(Env, oldKey, publicKey, w()); - }).nThen(function () { - // Create a session for the current user - makeSession(Env, publicKey, oldKey, session, cb); - }); -}; - -// Remove a login block IFF -// 1. You can sign for the block's public key -const removeBlock = Commands.TOTP_REMOVE_BLOCK = function (Env, body, cb) { - const { publicKey, auth } = body; - const code = auth; - - // they must provide a valid block public key - if (!isValidBlockId(publicKey)) { return void cb("INVALID_KEY"); } - if (!isValidOTP(code)) { return void cb('E_INVALID'); } - - let secret; - nThen(function (w) { - // check that there is an MFA configuration for this block - readMFA(Env, publicKey, w(function (err, content) { - if (err) { - w.abort(); - return void cb(err); - } - secret = content.secret; - })); - }).nThen(function () { - // check that the OTP code is valid - checkCode(Env, secret, code, publicKey, cb); - }); -}; - -removeBlock.complete = function (Env, body, cb) { - const { publicKey, edPublic, reason } = body; - nThen(function (w) { - // Remove the block - Block.removeLoginBlock(Env, publicKey, reason, edPublic, w((err) => { - if (err) { - w.abort(); - return void cb(err); - } - })); - }).nThen(() => { - // Delete the MFA settings and sessions - MFA.revoke(Env, publicKey, cb); - }); -}; - diff --git a/lib/client/index.js b/lib/client/index.js deleted file mode 100644 index a4b814ae6..000000000 --- a/lib/client/index.js +++ /dev/null @@ -1,102 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -var Netflux = require("netflux-websocket"); -var WebSocket = require("ws"); -var nThen = require("nthen"); - -var Util = require("../../www/common/common-util"); - -var Nacl = require("tweetnacl/nacl-fast"); - -var Client = module.exports; - -var createNetwork = Client.createNetwork = function (url, cb) { - var CB = Util.once(cb); - - var info = {}; - - Netflux.connect(url, function (url) { - // this websocket seems to never close properly if the error is - // ECONNREFUSED - info.websocket = new WebSocket(url) - .on('error', function (err) { - CB(err); - }) - .on('close', function (/* err */) { - delete info.websocket; - }); - return info.websocket; - }).then(function (network) { - info.network = network; - CB(void 0, info); - }, function (err) { - CB(err); - }); -}; - -var die = function (client) { - var disconnect = Util.find(client, ['config', 'network', 'disconnect']); - if (typeof(disconnect) === 'function') { - disconnect(); - } else { - console.error("disconnect was not a function"); - } - var close = Util.find(client, ['config', 'websocket', 'close']); - if (typeof(close) === 'function') { - client.config.websocket.close(); - } else { - console.error("close was not a function"); - } -}; - -Client.create = function (config, cb) { - if (typeof(config) === 'function') { - cb = config; - config = {}; - } - var client = { - config: config, - }; - var CB = Util.once(function (err, arg) { - if (err) { die(client); } - cb(err, arg); - }); - - client.shutdown = function () { - die(client); - }; - - nThen(function (w) { - if (config.network) { return; } - // connect to the network... - createNetwork('ws://localhost:3000/cryptpad_websocket', w(function (err, info) { - if (err) { - w.abort(); - return void CB(err); - } - config.network = info.network; - config.websocket = info.websocket; - })); - }).nThen(function (w) { - // make sure the network has a historyKeeper id on it - // we're responsible for adding it - if (config.network.historyKeeper) { return; } - var channel = Util.uint8ArrayToHex(Nacl.randomBytes(16)); - config.network.join(channel).then(w(function (wc) { - wc.members.some(function (member) { - if (member.length !== 16) { return; } - config.network.historyKeeper = member; - return true; - }); - wc.leave(); - }), function (err) { - w.abort(); - CB(err); - }); - }).nThen(function () { - CB(void 0, client); - }); -}; - diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js deleted file mode 100644 index e5cf01257..000000000 --- a/lib/commands/admin-rpc.js +++ /dev/null @@ -1,1221 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const nThen = require("nthen"); -const getFolderSize = require("get-folder-size"); -const Util = require("../common-util"); -const Ulimit = require("ulimit"); -const Decrees = require("../decrees"); -const Pinning = require("./pin-rpc"); -const Core = require("./core"); -const Channel = require("./channel"); -const Invitation = require("./invitation"); -const Users = require("./users"); -const Moderators = require("./moderators"); -const Linked = require("./linked"); -const BlockStore = require("../storage/block"); -const MFA = require("../storage/mfa"); -const ArchiveAccount = require('../archive-account'); -const { Worker } = require('node:worker_threads'); -const Fse = require("fs-extra"); -const Fs = require("fs"); - -const config = require("../load-config"); -const Keys = require("../keys"); - -var Admin = module.exports; - -var getFileDescriptorCount = function (Env, server, cb) { - Fs.readdir('/proc/self/fd', function(err, list) { - if (err) { return void cb(err); } - cb(void 0, list.length); - }); -}; - -var getFileDescriptorLimit = function (env, server, cb) { - Ulimit(cb); -}; - -var getCacheStats = function (env, server, cb) { - var metaSize = 0; - var channelSize = 0; - var metaCount = 0; - var channelCount = 0; - - try { - var meta = env.metadata_cache; - for (var x in meta) { - if (meta.hasOwnProperty(x)) { - metaCount++; - metaSize += JSON.stringify(meta[x]).length; - } - } - - var channels = env.channel_cache; - for (var y in channels) { - if (channels.hasOwnProperty(y)) { - channelCount++; - channelSize += JSON.stringify(channels[y]).length; - } - } - } catch (err) { - return void cb(err && err.message); - } - - cb(void 0, { - metadata: metaCount, - metaSize: metaSize, - channel: channelCount, - channelSize: channelSize, - memoryUsage: process.memoryUsage(), - }); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_WORKER_PROFILES'], console.log) -var getWorkerProfiles = function (Env, Server, cb) { - cb(void 0, Env.commandTimers); -}; - -var getActiveSessions = function (Env, Server, cb) { - var stats = Server.getSessionStats(); - cb(void 0, [ - stats.total, - stats.unique - ]); -}; - -var shutdown = function (Env, Server, cb) { - if (true) { - return void cb('E_NOT_IMPLEMENTED'); - } - - // disconnect all users and reject new connections - Server.shutdown(); - - // stop all intervals that may be running - Object.keys(Env.intervals).forEach(function (name) { - clearInterval(Env.intervals[name]); - }); - - // set a flag to prevent incoming database writes - // wait until all pending writes are complete - // then process.exit(0); - // and allow system functionality to restart the server -}; - -var getRegisteredUsers = Admin.getRegisteredUsers = function (Env, Server, cb) { - Env.batchRegisteredUsers('', cb, function (done) { - var dir = Env.paths.pin; - var folders; - var users = 0; - nThen(function (waitFor) { - Fs.readdir(dir, waitFor(function (err, list) { - if (err) { - waitFor.abort(); - return void done(err); - } - folders = list; - })); - }).nThen(function (waitFor) { - folders.forEach(function (f) { - var dir = Env.paths.pin + '/' + f; - Fs.readdir(dir, waitFor(function (err, list) { - if (err) { return; } - // Don't count placeholders - list = list.filter(name => { - return !/\.placeholder$/.test(name); - }); - users += list.length; - })); - }); - }).nThen(function () { - done(void 0, {users}); - }); - }); -}; - -var getDiskUsage = function (Env, Server, cb) { - Env.batchDiskUsage('', cb, function (done) { - var data = {}; - nThen(function (waitFor) { - getFolderSize('./', waitFor(function(err, info) { - data.total = info; - })); - getFolderSize(Env.paths.pin, waitFor(function(err, info) { - data.pin = info; - })); - getFolderSize(Env.paths.blob, waitFor(function(err, info) { - data.blob = info; - })); - getFolderSize(Env.paths.staging, waitFor(function(err, info) { - data.blobstage = info; - })); - getFolderSize(Env.paths.block, waitFor(function(err, info) { - data.block = info; - })); - getFolderSize(Env.paths.data, waitFor(function(err, info) { - data.datastore = info; - })); - }).nThen(function () { - done(void 0, data); - }); - }); -}; - -var getActiveChannelCount = function (Env, Server, cb) { - cb(void 0, Server.getActiveChannelCount()); -}; - -var flushCache = function (Env, Server, cb) { - Env.flushCache(); - cb(void 0, true); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_DOCUMENT', documentID], console.log) -var archiveDocument = function (Env, Server, _cb, data) { - const cb = Util.mkAsync(_cb); - if (!Array.isArray(data)) { return void cb("EINVAL"); } - var args = data[1]; - - var id, reason; - if (typeof(args) === 'string') { - id = args; - } else if (args && typeof(args) === 'object') { - id = args.id; - reason = args.reason; - } - - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - - const archiveReason = { - code: 'MODERATION_PAD', - txt: reason - }; - const reasonStr = `MODERATION_PAD:${reason}`; - - switch (id.length) { - case 32: - return void Linked.listLinkedDocuments(Env, id, (err, channels) => { - Env.msgStore.archiveChannel(id, archiveReason, Util.both(cb, function (err) { - if (!err && channels) { - Linked.archiveLinkedData(Env, id, archiveReason, channels, () => {}); - } - Env.Log.info("ARCHIVAL_CHANNEL_BY_ADMIN_RPC", { - channelId: id, - reason: reason, - status: err? String(err): "SUCCESS", - }); - Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reasonStr, err => { - if (err) { } // TODO - }); - })); - }); - case 48: - return void Env.blobStore.archive.blob(id, archiveReason, Util.both(cb, function (err) { - Env.Log.info("ARCHIVAL_BLOB_BY_ADMIN_RPC", { - id: id, - reason: reason, - status: err? String(err): "SUCCESS", - }); - })); - default: - return void cb("INVALID_ID_LENGTH"); - } - - // archival for blob proofs isn't automated, but evict-inactive.js will - // clean up orpaned blob proofs - // Env.blobStore.archive.proof(userSafeKey, blobId, cb) -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_DOCUMENTS', documents], console.log) -var archiveDocuments = function (Env, Server, cb, data) { - if (!Array.isArray(data)) { return void cb("EINVAL"); } - let args = data[1]; - const { list, reason } = args; - if (!Array.isArray(list)) { return void cb('EINVAL'); } - let n = nThen; - let failed = []; - list.forEach(id => { - n = n((w) => { - archiveDocument(Env, Server, w(err => { - console.log(err); - if (err && err !== 'ENOENT') { failed.push(id); } - }), [0, { id, reason }]); - }).nThen; - }); - n(() => { - cb(void 0, { state: true, failed }); - }); -}; - - -var removeDocument = function (Env, Server, cb, data) { - if (!Array.isArray(data)) { return void cb("EINVAL"); } - var args = data[1]; - - var id, reason; - if (typeof(args) === 'string') { - id = args; - } else if (args && typeof(args) === 'object') { - id = args.id; - reason = `MODERATION_DESTROY:${args.reason}`; - } - - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - - switch (id.length) { - case 32: - return void Linked.listLinkedDocuments(Env, id, (err, channels) => { - Env.msgStore.removeChannel(id, Util.both(cb, function (err) { - if (!err && channels) { - Linked.archiveLinkedData(Env, id, reason, channels, () => {}); - } - Env.Log.info("REMOVAL_CHANNEL_BY_ADMIN_RPC", { - channelId: id, - reason: reason, - status: err? String(err): "SUCCESS", - }); - Channel.disconnectChannelMembers(Env, Server, id, 'EDELETED', reason, err => { - if (err) { } // TODO - }); - })); - }); - case 48: - return void Env.blobStore.remove.blob(id, Util.both(cb, function (err) { - Env.Log.info("REMOVAL_BLOB_BY_ADMIN_RPC", { - id: id, - reason: reason, - status: err? String(err): "SUCCESS", - }); - })); - default: - return void cb("INVALID_ID_LENGTH"); - } -}; - - -var restoreArchivedDocument = function (Env, Server, cb, data) { - if (!Array.isArray(data)) { return void cb("EINVAL"); } - var args = data[1]; - - var id, reason; - if (typeof(args) === 'string') { - id = args; - } else if (args && typeof(args) === 'object') { - id = args.id; - reason = args.reason; - } - - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - - switch (id.length) { - case 32: - return void Env.msgStore.restoreArchivedChannel(id, Util.both(cb, function (err) { - Env.Log.info("RESTORATION_CHANNEL_BY_ADMIN_RPC", { - id: id, - reason: reason, - status: err? String(err): 'SUCCESS', - }); - })); - case 48: - // FIXME this does not yet restore blob ownership - // Env.blobStore.restore.proof(userSafekey, id, cb) - return void Env.blobStore.restore.blob(id, Util.both(cb, function (err) { - Env.Log.info("RESTORATION_BLOB_BY_ADMIN_RPC", { - id: id, - reason: reason, - status: err? String(err): 'SUCCESS', - }); - })); - default: - return void cb("INVALID_ID_LENGTH"); - } -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['ARCHIVE_ACCOUNT', {key, block, reason}], console.log) -var archiveAccount = function (Env, Server, _cb, data) { - const cb = Util.once(_cb); - const worker = new Worker('./lib/archive-account.js'); - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - worker.on('message', message => { - if (message === 'READY') { - return worker.postMessage({ - command: 'start', - content: args.key, - block: args.block, // optional, may be including in pin log - reason: args.reason - }); - } - - // DONE: disconnect all users from these channels - Env.Log.info('ARCHIVE_ACCOUNT_BY_ADMIN', { - safeKey: args.key, - reason: args.reason, - }); - const reason = `MODERATION_ACCOUNT:${args.reason}`; - var deletedChannels = Util.tryParse(message); - if (Array.isArray(deletedChannels)) { - let n = nThen; - deletedChannels.forEach((chanId) => { - n = n((w) => { - setTimeout(w(() => { - Channel.disconnectChannelMembers(Env, Server, chanId, 'EDELETED', reason, () => {}); - }), 10); - }).nThen; - }); - } - cb(void 0, { state: true }); - }); - worker.on('error', (err) => { - console.error(err); - cb(err); - }); - worker.on('exit', () => { worker.unref(); }); -}; -var restoreAccount = function (Env, Server, _cb, data) { - const cb = Util.once(_cb); - const worker = new Worker('./lib/archive-account.js'); - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - worker.on('message', message => { - if (message === 'READY') { - return worker.postMessage({ - command: 'restore', - content: args.key - }); - } - // Response - Env.Log.info('RESTORE_ACCOUNT_BY_ADMIN', { - safeKey: args.key, - reason: args.reason, - }); - cb(void 0, { - state: true, - errors: Util.tryParse(message) - }); - }); - worker.on('error', (err) => { - console.error(err); - cb(err); - }); - worker.on('exit', () => { worker.unref(); }); -}; -var getAccountArchiveStatus = function (Env, Server, cb, data) { - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - ArchiveAccount.getStatus(Env, args.key, cb); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_INDEX', documentID], console.log) -var clearChannelIndex = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - delete Env.channel_cache[id]; - cb(); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_CACHED_CHANNEL_INDEX', documentID], console.log) -var getChannelIndex = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - - var index = Util.find(Env, ['channel_cache', id]); - if (!index) { return void cb("ENOENT"); } - cb(void 0, index); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['CLEAR_CACHED_CHANNEL_METADATA', documentID], console.log) -var clearChannelMetadata = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - delete Env.metadata_cache[id]; - cb(); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_CACHED_CHANNEL_METADATA', documentID], console.log) -var getChannelMetadata = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string' || id.length < 32) { return void cb("EINVAL"); } - - var index = Util.find(Env, ['metadata_cache', id]); - if (!index) { return void cb("ENOENT"); } - cb(void 0, index); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log) -var adminDecree = Admin.sendDecree = function (Env, Server, cb, data, unsafeKey) { - var value = data[1]; - if (!Array.isArray(value)) { return void cb('INVALID_DECREE'); } - - var command = value[0]; - var args = value[1]; - -/* - -The admin should have sent a command to be run: - -the server adds two pieces of information to the supplied decree: - -* the unsafeKey of the admin who uploaded it -* the current time - -1. test the command to see if it's valid and will result in a change -2. if so, apply it and write it to the log for persistence -3. respond to the admin with an error or nothing - -*/ - - var decree = [command, args, unsafeKey, +new Date()]; - var changed; - try { - changed = Decrees.handleCommand(Env, decree) || false; - } catch (err) { - return void cb(err); - } - - if (!changed) { return void cb(); } - Env.Log.info('ADMIN_DECREE', decree); - let _err; - nThen((waitFor) => { - Decrees.write(Env, decree, waitFor((err) => { - _err = err; - })); - setTimeout(waitFor(), 300); // NOTE: 300 because cache update may take up to 250ms - }).nThen(function () { - cb(_err); - }); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_LAST_EVICTION', 0], console.log) -var setLastEviction = function (Env, Server, cb, data, unsafeKey) { - var time = data && data[1]; - if (typeof(time) !== 'number') { - return void cb('INVALID_ARGS'); - } - - Env.lastEviction = time; - cb(); - Env.Log.info('LAST_EVICTION_TIME_SET', { - author: unsafeKey, - time: time, - }); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['INSTANCE_STATUS], console.log) -const getAdminsData = (Env) => { - return Env.adminsData.map(str => { - // str is either a full public key or just the ed part - const edPublic = Keys.canonicalize(str); - const hardcoded = Array.isArray(config?.adminKeys) && - config.adminKeys.some(key => { - return Keys.canonicalize(key) === edPublic; - }); - if (str.length === 44) { - return { edPublic, first: true, hardcoded }; - } - let name; - try { - const parsed = Keys.parseUser(str); - name = parsed.user; - } catch (e) {} - return { - edPublic, hardcoded, name - }; - }); -}; -var instanceStatus = function (Env, Server, cb) { - - cb(void 0, { - - appsToDisable: Env.appsToDisable, - restrictRegistration: Env.restrictRegistration, - restrictSsoRegistration: Env.restrictSsoRegistration, - dontStoreSSOUsers: Env.dontStoreSSOUsers, - dontStoreInvitedUsers: Env.dontStoreInvitedUsers, - - enableEmbedding: Env.enableEmbedding, - launchTime: Env.launchTime, - currentTime: +new Date(), - - inactiveTime: Env.inactiveTime, - accountRetentionTime: Env.accountRetentionTime, - archiveRetentionTime: Env.archiveRetentionTime, - - defaultStorageLimit: Env.defaultStorageLimit, - - lastEviction: Env.lastEviction, - evictionReport: Env.evictionReport, - - disableIntegratedEviction: Env.disableIntegratedEviction, - disableIntegratedTasks: Env.disableIntegratedTasks, - - enableProfiling: Env.enableProfiling, - profilingWindow: Env.profilingWindow, - - maxUploadSize: Env.maxUploadSize, - premiumUploadSize: Env.premiumUploadSize, - - consentToContact: Env.consentToContact, - listMyInstance: Env.listMyInstance, - provideAggregateStatistics: Env.provideAggregateStatistics, - - removeDonateButton: Env.removeDonateButton, - blockDailyCheck: Env.blockDailyCheck, - - updateAvailable: Env.updateAvailable, - instancePurpose: Env.instancePurpose, - - instanceDescription: Env.instanceDescription, - instanceJurisdiction: Env.instanceJurisdiction, - instanceName: Env.instanceName, - instanceNotice: Env.instanceNotice, - enforceMFA: Env.enforceMFA, - - admins: getAdminsData(Env) - }); -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_LIMITS'], console.log) -var getLimits = function (Env, Server, cb) { - cb(void 0, Env.limits); -}; - -var isValidKey = key => { - return typeof(key) === 'string' && key.length === 44; -}; - -// CryptPad_AsyncStore.rpc.send('ADMIN', ['GET_USER_TOTAL_SIZE', "CrufexqXcY/z+eKJlEbNELVy5Sb7E/EAAEFI8GnEtZ0="], console.log) -var getUserTotalSize = function (Env, Server, cb, data) { - var signingKey = Array.isArray(data) && data[1]; - if (!isValidKey(signingKey)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(signingKey); - Pinning.getTotalSize(Env, safeKey, cb); -}; - -var getPinActivity = function (Env, Server, cb, data) { - var signingKey = Array.isArray(data) && data[1]; - if (!isValidKey(signingKey)) { return void cb("EINVAL"); } - // the db-worker ensures the signing key is of the appropriate form - Env.getPinActivity(signingKey, function (err, response) { - if (err) { return void cb(err && err.code); } - cb(void 0, response); - }); -}; - -var isUserOnline = function (Env, Server, cb, data) { - var key = Array.isArray(data) && data[1]; - if (!isValidKey(key)) { return void cb("EINVAL"); } - key = Util.unescapeKeyCharacters(key); - var online = false; - try { - Object.keys(Env.netfluxUsers).some(function (netfluxId) { - if (!Env.netfluxUsers[netfluxId][key]) { return; } - online = true; - return true; - }); - } catch (err) { - Env.Log.error('ADMIN_USER_ONLINE_CHECK', { - error: err, - key: key, - }); - return void cb("SERVER_ERROR"); - } - cb(void 0, online); -}; - -var getPinLogStatus = function (Env, Server, cb, data) { - var key = Array.isArray(data) && data[1]; - if (!isValidKey(key)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(key); - - var response = {}; - nThen(function (w) { - Env.pinStore.isChannelAvailable(safeKey, w(function (err, result) { - if (err) { - return void Env.Log.error('PIN_LOG_STATUS_AVAILABLE', err); - } - response.live = result; - })); - Env.pinStore.isChannelArchived(safeKey, w(function (err, result) { - if (err) { - return void Env.Log.error('PIN_LOG_STATUS_ARCHIVED', err); - } - response.archived = result; - })); - }).nThen(function () { - cb(void 0, response); - }); -}; - -var getDocumentStatus = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string') { return void cb("EINVAL"); } - var response = {}; - if (id.length === 44) { - return void nThen(function (w) { - BlockStore.isAvailable(Env, id, w(function (err, result) { - if (err) { - return void Env.Log.error('BLOCK_STATUS_AVAILABLE', err); - } - response.live = result; - })); - BlockStore.isArchived(Env, id, w(function (err, result) { - if (err) { - return void Env.Log.error('BLOCK_STATUS_ARCHIVED', err); - } - response.archived = result; - })); - BlockStore.readPlaceholder(Env, id, w((result) => { - if (!result) { return; } - response.placeholder = result; - })); - MFA.read(Env, id, w(function (err, v) { - if (err === 'ENOENT') { - response.totp = 'DISABLED'; - } else if (v) { - var parsed = Util.tryParse(v); - response.totp = { - enabled: true, - recovery: parsed.contact && parsed.contact.split(':')[0] - }; - } else { - response.totp = err; - } - })); - }).nThen(function () { - cb(void 0, response); - }); - } - if (id.length === 48) { - return void nThen(function (w) { - Env.blobStore.isBlobAvailable(id, w(function (err, result) { - if (err) { - return void Env.Log.error('BLOB_STATUS_AVAILABLE', err); - } - response.live = result; - })); - Env.blobStore.isBlobArchived(id, w(function (err, result) { - if (err) { - return void Env.Log.error('BLOB_STATUS_ARCHIVED', err); - } - response.archived = result; - })); - Env.blobStore.getPlaceholder(id, w((result) => { - if (!result) { return; } - response.placeholder = result; - })); - }).nThen(function () { - cb(void 0, response); - }); - } - if (id.length !== 32) { return void cb("EINVAL"); } - nThen(function (w) { - Env.store.isChannelAvailable(id, w(function (err, result) { - if (err) { - return void Env.Log.error('CHANNEL_STATUS_AVAILABLE', err); - } - response.live = result; - })); - Env.store.isChannelArchived(id, w(function (err, result) { - if (err) { - return void Env.Log.error('CHANNEL_STATUS_ARCHIVED', err); - } - response.archived = result; - })); - Env.store.getPlaceholder(id, w((result) => { - if (!result) { return; } - response.placeholder = result; - })); - }).nThen(function () { - cb(void 0, response); - }); -}; - -var disableMFA = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (typeof(id) !== 'string' || id.length !== 44) { return void cb("EINVAL"); } - MFA.revoke(Env, id, cb); -}; - -var getPinList = function (Env, Server, cb, data) { - var key = Array.isArray(data) && data[1]; - if (!isValidKey(key)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(key); - - Env.getPinState(safeKey, function (err, value) { - if (err) { return void cb(err); } - try { - return void cb(void 0, Object.keys(value).filter(k => value[k])); - } catch (err2) { } - cb("UNEXPECTED_SERVER_ERROR"); - }); -}; - -var getPinHistory = function (Env, Server, cb, data) { - Env.Log.debug('GET_PIN_HISTORY', data); - cb("NOT_IMPLEMENTED"); -}; - -/* -// NOTE: Deprecated, archive whole account now -var archivePinLog = function (Env, Server, cb, data) { - var args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - var key = args.key; - var reason = args.reason || ''; - if (!isValidKey(key)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(key); - - Env.pinStore.archiveChannel(safeKey, undefined, function (err) { - Core.expireSession(Env.Sessions, safeKey); - if (err) { - Env.Log.error('ARCHIVE_PIN_LOG_BY_ADMIN', { - error: err, - safeKey: safeKey, - reason: reason, - }); - } else { - Env.Log.info('ARCHIVE_PIN_LOG_BY_ADMIN', { - safeKey: safeKey, - reason: reason, - }); - } - cb(err); - }); -}; -*/ - -var archiveBlock = function (Env, Server, cb, data) { - var args = Array.isArray(data) && data[1]; - if (!args) { return void cb("INVALID_ARGS"); } - var key = args.key; - var reason = args.reason; - if (!isValidKey(key)) { return void cb("EINVAL"); } - const archiveReason = { - code: 'MODERATION_BLOCK', - txt: reason - }; - BlockStore.archive(Env, key, archiveReason, err => { - Env.Log.info("ARCHIVE_BLOCK_BY_ADMIN", { - error: err, - key: key, - reason: reason || '', - }); - cb(err); - }); - let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils; - if (SSOUtils) { SSOUtils.deleteAccount(Env, key, () => {}); } -}; - -var restoreArchivedBlock = function (Env, Server, cb, data) { - var args = Array.isArray(data) && data[1]; - if (!args) { return void cb("INVALID_ARGS"); } - var key = args.key; - var reason = args.reason; - if (!isValidKey(key)) { return void cb("EINVAL"); } - BlockStore.restore(Env, key, err => { - Env.Log.info("RESTORE_ARCHIVED_BLOCK_BY_ADMIN", { - error: err, - key: key, - reason: reason || '', - }); - - // Also restore SSO data - let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils; - if (SSOUtils) { SSOUtils.restoreAccount(Env, key, () => {}); } - - cb(err); - }); -}; - -/* -// NOTE: Deprecated, archive whole account now -var restoreArchivedPinLog = function (Env, Server, cb, data) { - var args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - var key = args.key; - var reason = args.reason || ''; - if (!isValidKey(key)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(key); - Env.pinStore.restoreArchivedChannel(safeKey, function (err) { - Core.expireSession(Env.Sessions, safeKey); - if (err) { - Env.Log.error("RESTORE_ARCHIVED_PIN_LOG_BY_ADMIN", { - error: err, - safeKey: safeKey, - reason: reason, - }); - } else { - Env.Log.info('RESTORE_ARCHIVED_PIN_LOG_BY_ADMIN', { - safeKey: safeKey, - reason: reason, - }); - } - cb(err); - }); -}; -*/ - -var archiveOwnedDocuments = function (Env, Server, cb, data) { - Env.Log.debug('ARCHIVE_OWNED_DOCUMENTS', data); - cb("NOT_IMPLEMENTED"); -}; - -// quotas... -var getUserQuota = function (Env, Server, cb, data) { - var key = Array.isArray(data) && data[1]; - if (!isValidKey(key)) { return void cb("EINVAL"); } - Pinning.getLimit(Env, key, cb); -}; - -var getUserStorageStats = function (Env, Server, cb, data) { - var key = Array.isArray(data) && data[1]; - if (!isValidKey(key)) { return void cb("EINVAL"); } - var safeKey = Util.escapeKeyCharacters(key); - - Env.getPinState(safeKey, function (err, value) { - if (err) { return void cb(err); } - try { - var res = { - channels: 0, - files: 0, - }; - Object.keys(value).forEach(k => { - switch (k.length) { - case 32: return void ((res.channels++)); - case 48: return void ((res.files++)); - } - }); - return void cb(void 0, res); - } catch (err2) { } - cb("UNEXPECTED_SERVER_ERROR"); - }); -}; - -var getStoredMetadata = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (!Core.isValidId(id)) { return void cb('INVALID_CHAN'); } - Env.computeMetadata(id, function (err, data) { - cb(err, data); - }); -}; - -var getDocumentSize = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (!Core.isValidId(id)) { return void cb('INVALID_CHAN'); } - Env.getFileSize(id, (err, size) => { - if (err) { return void cb(err); } - cb(err, size); - }); -}; - -var getLastChannelTime = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (!Core.isValidId(id)) { return void cb('INVALID_CHAN'); } - Env.getLastChannelTime(id, function (err, time) { - if (err) { return void cb(err && err.code); } - cb(err, time); - }); -}; - -var getMetadataHistory = function (Env, Server, cb, data) { - var id = Array.isArray(data) && data[1]; - if (!Core.isValidId(id)) { return void cb('INVALID_CHAN'); } - - var lines = []; - Env.msgStore.readChannelMetadata(id, (err, line) => { - if (err) { return; } - lines.push(line); - }, err => { - if (err) { - Env.Log.error('ADMIN_GET_METADATA_HISTORY', { - error: err, - id: id, - }); - return void cb(err); - } - cb(void 0, lines); - }); -}; - -var getKnownUsers = (Env, Server, cb) => { - Users.getAll(Env, cb); -}; -var addKnownUser = (Env, Server, cb, data, unsafeKey) => { - var obj = Array.isArray(data) && data[1]; - var edPublic = obj.edPublic; - var block = obj.block; - var alias = obj.alias; - var userData = { - edPublic, - block, - alias, - email: obj.email, - name: obj.name, - type: 'manual' - }; - Users.add(Env, edPublic, userData, unsafeKey, cb); -}; -var deleteKnownUser = (Env, Server, cb, data) => { - var id = Array.isArray(data) && data[1]; - Users.delete(Env, id, cb); -}; -var updateKnownUser = (Env, Server, cb, data) => { - var args = Array.isArray(data) && data[1]; - var edPublic = args.edPublic; - var changes = args.changes; - Users.update(Env, edPublic, changes, cb); -}; - -var getInvitations = (Env, Server, cb) => { - Invitation.getAll(Env, cb); -}; -var createInvitation = (Env, Server, cb, data, unsafeKey) => { - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - Invitation.create(Env, args.alias, args.email, cb, unsafeKey); -}; -var deleteInvitation = (Env, Server, cb, data) => { - var id = Array.isArray(data) && data[1]; - Invitation.delete(Env, id, cb); -}; - -var getModerators = (Env, Server, cb) => { - Moderators.getAll(Env, cb); -}; -var addModerator = (Env, Server, cb, data, unsafeKey) => { - const obj = Array.isArray(data) && data[1]; - const name = obj.name; - const edPublic = obj.edPublic; - const curvePublic = obj.curvePublic; - const mailbox = obj.mailbox; - const profile = obj.profile; - const userData = { - name, - edPublic, - curvePublic, - mailbox, - profile - }; - Moderators.add(Env, edPublic, userData, unsafeKey, cb); -}; -var removeModerator = (Env, Server, cb, data) => { - const id = Array.isArray(data) && data[1]; - Moderators.delete(Env, id, cb); -}; -var archiveSupport = (Env, Server, cb) => { - let supportPinKey = Env.supportPinKey; - getPinList(Env, Server, (err, list) => { - if (err) { return void cb(err); } - let n = nThen; - list.forEach(id => { - n = n(waitFor => { - archiveDocument(Env, Server, waitFor(), [null, {id, reason:'DISABLE_SUPPORT'}]); - }).nThen; - }); - n(() => { - cb(); - }); - }, [null, supportPinKey]); -}; - -const MAX_LOGO_SIZE = 200*1024; // 200KB -var removeLogo = (Env, Server, cb) => { - Fse.unlink('./customize/CryptPad_logo_hero.svg', (err) => { - cb(err); - }); -}; -var uploadLogo = (Env, Server, cb, data, unsafeKey) => { - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - let dataURL = args.dataURL; - - // (size*4/3) + 24 ==> base64 and dataURL overhead - if (!dataURL || dataURL.length > ((MAX_LOGO_SIZE*4/3)+24)) { - return void cb('E_TOO_LARGE'); - } - - let s = dataURL.split(','); - let base64 = s[1]; - let mime = s[0].slice(s[0].indexOf(":")+1, s[0].indexOf(";")); - if (!base64 || !mime) { return void cb('EINVAL'); } - let buf; - try { - buf = Buffer.from(base64, 'base64'); - } catch (e) { - return void cb(e); - } - - nThen(waitFor => { - Fse.mkdirp('customize', {}, waitFor((err) => { - if (!err) { return; } - waitFor.abort(); - return void cb(err); - })); - }).nThen(waitFor => { - Fse.writeFile('./customize/CryptPad_logo_hero.svg', buf, waitFor((err) => { - if (!err) { return; } - waitFor.abort(); - return void cb(err); - })); - }).nThen(() => { - adminDecree(Env, null, function (err) { - if (err) { return void cb(err); } - Env.flushCache(); - cb(void 0, true); - }, ['UPLOAD_LOGO', [ - 'SET_LOGO_MIME', - [mime] - ]], unsafeKey); - }); -}; - -var changeColor = (Env, Server, cb, data, unsafeKey) => { - const args = Array.isArray(data) && data[1]; - if (!args || typeof(args) !== 'object') { return void cb("EINVAL"); } - let color = args.color; - adminDecree(Env, null, function (err) { - if (err) { return void cb(err); } - Env.flushCache(); - cb(void 0, true); - }, ['CHANGE_COLOR', [ - 'SET_ACCENT_COLOR', - [color] - ]], unsafeKey); -}; - - -var commands = { - ACTIVE_SESSIONS: getActiveSessions, - ACTIVE_PADS: getActiveChannelCount, - REGISTERED_USERS: getRegisteredUsers, - DISK_USAGE: getDiskUsage, - FLUSH_CACHE: flushCache, - SHUTDOWN: shutdown, - GET_FILE_DESCRIPTOR_COUNT: getFileDescriptorCount, - GET_FILE_DESCRIPTOR_LIMIT: getFileDescriptorLimit, - GET_CACHE_STATS: getCacheStats, - - GET_PIN_ACTIVITY: getPinActivity, - IS_USER_ONLINE: isUserOnline, - GET_USER_QUOTA: getUserQuota, - GET_USER_STORAGE_STATS: getUserStorageStats, - GET_PIN_LOG_STATUS: getPinLogStatus, - - GET_METADATA_HISTORY: getMetadataHistory, - GET_STORED_METADATA: getStoredMetadata, - GET_DOCUMENT_SIZE: getDocumentSize, - GET_LAST_CHANNEL_TIME: getLastChannelTime, - GET_DOCUMENT_STATUS: getDocumentStatus, - - DISABLE_MFA: disableMFA, - - GET_PIN_LIST: getPinList, - GET_PIN_HISTORY: getPinHistory, - //ARCHIVE_PIN_LOG: archivePinLog, - ARCHIVE_OWNED_DOCUMENTS: archiveOwnedDocuments, - //RESTORE_ARCHIVED_PIN_LOG: restoreArchivedPinLog, - - ARCHIVE_BLOCK: archiveBlock, - RESTORE_ARCHIVED_BLOCK: restoreArchivedBlock, - - ARCHIVE_DOCUMENT: archiveDocument, - ARCHIVE_DOCUMENTS: archiveDocuments, - RESTORE_ARCHIVED_DOCUMENT: restoreArchivedDocument, - - ARCHIVE_ACCOUNT: archiveAccount, - RESTORE_ACCOUNT: restoreAccount, - GET_ACCOUNT_ARCHIVE_STATUS: getAccountArchiveStatus, - - CLEAR_CACHED_CHANNEL_INDEX: clearChannelIndex, - GET_CACHED_CHANNEL_INDEX: getChannelIndex, - // TODO implement admin historyTrim - // TODO implement kick from channel - // TODO implement force-disconnect user(s)? - - CLEAR_CACHED_CHANNEL_METADATA: clearChannelMetadata, - GET_CACHED_CHANNEL_METADATA: getChannelMetadata, - - ADMIN_DECREE: adminDecree, - INSTANCE_STATUS: instanceStatus, - GET_LIMITS: getLimits, - SET_LAST_EVICTION: setLastEviction, - GET_WORKER_PROFILES: getWorkerProfiles, - GET_USER_TOTAL_SIZE: getUserTotalSize, - - REMOVE_DOCUMENT: removeDocument, - - GET_ALL_INVITATIONS: getInvitations, - CREATE_INVITATION: createInvitation, - DELETE_INVITATION: deleteInvitation, - - GET_ALL_USERS: getKnownUsers, - ADD_KNOWN_USER: addKnownUser, - DELETE_KNOWN_USER: deleteKnownUser, - UPDATE_KNOWN_USER: updateKnownUser, - - ARCHIVE_SUPPORT: archiveSupport, - GET_MODERATORS: getModerators, - ADD_MODERATOR: addModerator, - REMOVE_MODERATOR: removeModerator, - - UPLOAD_LOGO: uploadLogo, - REMOVE_LOGO: removeLogo, - CHANGE_COLOR: changeColor, -}; - -// addFirstAdmin is an anon_rpc command -Admin.addFirstAdmin = function (Env, data, cb) { - if (!Env.installToken) { return void cb('EINVAL'); } - var token = data.token; - if (!token || !data.edPublic) { return void cb('MISSING_ARGS'); } - if (token.length !== 64 || data.edPublic.length !== 44) { return void cb('INVALID_ARGS'); } - if (token !== Env.installToken) { return void cb('FORBIDDEN'); } - if (Array.isArray(Env.admins) && Env.admins.length) { return void cb('EEXISTS'); } - - var key = data.edPublic; - - adminDecree(Env, null, function (err) { - if (err) { return void cb(err); } - Env.flushCache(); - cb(); - }, ['ADD_FIRST_ADMIN', [ - 'ADD_ADMIN_KEY', - [key] - ]], ""); -}; - -Admin.command = function (Env, safeKey, data, _cb, Server) { - var cb = Util.once(Util.mkAsync(_cb)); - - var admins = Env.admins; - - var unsafeKey = Util.unescapeKeyCharacters(safeKey); - if (admins.indexOf(unsafeKey) === -1) { - return void cb("FORBIDDEN"); - } - - var command = commands[data[0]]; - - Object.keys(Env.plugins || {}).forEach(name => { - let plugin = Env.plugins[name]; - if (!plugin.addAdminCommands) { return; } - try { - let c = plugin.addAdminCommands(Env); - Object.keys(c || {}).forEach(cmd => { - if (typeof(c[cmd]) !== "function") { return; } - if (commands[cmd]) { return; } - commands[cmd] = c[cmd]; - }); - } catch (e) {} - }); - - if (typeof(command) === 'function') { - return void command(Env, Server, cb, data, unsafeKey); - } - - return void cb('UNHANDLED_ADMIN_COMMAND'); -}; - diff --git a/lib/commands/block.js b/lib/commands/block.js deleted file mode 100644 index 450eb4a86..000000000 --- a/lib/commands/block.js +++ /dev/null @@ -1,258 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Block = module.exports; -const Nacl = require("tweetnacl/nacl-fast"); -const nThen = require("nthen"); -const Util = require("../common-util"); -const BlockStore = require("../storage/block"); -const Invitation = require("./invitation"); -const Users = require("./users"); - -var isString = s => typeof(s) === 'string'; -Block.isValidBlockId = id => { - return id && isString(id) && id.length === 44; -}; - -/* - We assume that the server is secured against MitM attacks - via HTTPS, and that malicious actors do not have code execution - capabilities. If they do, we have much more serious problems. - - The capability to replay a block write or remove results in either - a denial of service for the user whose block was removed, or in the - case of a write, a rollback to an earlier password. - - Since block modification is destructive, this can result in loss - of access to the user's drive. - - So long as the detached signature is never observed by a malicious - party, and the server discards it after proof of knowledge, replays - are not possible. However, this precludes verification of the signature - at a later time. - - Despite this, an integrity check is still possible by the original - author of the block, since we assume that the block will have been - encrypted with xsalsa20-poly1305 which is authenticated. -*/ -Block.validateLoginBlock = function (Env, publicKey, signature, block, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - - // convert the public key to a Uint8Array and validate it - if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); } - - var u8_public_key; - try { - u8_public_key = Util.decodeBase64(publicKey); - } catch (e) { - return void cb('E_INVALID_KEY'); - } - - var u8_signature; - try { - u8_signature = Util.decodeBase64(signature); - } catch (e) { - Env.Log.error('INVALID_BLOCK_SIGNATURE', e); - return void cb('E_INVALID_SIGNATURE'); - } - - // convert the block to a Uint8Array - var u8_block; - try { - u8_block = Util.decodeBase64(block); - } catch (e) { - return void cb('E_INVALID_BLOCK'); - } - - // take its hash - var hash = Nacl.hash(u8_block); - - // validate the signature against the hash of the content - var verified = Nacl.sign.detached.verify(hash, u8_signature, u8_public_key); - - // existing authentication ensures that users cannot replay old blocks - - // call back with (err) if unsuccessful - if (!verified) { return void cb("E_COULD_NOT_VERIFY"); } - - return void cb(null, block); -}; - -Block.validateAncestorProof = function (Env, proof, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); -/* prove that you own an existing block by signing for its publicKey */ - try { - var parsed = JSON.parse(proof); - var pub = parsed[0]; - var u8_pub = Util.decodeBase64(pub); - var sig = parsed[1]; - var u8_sig = Util.decodeBase64(sig); - var valid = false; - nThen(function (w) { - valid = Nacl.sign.detached.verify(u8_pub, u8_sig, u8_pub); - if (!valid) { - w.abort(); - return void cb('E_INVALID_ANCESTOR_PROOF'); - } - // else fall through to next step - }).nThen(function () { - BlockStore.check(Env, pub, function (err) { - if (err) { return void cb('E_MISSING_ANCESTOR'); } - cb(void 0, pub); - }); - }); - } catch (err) { - return void cb(err); - } -}; - -Block.writeLoginBlock = function (Env, msg, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - const { publicKey, signature, ciphertext, registrationProof, userData, inviteToken, isSSO } = msg; - - var previousKey; - var validatedBlock, path; - var validatedInvite; - nThen(function (w) { - if (!inviteToken) { return; } - Invitation.check(Env, inviteToken, w((err, state) => { - if (err || !state) { return; } // Invalid token, don't abort, check registration proof - validatedInvite = true; - })); - }).nThen(function (w) { - if (!Env.restrictRegistration) { return; } - var ssoAllowed = isSSO && !Env.restrictSsoRegistration; - if (!(registrationProof || validatedInvite || ssoAllowed)) { - // we allow users with existing blocks to create new ones - // call back with error if registration is restricted and no proof of an existing block was provided - w.abort(); - Env.Log.info("BLOCK_REJECTED_REGISTRATION", { - publicKey: publicKey, - }); - return cb("E_RESTRICTED"); - } - if (!registrationProof) { return; } - Block.validateAncestorProof(Env, registrationProof, w(function (err, provenKey) { - if (err || !provenKey) { // double check that a key was validated - w.abort(); - Env.Log.warn('BLOCK_REJECTED_INVALID_ANCESTOR', { - error: err, - }); - return void cb("E_RESTRICTED"); - } - previousKey = provenKey; - })); - }).nThen(function (w) { - Block.validateLoginBlock(Env, publicKey, signature, ciphertext, w(function (e, _validatedBlock) { - if (e) { - w.abort(); - return void cb(e); - } - if (typeof(_validatedBlock) !== 'string') { - w.abort(); - return void cb('E_INVALID_BLOCK_RETURNED'); - } - - validatedBlock = _validatedBlock; - })); - }).nThen(function () { - var buffer; - try { - buffer = Buffer.from(Util.decodeBase64(validatedBlock)); - } catch (err) { - return void cb('E_BLOCK_DESERIALIZATION'); - } - BlockStore.write(Env, publicKey, buffer, function (err) { - Env.Log.info('BLOCK_WRITE_BY_OWNER', { - blockId: publicKey, - isChange: Boolean(registrationProof), - previousKey: previousKey, - path: path, - }); - cb(err); - if (!err && registrationProof) { - Users.checkUpdate(Env, userData, publicKey, (err) => { - if (!err) { return; } - Env.Log.error('UPDATE_KNOWN_USER', { - userData, - publicKey - }); - }); - } - }); - - if (validatedInvite) { - Invitation.use(Env, inviteToken, publicKey, userData, (err) => { - if (!err) { return; } - Env.Log.error('USE_INVITATION_LINK', { - inviteToken, - userData, - publicKey - }); - }); - } else if (isSSO && !Env.dontStoreSSOUsers && !registrationProof) { - let edPublic = Array.isArray(userData) && userData[1]; - let name = Array.isArray(userData) && userData[0]; - if (!edPublic) { return; } - let data = { - block: publicKey, - name, - edPublic, - type: 'sso', - alias: name - }; - Users.add(Env, edPublic, data, null, (err) => { - if (err) { - Env.Log.error('INVITATION_ADD_USER', { - error: err, - data: data - }); - } - }); - } - }); -}; - -/* - When users write a block, they upload the block, and provide - a signature proving that they deserve to be able to write to - the location determined by the public key. - - When removing a block, there is nothing to upload, but we need - to sign something. Since the signature is considered sensitive - information, we can just sign some constant and use that as proof. - -*/ -Block.removeLoginBlock = function (Env, publicKey, reason, edPublic, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - - BlockStore.archive(Env, publicKey, reason, function (err) { - Env.Log.info('ARCHIVAL_BLOCK_BY_OWNER_RPC', { - publicKey: publicKey, - status: err? String(err): 'SUCCESS', - }); - cb(err); - }); - - if (edPublic && reason !== 'PASSWORD_CHANGE') { - Users.delete(Env, edPublic, (err) => { - if (err) { Env.Log.error('KNOWN_USER_DELETION_ERROR', { error: err, key: edPublic }); } - }); - } - - // We should also try to remove the SSO data. Errors will be logged - // but they don't have to be shown to the user. The account data - // is already deleted anyway. - - // If this is NOT a password change, also delete sso user. - let SSOUtils = Env.plugins && Env.plugins.SSO && Env.plugins.SSO.utils; - - if (!SSOUtils) { return; } - if (reason !== 'PASSWORD_CHANGE') { - SSOUtils.deleteAccount(Env, publicKey, () => {}); - } else { - SSOUtils.deleteBlock(Env, publicKey, () => {}); - } -}; - diff --git a/lib/commands/channel.js b/lib/commands/channel.js deleted file mode 100644 index 493ec9e17..000000000 --- a/lib/commands/channel.js +++ /dev/null @@ -1,389 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Channel = module.exports; - -const Util = require("../common-util"); -const nThen = require("nthen"); -const Core = require("./core"); -const Metadata = require("./metadata"); -const Linked = require("./linked"); -const HK = require("../hk-util"); -const Nacl = require("tweetnacl/nacl-fast"); - -Channel.disconnectChannelMembers = function (Env, Server, channelId, code, reason, cb) { - var done = Util.once(Util.mkAsync(cb)); - if (!Core.isValidId(channelId)) { return done('INVALID_ID'); } - - const channel_cache = Env.channel_cache; - const metadata_cache = Env.metadata_cache; - - const clear = function () { - delete channel_cache[channelId]; - Server.clearChannel(channelId); - delete metadata_cache[channelId]; - }; - - - // an owner of a channel deleted it - nThen(function (w) { - // close the channel in the store - Env.msgStore.closeChannel(channelId, w()); - }).nThen(function (w) { - // Server.channelBroadcast would be better - // but we can't trust it to track even one callback, - // let alone many in parallel. - // so we simulate it on this side to avoid race conditions - Server.getChannelUserList(channelId).forEach(function (userId) { - Server.send(userId, [ - 0, - Env.historyKeeper.id, - "MSG", - userId, - JSON.stringify({ - error: code, //'EDELETED', - message: reason, - channel: channelId, - }) - ], w()); - }); - }).nThen(function () { - // clear the channel's data from memory - // once you've sent everyone a notice that the channel has been deleted - clear(); - done(); - }).orTimeout(function () { - Env.Log.warn('DISCONNECT_CHANNEL_MEMBERS_TIMEOUT', { - channelId, - code, - reason - }); - clear(); - done(); - }, 30000); -}; - -Channel.clearOwnedChannel = function (Env, safeKey, channelId, cb, Server) { - if (typeof(channelId) !== 'string' || channelId.length !== 32) { - return cb('INVALID_ARGUMENTS'); - } - var unsafeKey = Util.unescapeKeyCharacters(safeKey); - - Metadata.getMetadata(Env, channelId, function (err, metadata) { - if (err) { return void cb(err); } - if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); } - // Confirm that the channel is owned by the user in question - if (!Core.isOwner(metadata, unsafeKey)) { - return void cb('INSUFFICIENT_PERMISSIONS'); - } - return void Env.msgStore.clearChannel(channelId, function (e) { - if (e) { return void cb(e); } - cb(); - - const channel_cache = Env.channel_cache; - - const clear = function () { - // delete the channel cache because it will have been invalidated - delete channel_cache[channelId]; - }; - - nThen(function (w) { - Server.getChannelUserList(channelId).forEach(function (userId) { - Server.send(userId, [ - 0, - Env.historyKeeper.id, - 'MSG', - userId, - JSON.stringify({ - error: 'ECLEARED', - channel: channelId - }) - ], w()); - }); - }).nThen(function () { - clear(); - }).orTimeout(function () { - Env.Log.warn("ON_CHANNEL_CLEARED_TIMEOUT", channelId); - clear(); - }, 30000); - }); - }); -}; - -var archiveOwnedChannel = function (Env, safeKey, channelId, reason, __cb, Server) { - var _cb = Util.once(Util.mkAsync(__cb)); - var unsafeKey = Util.unescapeKeyCharacters(safeKey); - reason = reason || 'ARCHIVE_OWNED'; - nThen(function (w) { - // confirm that the channel exists before worrying about whether - // we have permission to delete it. - var cb = _cb; - Env.msgStore.getChannelSize(channelId, w(function (err, bytes) { - if (!bytes) { - w.abort(); - return cb(err || "ENOENT"); - } - })); - }).nThen(function (w) { - var cb = Util.both(w.abort, _cb); - Metadata.getMetadata(Env, channelId, w(function (err, metadata) { - if (err) { return void cb(err); } - if (!Core.hasOwners(metadata)) { return void cb('E_NO_OWNERS'); } - if (!Core.isOwner(metadata, unsafeKey)) { - return void cb('INSUFFICIENT_PERMISSIONS'); - } - })); - }).nThen(function () { - var cb = _cb; - // temporarily archive the file - return void Env.msgStore.archiveChannel(channelId, reason, function (e) { - Env.Log.info('ARCHIVAL_CHANNEL_BY_OWNER_RPC', { - unsafeKey: unsafeKey, - channelId: channelId, - status: e? String(e): 'SUCCESS', - }); - if (e) { - return void cb(e); - } - cb(void 0, 'OK'); - - Channel.disconnectChannelMembers(Env, Server, channelId, 'EDELETED', reason, err => { - if (err) { } // TODO - }); - }); - }); -}; - -Channel.removeOwnedChannel = function (Env, safeKey, obj, __cb, Server) { - var _cb = Util.once(Util.mkAsync(__cb)); - - var channelId = obj.channel; - var reason = obj.reason; - - if (typeof(channelId) !== 'string' || !Core.isValidId(channelId)) { - return _cb('INVALID_ARGUMENTS'); - } - - // archiving large channels or files can be expensive, so do it one at a time - // for any given user to ensure that nobody can use too much of the server's resources - Env.queueDeletes(safeKey, function (next) { - var cb = Util.both(_cb, next); - if (Env.blobStore.isFileId(channelId)) { - return void Env.removeOwnedBlob(channelId, safeKey, reason, cb); - } - Linked.listLinkedDocuments(Env, channelId, (err, channels) => { - archiveOwnedChannel(Env, safeKey, channelId, reason, (err, data) => { - if (!channels) { return void cb(err, data); } - if (err) { return void cb(err); } - Linked.archiveLinkedData(Env, channelId, reason, channels, () => { - cb(void 0, data); - }); - }, Server); - }); - }); -}; - -Channel.trimHistory = function (Env, safeKey, data, cb) { - if (!(data && typeof(data.channel) === 'string' && typeof(data.hash) === 'string' && data.hash.length === 64)) { - return void cb('INVALID_ARGS'); - } - - var channelId = data.channel; - var unsafeKey = Util.unescapeKeyCharacters(safeKey); - var hash = data.hash; - - nThen(function (w) { - Metadata.getMetadataRaw(Env, channelId, w(function (err, metadata) { - if (err) { - w.abort(); - return void cb(err); - } - if (!Core.hasOwners(metadata)) { - w.abort(); - return void cb('E_NO_OWNERS'); - } - if (!Core.isOwner(metadata, unsafeKey)) { - w.abort(); - return void cb("INSUFFICIENT_PERMISSIONS"); - } - // else fall through to the next block - })); - }).nThen(function (w) { - // Archive old checkpoints - Linked.trimHistory(Env, { channel: channelId }, w()); - }).nThen(function () { - // Trim chainpad doc: - Env.msgStore.trimChannel(channelId, hash, function (err) { - Env.Log.info('HK_TRIM_HISTORY', { - unsafeKey: unsafeKey, - channelId: channelId, - status: err? String(err): 'SUCCESS', - }); - if (err) { return void cb(err); } - // clear historyKeeper's cache for this channel - Env.historyKeeper.channelClose(channelId); - cb(void 0, 'OK'); - delete Env.channel_cache[channelId]; - delete Env.metadata_cache[channelId]; - }); - }); -}; - -// Delete a signed mailbox message. This is used when users want -// to delete their form reponses. -Channel.deleteMailboxMessage = function (Env, data, cb) { - const channelId = data.channel; - const hash = data.hash; - const proof = data.proof; - let nonce, proofBytes; - try { - nonce = Util.decodeBase64(proof.split('|')[0]); - proofBytes = Util.decodeBase64(proof.split('|')[1]); - } catch (e) { - return void cb('EINVAL'); - } - Env.msgStore.deleteChannelLine(channelId, hash, function (msg) { - // Check if you're allowed to delete this hash - try { - const mySecret = Env.curvePrivate; - const msgBytes = Util.decodeBase64(msg).subarray(64); // Remove signature - const theirPublic = msgBytes.subarray(24,56); // 0-24 = nonce; 24-56=publickey (32 bytes) - const hashBytes = Nacl.box.open(proofBytes, nonce, theirPublic, mySecret); - return Util.encodeUTF8(hashBytes) === hash; - } catch (e) { - return false; - } - }, function (err) { - if (err) { return void cb(err); } - // clear historyKeeper's cache for this channel - Env.historyKeeper.channelClose(channelId); - cb(); - delete Env.channel_cache[channelId]; - delete Env.metadata_cache[channelId]; - }); -}; - -var ARRAY_LINE = /^\[/; - -/* Files can contain metadata but not content - call back with true if the channel log has no content other than metadata - otherwise false -*/ -Channel.isNewChannel = function (Env, channel, _cb) { - var cb = Util.once(_cb); - if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); } - if (channel.length !== HK.STANDARD_CHANNEL_LENGTH && - channel.length !== HK.ADMIN_CHANNEL_LENGTH) { return void cb('INVALID_CHAN'); } - - Env.msgStore.readMessagesBin(channel, 0, function (msgObj, readMore, abort) { - try { - var msg = msgObj.buff.toString('utf8'); - if (typeof(msg) === 'string' && ARRAY_LINE.test(msg)) { - abort(); - return void cb(void 0, {isNew: false}); - } - } catch (e) { - Env.WARN('invalid message read from store', e); - } - readMore(); - }, function (err, reason) { - // no more messages... - cb(void 0, { - isNew: true, - reason: reason - }); - }); -}; - -/* writePrivateMessage - allows users to anonymously send a message to the channel - prevents their netflux-id from being stored in history - and from being broadcast to anyone that might currently be in the channel - - Otherwise behaves the same as sending to a channel -*/ -Channel.writePrivateMessage = function (Env, args, _cb, Server, netfluxId) { - var cb = Util.once(Util.mkAsync(_cb)); - - var channelId = args[0]; - var msg = args[1]; - - // don't bother handling empty messages - if (!msg) { return void cb("INVALID_MESSAGE"); } - - // don't support anything except regular channels - if (!Core.isValidId(channelId) || (channelId.length !== HK.STANDARD_CHANNEL_LENGTH - && channelId.length !== HK.ADMIN_CHANNEL_LENGTH)) { - return void cb("INVALID_CHAN"); - } - - // We expect a modern netflux-websocket-server instance - // if this API isn't here everything will fall apart anyway - if (!(Server && typeof(Server.send) === 'function')) { - return void cb("NOT_IMPLEMENTED"); - } - - nThen(function (w) { - Metadata.getMetadataRaw(Env, channelId, w(function (err, metadata) { - if (err) { - w.abort(); - Env.Log.error('HK_WRITE_PRIVATE_MESSAGE', err); - return void cb('METADATA_ERR'); - } - - // treat the broadcast channel as write-protected - if (channelId.length === HK.ADMIN_CHANNEL_LENGTH) { - metadata.restricted = true; - } - - if (!metadata || !metadata.restricted) { - return; - } - - var session = HK.getNetfluxSession(Env, netfluxId); - var allowed = HK.listAllowedUsers(metadata); - - - if (HK.isUserSessionAllowed(allowed, session)) { return; } - - w.abort(); - cb('INSUFFICIENT_PERMISSIONS'); - })); - }).nThen(function () { - // historyKeeper expects something with an 'id' attribute - // it will fail unless you provide it, but it doesn't need anything else - var channelStruct = { - id: channelId, - }; - - // construct a message to store and broadcast - var fullMessage = [ - 0, // idk - null, // normally the netflux id, null isn't rejected, and it distinguishes messages written in this way - "MSG", // indicate that this is a MSG - channelId, // channel id - msg // the actual message content. Generally a string - ]; - - - // historyKeeper already knows how to handle metadata and message validation, so we just pass it off here - // if the message isn't valid it won't be stored. - Env.historyKeeper.channelMessage(Server, channelStruct, fullMessage, function (err, time) { - if (err) { - // Message not stored... - return void cb(err); - } - - // Broadcast the message - Server.getChannelUserList(channelId).forEach(function (userId) { - Server.send(userId, fullMessage); - }); - - cb(void 0, time); - }); - - - }); -}; - diff --git a/lib/commands/core.js b/lib/commands/core.js deleted file mode 100644 index 6c92af74f..000000000 --- a/lib/commands/core.js +++ /dev/null @@ -1,155 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Core = module.exports; -const Util = require("../common-util"); -const escapeKeyCharacters = Util.escapeKeyCharacters; -//const { fork } = require('child_process'); - -Core.DEFAULT_LIMIT = 50 * 1024 * 1024; -Core.SESSION_EXPIRATION_TIME = 60 * 1000; - -Core.isValidId = function (chan) { - return chan && chan.length && /^[a-zA-Z0-9=+-]*$/.test(chan) && - [32, 33, 48].indexOf(chan.length) > -1; -}; - -Core.isValidPublicKey = function (owner) { - return typeof(owner) === 'string' && owner.length === 44; -}; - -var makeToken = Core.makeToken = function () { - return Number(Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)) - .toString(16); -}; - -Core.makeCookie = function (token) { - var time = (+new Date()); - time -= time % 5000; - - return [ - time, - process.pid, - token - ]; -}; - -var parseCookie = function (cookie) { - if (!(cookie && cookie.split)) { return null; } - - var parts = cookie.split('|'); - if (parts.length !== 3) { return null; } - - var c = {}; - c.time = new Date(parts[0]); - c.pid = Number(parts[1]); - c.seq = parts[2]; - return c; -}; - -Core.getSession = function (Sessions, key) { - var safeKey = escapeKeyCharacters(key); - if (Sessions[safeKey]) { - Sessions[safeKey].atime = +new Date(); - return Sessions[safeKey]; - } - var user = Sessions[safeKey] = {}; - user.atime = +new Date(); - user.tokens = [ - makeToken() - ]; - return user; -}; - -Core.expireSession = function (Sessions, safeKey) { - var session = Sessions[safeKey]; - if (!session) { return; } - if (session.blobstage) { - session.blobstage.close(); - } - delete Sessions[safeKey]; -}; - -Core.expireSessionAsync = function (Env, safeKey, cb) { - setTimeout(function () { - Core.expireSession(Env.Sessions, safeKey); - cb(void 0, 'OK'); - }); -}; - -var isTooOld = function (time, now) { - return (now - time) > 300000; -}; - -Core.expireSessions = function (Sessions) { - var now = +new Date(); - Object.keys(Sessions).forEach(function (safeKey) { - var session = Sessions[safeKey]; - if (session && isTooOld(session.atime, now)) { - Core.expireSession(Sessions, safeKey); - } - }); -}; - -var addTokenForKey = function (Sessions, publicKey, token) { - if (!Sessions[publicKey]) { throw new Error('undefined user'); } - - var user = Core.getSession(Sessions, publicKey); - user.tokens.push(token); - user.atime = +new Date(); - if (user.tokens.length > 2) { user.tokens.shift(); } -}; - -Core.isValidCookie = function (Sessions, publicKey, cookie) { - var parsed = parseCookie(cookie); - if (!parsed) { return false; } - - var now = +new Date(); - - if (!parsed.time) { return false; } - if (isTooOld(parsed.time, now)) { - return false; - } - - // different process. try harder - if (process.pid !== parsed.pid) { - return false; - } - - var user = Core.getSession(Sessions, publicKey); - if (!user) { return false; } - - var idx = user.tokens.indexOf(parsed.seq); - if (idx === -1) { return false; } - - if (idx > 0) { - // make a new token - addTokenForKey(Sessions, publicKey, Core.makeToken()); - } - - return true; -}; - -// E_NO_OWNERS -Core.hasOwners = function (metadata) { - return Boolean(metadata && Array.isArray(metadata.owners)); -}; - -Core.hasPendingOwners = function (metadata) { - return Boolean(metadata && Array.isArray(metadata.pending_owners)); -}; - -// INSUFFICIENT_PERMISSIONS -Core.isOwner = function (metadata, unsafeKey) { - return metadata.owners.indexOf(unsafeKey) !== -1; -}; - -Core.isPendingOwner = function (metadata, unsafeKey) { - return metadata.pending_owners.indexOf(unsafeKey) !== -1; -}; - -Core.haveACookie = function (Env, safeKey, cb) { - cb(); -}; - diff --git a/lib/commands/invitation.js b/lib/commands/invitation.js deleted file mode 100644 index 337e56bec..000000000 --- a/lib/commands/invitation.js +++ /dev/null @@ -1,89 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Invitation = module.exports; - -const Invite = require('../storage/invite'); -const Util = require("../common-util"); -const Users = require("./users"); -const Crypto = require('node:crypto'); - -const getUid = () => { - return Crypto.randomBytes(18).toString('hex'); -}; - -Invitation.getAll = (Env, cb) => { - Invite.getAll(Env, (err, data) => { - if (err) { return void cb(err); } - cb(null, data); - }); -}; - -Invitation.create = (Env, alias, email, _cb, unsafeKey) => { - const cb = Util.once(Util.mkAsync(_cb)); - const id = getUid(); - const invitation = { - alias, - email, - createdBy: unsafeKey, - time: +new Date() - }; - Invite.write(Env, id, invitation, (err) => { - if (err) { return void cb(err); } - cb(null, id); - }); -}; - -Invitation.delete = (Env, id, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - Invite.delete(Env, id, (err) => { - if (err && err !== 'ENOENT') { return void cb(err); } - cb(void 0, true); - }); -}; - -Invitation.check = (Env, id, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - Invite.read(Env, id, (err) => { - if (err) { return void cb(err); } - cb(void 0, true); - }); -}; - -Invitation.use = (Env, id, blockId, userData, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - Invite.read(Env, id, (err, _data) => { - if (err) { return void cb(err); } - - let data = Util.clone(_data); - if (!Array.isArray(userData)) { userData = []; } - let name = userData[0]; - let edPublic = userData[1]; - data.block = blockId; - data.name = name; - data.edPublic = edPublic; - data.type = 'invite:' + id; - let adminKey = data.createdBy; - if (!Env.dontStoreInvitedUsers) { - Users.add(Env, edPublic, data, adminKey, (err) => { - if (err) { - Env.Log.error('INVITATION_ADD_USER', { - error: err, - data: data - }); - } - }); - } - - Invite.delete(Env, id, (err) => { - if (err) { - Env.Log.error('INVITATION_DELETE_USE', { - error: err, - id: id - }); - } - }); - }); -}; - diff --git a/lib/commands/linked.js b/lib/commands/linked.js deleted file mode 100644 index 7d95af90b..000000000 --- a/lib/commands/linked.js +++ /dev/null @@ -1,445 +0,0 @@ -// SPDX-FileCopyrightText: 2026 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Linked = module.exports; - -const nThen = require("nthen"); -//const Core = require("./core"); -//const CPCrypto = require('../crypto'); -const Util = require("../common-util"); -const MetaRPC = require("./metadata"); -const HK = require("../hk-util"); - -const getMetadata = (Env, channel, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - - const metadata = Env.metadata_cache[channel]; - if (metadata && typeof(metadata) === 'object') { - return void cb(undefined, metadata); - } - - MetaRPC.getMetadataRaw(Env, channel, (err, metadata) => { - if (err) { return void cb(err); } - if (metadata?.channel !== channel && channel.length !== HK.BLOB_ID_LENGTH) { - return cb(); - } - - // cache it - if (channel.length !== HK.BLOB_ID_LENGTH) { - Env.metadata_cache[channel] = metadata; - } - cb(undefined, metadata); - }); -}; - -const allowedTypes = ['checkpoints', 'media', 'channels']; - -// XXX add logs - -Linked.getLinkedDocuments = (Env, data, cb) => { - Env.store.getLinkedDocuments(data.channel, (err, json) => { - if (err && err !== 'ENOENT') { return void cb(err?.message); } - cb(void 0, json || {}); - }); -}; - -Linked.listLinkedDocuments = (Env, channel, _cb) => { - const cb = Util.mkAsync(_cb); - if (channel.length !== HK.STANDARD_CHANNEL_LENGTH) { - return void cb(void 0, []); - } - - const list = new Set(); - Linked.getLinkedDocuments(Env, { channel }, (err, json) => { - if (err) { return void cb(err); } - // For each type, add the channels and/or blobs - allowedTypes.forEach(type => { - const data = json[type]; - if (!Array.isArray(data)) { return; } - // Media or channel: - if (type !== 'checkpoints') { - data.forEach(id => { list.add(id); }); - return; - } - // Checkpoint: - data.forEach(obj => { - if (obj?.rtChannel) { list.add(obj.rtChannel); } - if (obj?.blob) { list.add(obj.blob); } - }); - }); - cb(void 0, Array.from(list)); - }); -}; - -Linked.listOldCheckpoints = (Env, channel, cb) => { - const list = new Set(); - Linked.getLinkedDocuments(Env, { channel }, (err, json) => { - if (err) { return void cb(err); } - - const cps = json.checkpoints || []; - cps.pop(); // preserve last cp - - cps.forEach(obj => { - if (obj?.rtChannel) { list.add(obj.rtChannel); } - if (obj?.blob) { list.add(obj.blob); } - }); - - cb(void 0, Array.from(list)); - }); -}; - - -const checkContent = (content, user) => { - const { type, data } = content; - - if (type === 'checkpoints' && data) { - const { rtChannel, blob } = data; - if (rtChannel?.length !== 32 || (blob && blob?.length !== 48)) { - return false; - } - return { - rtChannel, blob, user, - time: Date.now() - }; - } - - if (type === 'media') { - return data?.length === 48 ? data : false; - } - - if (type === 'channels') { - return data?.length === 32 ? data : false; - } - - return false; -}; - -Linked.addLinkedDocument = (Env, data, cb, _S, userId) => { - // data.user - // data.channel - // data.content - // type, data (channelId or blobId or checkpoint {blob, rtChannel}}) - // data.proof - // (sign "{ user, channel, content }" with pad signing key - - const { user, channel, content, netfluxId, proof } = data; - if (userId !== netfluxId) { return void cb('EFORBIDDEN'); } - - const msg = Util.clone(data); - delete msg.proof; - const signedMsg = JSON.stringify(msg); - - const type = content?.type; - - if (!allowedTypes.includes(type)) { - return void cb('INVALID_TYPE'); - } - - const value = checkContent(content, user); - if (!value) { return void cb('INVALID_CONTENT'); } - - let validateKey; - - nThen(waitFor => { - getMetadata(Env, channel, waitFor((err, metadata) => { - if (!metadata?.validateKey) { - waitFor.abort(); - return void cb(err || 'METADATA_ERROR'); - } - validateKey = metadata.validateKey; - })); - }).nThen(waitFor => { - Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> { - if (err) { - waitFor.abort(); - return void cb('INVALID_PROOF'); - } - })); - }).nThen(() => { - Env.store.addLinkedDocument(channel, type, value, cb); - }); -}; - -Linked.resetLinkedDocuments = (Env, data, cb, _S, userId) => { - // data.user - // data.channel - // data.content - // data.proof - // (sign "{ user, channel, content }" with pad signing key - - const { user, channel, content, netfluxId, proof } = data; - - if (userId !== netfluxId) { return void cb('EFORBIDDEN'); } - - const msg = Util.clone(data); - delete msg.proof; - const signedMsg = JSON.stringify(msg); - - let validateKey; - const newContent = {}; - allowedTypes.forEach(type => { newContent[type] = []; }); - - nThen(waitFor => { - getMetadata(Env, channel, waitFor((err, metadata) => { - if (!metadata?.validateKey) { - waitFor.abort(); - return void cb(err || 'METADATA_ERROR'); - } - validateKey = metadata.validateKey; - })); - }).nThen(waitFor => { - Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> { - if (err) { - waitFor.abort(); - return void cb('INVALID_PROOF'); - } - })); - }).nThen(waitFor => { - Linked.getLinkedDocuments(Env, { channel }, waitFor((err, json = {}) => { - // checkpoints - if (Array.isArray(content?.checkpoints)) { - const old = json?.checkpoints || []; - // add last 10 valid checkpoints - let i = 0; - content.checkpoints.reverse().some(data => { - // If cp already exists, recover user and time - // Otherwise, check integrity of new value and add them now - const oldValue = old.find(obj => { - return obj.blob === data.blob && - obj.rtChannel === data.rtChannel; - }); - const toAdd = oldValue || checkContent({ - type: 'checkpoints', - data - }, user); - if (!toAdd) { return false; } - newContent.checkpoints.unshift(toAdd); - - // Abort after 10 cps - if (++i >= 10) { return true; } - }); - } - - // channels and media - ['channels', 'media'].forEach(type => { - if (!Array.isArray(content?.[type])) { return; } - content[type].forEach(data => { - const toAdd = checkContent({type, data}, user); - if (!toAdd) { return false; } - newContent[type].push(toAdd); - }); - }); - })); - }).nThen(() => { - Env.store.resetLinkedDocuments(channel, newContent, (err, data) => { - const { oldContent } = data; - Env.Log.info('RESET_LINKED_DOCUMENTS', {user, channel, oldContent, content}); - cb(); - }); - }); -}; - -Linked.removeLinkedDocument = (Env, allData, cb, _S, userId) => { - // data.user - // data.channel - // data.content - // type, channelId or blobId - // data.proof - // (sign "{ user, channel, content }" with pad signing key - - const { channel, content, netfluxId, proof } = allData; - - if (userId !== netfluxId) { return void cb('EFORBIDDEN'); } - - const { type, data } = content; - - const msg = Util.clone(data); - delete msg.proof; - const signedMsg = JSON.stringify(msg); - - if (!allowedTypes.includes(type)) { - return void cb('INVALID_TYPE'); - } - - if (typeof(data) !== "string" || ![32,48].includes(data.length)) { - return void cb('INVALID_CONTENT'); - } - - let validateKey; - nThen(waitFor => { - getMetadata(Env, channel, waitFor((err, metadata) => { - if (!metadata?.validateKey) { - waitFor.abort(); - return void cb(err || 'METADATA_ERROR'); - } - validateKey = metadata.validateKey; - })); - }).nThen(waitFor => { - Env.checkSignature(signedMsg, proof, validateKey, waitFor((err)=> { - if (err) { - waitFor.abort(); - return void cb('INVALID_PROOF'); - } - })); - }).nThen(() => { - Env.store.removeLinkedDocument(channel, type, data, cb); - }); -}; - -Linked.getFileSize = (Env, data, _cb) => { - const cb = Util.once(_cb); - const channel = data.channel; - let linked; - nThen(waitFor => { - Linked.listLinkedDocuments(Env, channel, waitFor((err, channels) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - linked = channels || []; - })); - }).nThen(() => { - linked.push(channel); - Env.getTotalSize(linked, cb); - }); -}; - -Linked.getHistorySize = (Env, data, _cb) => { - const cb = Util.once(_cb); - const channel = data.channel; - let linked; - let channelTotalSize = 0; - let size = 0; - let start = 0; - let hash; - - nThen(waitFor => { - Linked.getLinkedDocuments(Env, data, waitFor((err, json) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - linked = Util.clone(json); - })); - }).nThen(waitFor => { - // Get main channel size (chainpad) - Env.getFileSize(channel, waitFor((err, _size) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - channelTotalSize = _size; - }), true); - }).nThen(waitFor => { - // Get history offset to compute non-history size - HK.getHistoryOffset(Env, channel, null, waitFor((err, offset) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - start = offset; - const chanSize = channelTotalSize - offset; - size += chanSize; - })); - }).nThen(waitFor => { - // Get oldest hash of non-history data - Env.store.readMessagesBin(channel, start, (msgObj, readMore, abort) => { - const parsed = Util.tryParse(msgObj.buff.toString('utf8')); - if (!parsed) { return void readMore(); } - hash = HK.getHash(parsed[4]); - abort(); - }, waitFor()); - - }).nThen(waitFor => { - // Get last checkpoint size (blob + rtChannel) - // Note: blob may be falsy if no checkpoint - - const lastCp = (linked?.checkpoints || []).pop(); - if (!lastCp) { return; } - const { blob, rtChannel } = lastCp; - - if (blob) { - Env.getFileSize(blob, waitFor((err, _size) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - size += _size; - }), true); - } - - Env.getFileSize(rtChannel, waitFor((err, _size) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - size += _size; - }), true); - }).nThen(() => { - cb(void 0, { - size, hash - }); - }); -}; - -Linked.trimHistory = (Env, data, cb) => { - const channel = data.channel; - let linked; - // if we reach this step, it means this user is an owner of "channel" - // so we can also delete any document linked to "channel" (from metadata) - nThen(waitFor => { - // List all but the current checkpoints - Linked.listOldCheckpoints(Env, channel, waitFor((err, channels) => { - if (err) { - waitFor.abort(); - return void cb(err); - } - linked = channels || []; - })); - }).nThen(() => { - let n = nThen; - linked.forEach(chan => { - n = n(w => { - // If channel is "linked", we can archive all but last cp - getMetadata(Env, chan, w((err, md) => { - if (md?.linked !== channel) { return; } - // This is an old checkpoint linked to our document, - // we can archive it - const reason = "TRIM_HISTORY"; - if (chan.length === HK.BLOB_ID_LENGTH) { - return Env.blobStore.archive.blob(chan, reason, w()); - } - Env.store.archiveChannel(chan, reason, w()); - })); - }).nThen; - }); - n(() => { - cb(); - }); - }); -}; - -// Archive all linked documents that inherit metadata from their -// parent. We consider ownership has already been checked when -// this function is called. -Linked.archiveLinkedData = (Env, channel, reason, channels, _cb) => { - const cb = Util.once(_cb); - let n = nThen; - channels.forEach(chan => { - n = n(w => { - // For each linked document, check if they inherit properties - getMetadata(Env, chan, w((err, md) => { - if (md?.linked !== channel) { return; } - // If they do, archive the document - if (chan.length === HK.BLOB_ID_LENGTH) { - return Env.blobStore.archive.blob(chan, reason, w()); - } - Env.store.archiveChannel(chan, reason, w()); - })); - }).nThen; - }); - n(() => { - cb(); - }); -}; diff --git a/lib/commands/metadata.js b/lib/commands/metadata.js deleted file mode 100644 index f218492a7..000000000 --- a/lib/commands/metadata.js +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Data = module.exports; - -const Meta = require("../metadata"); -const Core = require("./core"); -const Util = require("../common-util"); -const HK = require("../hk-util"); - -Data.getMetadataRaw = function (Env, channel, _cb, resolveLinked) { - const cb = Util.once(Util.mkAsync(_cb)); - if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); } - if (channel.length !== HK.STANDARD_CHANNEL_LENGTH && - channel.length !== HK.ADMIN_CHANNEL_LENGTH && - channel.length !== HK.BLOB_ID_LENGTH) { return cb("INVALID_CHAN_LENGTH"); } - - // return synthetic metadata for admin broadcast channels as a safety net - // in case anybody manages to write metadata - if (channel.length === HK.ADMIN_CHANNEL_LENGTH) { - return void cb(void 0, { - channel: channel, - creation: +new Date(), - owners: Env.admins, - }); - } - - var cached = Env.metadata_cache[channel]; - if (HK.isMetadataMessage(cached)) { - Env.checkCache(channel); - return void cb(void 0, cached); - } - - Env.batchMetadata(channel, cb, function (done) { - Env.computeMetadata(channel, function (err, meta) { - if (!err && HK.isMetadataMessage(meta)) { - Env.metadata_cache[channel] = meta; - // clear metadata after a delay if nobody has joined the channel within 30s - Env.checkCache(channel); - } - - if (resolveLinked && meta?.linked?.length === HK.STANDARD_CHANNEL_LENGTH - && meta?.linked !== channel) { - Data.getMetadataRaw(Env, meta.linked, (err, _meta) => { - meta.owners = _meta.owners; - meta.restricted = _meta.restricted; - meta.allowed = _meta.allowed; - done(err, meta); - }); - return; - } - done(err, meta); - }); - }); -}; - -Data.getMetadata = function (Env, channel, cb, Server, netfluxId) { - Data.getMetadataRaw(Env, channel, function (err, metadata) { - if (err) { return void cb(err); } - - if (!(metadata && metadata.restricted)) { - // if it's not restricted then just call back - return void cb(void 0, metadata); - } - - const session = HK.getNetfluxSession(Env, netfluxId); - const allowed = HK.listAllowedUsers(metadata); - - if (!HK.isUserSessionAllowed(allowed, session)) { - return void cb(void 0, { - restricted: metadata.restricted, - allowed: allowed, - rejected: true, - }); - } - cb(void 0, metadata); - }); -}; - -/* setMetadata - - write a new line to the metadata log if a valid command is provided - - data is an object: { - channel: channelId, - command: metadataCommand (string), - value: value - } -*/ -Data.setMetadata = function (Env, safeKey, data, cb, Server) { - var unsafeKey = Util.unescapeKeyCharacters(safeKey); - - var channel = data.channel; - var command = data.command; - - if (!channel || !Core.isValidId(channel)) { return void cb ('INVALID_CHAN'); } - if (!command || typeof (command) !== 'string') { return void cb('INVALID_COMMAND'); } - if (Meta.commands.indexOf(command) === -1) { return void cb('UNSUPPORTED_COMMAND'); } - - Env.queueMetadata(channel, function (next) { - Data.getMetadataRaw(Env, channel, function (err, metadata) { - if (err) { - cb(err); - return void next(); - } - if (!Core.hasOwners(metadata)) { - cb('E_NO_OWNERS'); - return void next(); - } - - // if you are a pending owner and not an owner - // you can either ADD_OWNERS, or RM_PENDING_OWNERS - // and you should only be able to add yourself as an owner - // everything else should be rejected - // else if you are not an owner - // you should be rejected - // else write the command - - // Confirm that the channel is owned by the user in question - // or the user is accepting a pending ownership offer - if (Core.hasPendingOwners(metadata) && - Core.isPendingOwner(metadata, unsafeKey) && - !Core.isOwner(metadata, unsafeKey)) { - - // If you are a pending owner, make sure you can only add yourelf as an owner - if ((command !== 'ADD_OWNERS' && command !== 'RM_PENDING_OWNERS') - || !Array.isArray(data.value) - || data.value.length !== 1 - || data.value[0] !== unsafeKey) { - cb('INSUFFICIENT_PERMISSIONS'); - return void next(); - } - // FIXME wacky fallthrough is hard to read - // we could pass this off to a writeMetadataCommand function - // and make the flow easier to follow - } else if (!Core.isOwner(metadata, unsafeKey)) { - cb('INSUFFICIENT_PERMISSIONS'); - return void next(); - } - - // Add the new metadata line - var line = [command, data.value, +new Date()]; - var changed = false; - try { - changed = Meta.handleCommand(metadata, line); - } catch (e) { - cb(e); - return void next(); - } - - // if your command is valid but it didn't result in any change to the metadata, - // call back now and don't write any "useless" line to the log - if (!changed) { - cb(void 0, metadata); - return void next(); - } - let store = Env.msgStore; - if (channel.length === HK.BLOB_ID_LENGTH) { - store = Env.blobStore; - } - store.writeMetadata(channel, JSON.stringify(line), function (e) { - if (e) { - cb(e); - return void next(); - } - - // send the message back to the person who changed it - // since we know they're allowed to see it - cb(void 0, metadata); - next(); - - const metadata_cache = Env.metadata_cache; - - // update the cached metadata - metadata_cache[channel] = metadata; - Env.checkCache(channel); - - // it's easy to check if the channel is restricted - const isRestricted = metadata.restricted; - // and these values will be used in any case - const s_metadata = JSON.stringify(metadata); - const hk_id = Env.historyKeeper.id; - - if (!isRestricted) { - // pre-allow-list behaviour - // if it's not restricted, broadcast the new metadata to everyone - return void Server.channelBroadcast(channel, s_metadata, hk_id); - } - - // otherwise derive the list of users (unsafeKeys) that are allowed to stay - const allowed = HK.listAllowedUsers(metadata); - // anyone who is not allowed will get the same error message - const s_error = JSON.stringify({ - error: 'ERESTRICTED', - channel: channel, - }); - - // iterate over the channel's userlist - const toRemove = []; - Server.getChannelUserList(channel).forEach(function (userId) { - const session = HK.getNetfluxSession(Env, userId); - - // if the user is allowed to remain, send them the metadata - if (HK.isUserSessionAllowed(allowed, session)) { - return void Server.send(userId, [ - 0, - hk_id, - 'MSG', - userId, - s_metadata - ], function () {}); - } - // otherwise they are not in the list. - // send them an error and kick them out! - toRemove.push(userId); - Server.send(userId, [ - 0, - hk_id, - 'MSG', - userId, - s_error - ], function () {}); - }); - - Server.removeFromChannel(channel, toRemove); - }); - }, true); - }); -}; diff --git a/lib/commands/moderators.js b/lib/commands/moderators.js deleted file mode 100644 index 3c86ad5ba..000000000 --- a/lib/commands/moderators.js +++ /dev/null @@ -1,54 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -/*jshint esversion: 6 */ -const Moderators = module.exports; - -const Moderator = require('../storage/moderator'); -const Util = require("../common-util"); - -Moderators.getAll = (Env, cb) => { - Moderator.getAll(Env, (err, data) => { - if (err) { return void cb(err); } - let res = {}; - Object.keys(data).forEach(safeKey => { - const unsafeKey = Util.unescapeKeyCharacters(safeKey); - res[unsafeKey] = data[safeKey]; - }); - cb(null, res); - }); -}; -Moderators.getKeysSync = (Env) => { - return Moderator.getAllKeys(Env); -}; - -Moderators.add = (Env, edPublic, data, adminKey, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - data.createdBy = adminKey; - data.time = +new Date(); - const safeKey = Util.escapeKeyCharacters(edPublic); - Moderator.write(Env, safeKey, data, (err) => { - if (err) { return void cb(err); } - if (!Env.moderators.includes(edPublic)) { - Env.moderators.push(edPublic); - } - Env.envUpdated.fire(); - cb(); - }); -}; - -Moderators.delete = (Env, id, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - const safeKey = Util.escapeKeyCharacters(id); - Moderator.delete(Env, safeKey, (err) => { - if (err && err !== 'ENOENT') { return void cb(err); } - let idx = Env.moderators.indexOf(id); - if (idx !== -1) { - Env.moderators.splice(idx, 1); - Env.envUpdated.fire(); - } - cb(void 0, true); - }); -}; - diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js deleted file mode 100644 index 55b55f9c2..000000000 --- a/lib/commands/pin-rpc.js +++ /dev/null @@ -1,339 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Core = require("./core"); - -const Pinning = module.exports; -const Util = require("../common-util"); -const nThen = require("nthen"); -const Linked = require('./linked'); - -const escapeKeyCharacters = Util.escapeKeyCharacters; -const unescapeKeyCharacters = Util.unescapeKeyCharacters; - -var sumChannelSizes = function (sizes) { // FIXME this synchronous code could be done by a worker - return Object.keys(sizes).map(function (id) { return sizes[id]; }) - .filter(function (x) { - // only allow positive numbers - return !(typeof(x) !== 'number' || x <= 0); - }) - .reduce(function (a, b) { return a + b; }, 0); -}; - -// FIXME it's possible for this to respond before the server has had a chance -// to fetch the limits. Maybe we should respond with an error... -// or wait until we actually know the limits before responding -var getLimit = Pinning.getLimit = function (Env, safeKey, cb) { - var unsafeKey = unescapeKeyCharacters(safeKey); - var limit = Env.limits[unsafeKey]; - var defaultLimit = typeof(Env.defaultStorageLimit) === 'number'? - Env.defaultStorageLimit: Core.DEFAULT_LIMIT; - - var toSend = limit && typeof(limit.limit) === "number"? - [limit.limit, limit.plan, limit.note] : [defaultLimit, '', '']; - - cb(void 0, toSend); -}; - -var getMultipleFileSize = function (Env, channels, cb) { - Env.getMultipleFileSize(channels, cb); -}; - -var loadUserPins = function (Env, safeKey, cb) { - var session = Core.getSession(Env.Sessions, safeKey); - - if (session.channels) { - return cb(session.channels); - } - - Env.batchUserPins(safeKey, cb, function (done) { - Env.getPinState(safeKey, function (err, value) { - if (!err) { - // only put this into the cache if it completes - session.channels = value; - } - done(value); - }); - }); -}; - -var truthyKeys = function (O) { - try { - return Object.keys(O).filter(function (k) { - return O[k]; - }); - } catch (err) { - return []; - } -}; - -var getChannelList = Pinning.getChannelList = function (Env, safeKey, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - loadUserPins(Env, safeKey, function (pins) { - cb(truthyKeys(pins)); - }); -}; - -var addLinkedDocuments = (Env, channels, cb) => { - var arr = Array.from(channels); - var n = nThen; - arr.forEach(chan => { - // For each channel, add their linked documents - n = n(w => { - Linked.listLinkedDocuments(Env, chan, w((err, linked) => { - if (err || !linked) { return; } - linked.forEach(id => { - channels.add(id); - }); - })); - }).nThen; - }); - n(() => { - cb(); - }); -}; - -Pinning.getTotalSize = function (Env, safeKey, cb) { - var unsafeKey = unescapeKeyCharacters(safeKey); - var limit = Env.limits[unsafeKey]; - - // Get a common key if multiple users share the same quota, otherwise take the public key - var batchKey = (limit && Array.isArray(limit.users)) ? limit.users.join('') : safeKey; - - Env.batchTotalSize(batchKey, cb, function (done) { - let channels = new Set(); - - nThen(function (waitFor) { - // Get the channels list for our user account - getChannelList(Env, safeKey, waitFor(function (_channels) { - if (!_channels) { - waitFor.abort(); - return done('INVALID_PIN_LIST'); - } - - for (let channel of _channels) { - channels.add(channel); - } - })); - // Get the channels list for users sharing our quota - if (limit && Array.isArray(limit.users) && limit.users.length > 1) { - limit.users.forEach(function (key) { - if (key === unsafeKey) { return; } // Don't count ourselves twice - getChannelList(Env, key, waitFor(function (_channels) { - if (!_channels) { return; } // Broken user, don't count their quota - for (let channel of _channels) { - channels.add(channel); - } - })); - }); - } - }).nThen(function (waitFor) { - addLinkedDocuments(Env, channels, waitFor()); - }).nThen(function () { - Env.getTotalSize(Array.from(channels), done); - }); - }); -}; - -/* Users should be able to clear their own pin log with an authenticated RPC -*/ -Pinning.removePins = function (Env, safeKey, cb) { - // FIXME respect the queue - Env.pinStore.archiveChannel(safeKey, undefined, function (err) { - Core.expireSession(Env.Sessions, safeKey); - Env.Log.info('ARCHIVAL_PIN_BY_OWNER_RPC', { - safeKey: safeKey, - status: err? String(err): 'SUCCESS', - }); - - if (err) { return void cb(err); } - cb(void 0, 'OK'); - }); -}; - -Pinning.trimPins = function (Env, safeKey, cb) { - cb("NOT_IMPLEMENTED"); -}; - -var getFreeSpace = Pinning.getFreeSpace = function (Env, safeKey, cb) { - getLimit(Env, safeKey, function (e, limit) { - if (e) { return void cb(e); } - Pinning.getTotalSize(Env, safeKey, function (e, size) { - if (typeof(size) === 'undefined') { return void cb(e); } - - var rem = limit[0] - size; - if (typeof(rem) !== 'number') { - return void cb('invalid_response'); - } - cb(void 0, rem); - }); - }); -}; - -Pinning.getHash = function (Env, safeKey, cb) { - getChannelList(Env, safeKey, function (channels) { - Env.hashChannelList(channels, cb); - }); -}; - -Pinning.pinChannel = function (Env, safeKey, channels, cb) { - if (!channels && channels.filter) { - return void cb('INVALID_PIN_LIST'); - } - - // get channel list ensures your session has a cached channel list - getChannelList(Env, safeKey, function (pinned) { - var session = Core.getSession(Env.Sessions, safeKey); - - // only pin channels which are not already pinned - var toStore = channels.filter(function (channel) { - return channel && pinned.indexOf(channel) === -1; - }); - - if (toStore.length === 0) { - return void cb(); - } - - let pin = function () { - Env.pinStore.message(safeKey, JSON.stringify(['PIN', toStore, +new Date()]), - function (e) { - if (e) { return void cb(e); } - if (!session || !session.channels) { return void cb(); } - toStore.forEach(function (channel) { - session.channels[channel] = true; - }); - cb(); - }); - }; - - // Support tickets are always pinned, no need to check the limit - if (safeKey === escapeKeyCharacters(Env.supportPinKey)) { - return void pin(); - } - - getMultipleFileSize(Env, toStore, function (e, sizes) { - if (typeof(sizes) === 'undefined') { return void cb(e); } - var pinSize = sumChannelSizes(sizes); // FIXME don't do this in the main thread... - - getFreeSpace(Env, safeKey, function (e, free) { - if (typeof(free) === 'undefined') { - Env.WARN('getFreeSpace', e); - return void cb(e); - } - if (pinSize > free) { return void cb('E_OVER_LIMIT'); } - - pin(); - }); - }); - }); -}; - -Pinning.unpinChannel = function (Env, safeKey, channels, cb) { - if (!channels && channels.filter) { - // expected array - return void cb('INVALID_PIN_LIST'); - } - - getChannelList(Env, safeKey, function (pinned) { - var session = Core.getSession(Env.Sessions, safeKey); - - // only unpin channels which are pinned - var toStore = channels.filter(function (channel) { - return channel && pinned.indexOf(channel) !== -1; - }); - - if (toStore.length === 0) { - return void cb(); - } - - Env.pinStore.message(safeKey, JSON.stringify(['UNPIN', toStore, +new Date()]), - function (e) { - if (e) { return void cb(e); } - toStore.forEach(function (channel) { - delete session.channels[channel]; - }); - cb(); - }); - }); -}; - -Pinning.resetUserPins = function (Env, safeKey, channelList, _cb) { - var cb = Util.once(Util.mkAsync(_cb)); - if (!Array.isArray(channelList)) { return void cb('INVALID_PIN_LIST'); } - var session = Core.getSession(Env.Sessions, safeKey); - - - if (!channelList.length) { - return void cb(); - } - - let reset = function () { - var pins = {}; - Env.pinStore.message(safeKey, JSON.stringify(['RESET', channelList, +new Date()]), - function (e) { - if (e) { return void cb(e); } - channelList.forEach(function (channel) { - pins[channel] = true; - }); - - // update in-memory cache IFF the reset was allowed. - if (session) { session.channels = pins; } - cb(); - }); - }; - - // Support tickets are always pinned, no need to check the limit - if (safeKey === escapeKeyCharacters(Env.supportPinKey)) { - return void reset(); - } - - getMultipleFileSize(Env, channelList, function (e, sizes) { - if (typeof(sizes) === 'undefined') { return void cb(e); } - var pinSize = sumChannelSizes(sizes); - - - getLimit(Env, safeKey, function (e, limit) { - if (e) { - Env.WARN('[RESET_ERR]', e); - return void cb(e); - } - - /* we want to let people pin, even if they are over their limit, - but they should only be able to do this once. - - This prevents data loss in the case that someone registers, but - does not have enough free space to pin their migrated data. - - They will not be able to pin additional pads until they upgrade - or delete enough files to go back under their limit. */ - if (pinSize > limit[0] && session.hasPinned) { return void(cb('E_OVER_LIMIT')); } - reset(); - }); - }); -}; - -Pinning.getFileSize = function (Env, channel, cb) { - Env.getFileSize(channel, cb); -}; - -/* accepts a list, and returns a sublist of channel or file ids which seem - to have been deleted from the server (file size 0) - - we might consider that we should only say a file is gone if fs.stat returns - ENOENT, but for now it's simplest to just rely on getFileSize... -*/ -Pinning.getDeletedPads = function (Env, channels, cb) { - Env.getDeletedPads(channels, cb); -}; - -// FIXME this will be removed from the client -Pinning.isChannelPinned = function (Env, channel, cb) { - return void cb(void 0, true); -}; - -Pinning.isPremium = function (Env, userKey, cb) { - const limit = Env.limits[userKey]; - return void cb(void 0, !!limit?.plan); - //return void cb(void 0, (limit?.plan && limit.plan !== "custom")); -}; diff --git a/lib/commands/quota.js b/lib/commands/quota.js deleted file mode 100644 index 648d8c5d7..000000000 --- a/lib/commands/quota.js +++ /dev/null @@ -1,322 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Quota = module.exports; - -//const Util = require("../common-util"); -const Keys = require("../keys"); -const Https = require("https"); -const Http = require("http"); -const Util = require("../common-util"); -const Stats = require("../stats"); -const Admin = require("./admin-rpc.js"); -const nThen = require('nthen'); - -var validLimitFields = ['limit', 'plan', 'note', 'users', 'origin']; - -Quota.isValidLimit = function (o) { - var valid = o && typeof(o) === 'object' && - typeof(o.limit) === 'number' && - typeof(o.plan) === 'string' && - typeof(o.note) === 'string' && - // optionally contains a 'users' array - (Array.isArray(o.users) || typeof(o.users) === 'undefined') && - // check that the object contains only the expected fields - !Object.keys(o).some(function (k) { - return validLimitFields.indexOf(k) === -1; - }); - - return valid; -}; - -Quota.applyCustomLimits = function (Env) { - // DecreedLimits > customLimits > serverLimits; - - // FIXME perform an integrity check on shared limits - // especially relevant because we use Env.limits - // when considering whether to archive inactive accounts - - // read custom limits from the Environment (taken from config) - var customLimits = (function (custom) { - var limits = {}; - Object.keys(custom).forEach(function (k) { - var unsafeKey = Keys.canonicalize(k); - if (!unsafeKey) { return; } - limits[unsafeKey] = custom[k]; - }); - return limits; - }(Env.customLimits || {})); - - Env.limits = Env.limits || {}; - Object.keys(customLimits).forEach(function (k) { - if (!Quota.isValidLimit(customLimits[k])) { return; } - Env.limits[k] = customLimits[k]; - }); - // console.log(Env.limits); -}; - -var isRemoteVersionNewer = function (local, remote) { - try { - local = local.split('.').map(Number); - remote = remote.split('.').map(Number); - for (var i = 0; i < 3; i++) { - if (remote[i] < local[i]) { return false; } - if (remote[i] > local[i]) { return true; } - } - } catch (err) { - // if anything goes wrong just fall through and return false - // false negatives are better than false positives - } - return false; -}; - -/* -var Assert = require("assert"); -[ -// remote versions - ['4.5.0', '4.5.0', false], // equal semver should not prompt - ['4.5.0', '4.5.1', true], // patch versions should prompt - ['4.5.0', '4.6.0', true], // minor versions should prompt - ['4.5.0', '5.0.0', true], // major versions should prompt -// local - ['5.3.1', '4.9.0', false], // newer major should not prompt - ['4.7.0', '4.6.0', false], // newer minor should not prompt - ['4.7.0', '4.6.1', false], // newer patch should not prompt if other values are greater -].forEach(function (x) { - var result = isRemoteVersionNewer(x[0], x[1]); - Assert.equal(result, x[2]); -}); -*/ - -// check if the remote endpoint reported an available server version -// which is newer than your current version (Env.version) -// if so, set Env.updateAvailable to the URL of its release notes -var checkUpdateAvailability = function (Env, json) { - if (!(json && typeof(json.updateAvailable) === 'string' && typeof(json.version) === 'string')) { return; } - // expects {updateAvailable: 'https://github.com/cryptpad/cryptpad/releases/4.7.0', version: '4.7.0'} - // the version string is provided explicitly even though it could be parsed from GitHub's URL - // this will allow old instances to understand responses of arbitrary URLs - // as long as we keep using semver for 'version' - if (!isRemoteVersionNewer(Env.version, json.version)) { - Env.updateAvailable = undefined; - return; - } - Env.updateAvailable = json.updateAvailable; - Env.Log.info('AN_UPDATE_IS_AVAILABLE', { - version: json.version, - updateAvailable: json.updateAvaiable, - }); -}; - -var queryAccountServer = function (Env, cb) { - var done = Util.once(Util.mkAsync(cb)); - - var rawBody = Stats.instanceData(Env); - - let send = () => { - Env.Log.info("SERVER_TELEMETRY", rawBody); - var body = JSON.stringify(rawBody); - - var options = { - host: 'accounts.cryptpad.fr', - path: '/api/getauthorized', - method: 'POST', - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(body) - } - }; - - var req = Https.request(options, function (response) { - if (!('' + response.statusCode).match(/^2\d\d$/)) { - return void cb('SERVER ERROR ' + response.statusCode); - } - var str = ''; - - response.on('data', function (chunk) { - str += chunk; - }); - - response.on('end', function () { - try { - var json = JSON.parse(str); - checkUpdateAvailability(Env, json); - done(void 0, json); - } catch (e) { - done(e); - } - }); - }); - - req.on('error', function () { - done(); - }); - - req.end(body); - }; - - if (Env.provideAggregateStatistics) { - let stats = {}; - nThen(waitFor => { - Admin.getRegisteredUsers(Env, null, waitFor((err, data) => { - if (err) { return; } - stats.registered = data.users; - if (Env.lastPingRegisteredUsers) { - stats.usersDiff = stats.registered - Env.lastPingRegisteredUsers; - } - Env.lastPingRegisteredUsers = stats.registered; - })); - }).nThen(() => { - if (Env.maxConcurrentWs) { - stats.maxConcurrentWs = Env.maxConcurrentWs; - Env.maxConcurrentWs = 0; - } - if (Env.maxConcurrentUniqueWs) { - stats.maxConcurrentUniqueIPs = Env.maxConcurrentUniqueWs; - Env.maxConcurrentUniqueWs = 0; - } - if (Env.maxConcurrentRegUsers) { - stats.maxConcurrentRegUsers = Env.maxConcurrentRegUsers; - Env.maxConcurrentRegUsers = 0; - } - if (Env.maxActiveChannels) { - stats.maxConcurrentChannels = Env.maxActiveChannels; - Env.maxActiveChannels = 0; - } - rawBody.statistics = stats; - send(); - }); - return; - } - send(); -}; -Quota.shouldContactServer = function (Env) { - return !(Env.blockDailyCheck === true || - ( - typeof(Env.blockDailyCheck) === 'undefined' && - Env.adminEmail === false - ) - ); -}; -Quota.pingAccountsDaily = function (Env, _cb) { - var cb = Util.mkAsync(_cb); - if (!Quota.shouldContactServer(Env)) { return void cb(); } - queryAccountServer(Env, function (err) { - cb(err); - }); -}; - -var queryQuotaServer = function (Env, cb) { - var done = Util.once(Util.mkAsync(cb)); - - var rawBody = Stats.instanceData(Env); - Env.Log.info("QUOTA_UPDATE", rawBody); - var body = JSON.stringify(rawBody); - - var options = { - host: undefined, - path: '/api/getquota', - method: 'POST', - headers: { - "Content-Type": "application/json", - "Content-Length": Buffer.byteLength(body) - } - }; - - const accountsOrigin = Env.accounts_api; - var H = Https; - if (typeof(accountsOrigin) === 'string') { - try { - let url = new URL(accountsOrigin); - if (!['https:', 'http:'].includes(url.protocol)) { throw new Error("INVALID_PROTOCOL"); } - if (url.protocol === 'http:') { H = Http; } - let port = Number(url.port); - if (port && typeof(port) === 'number') { options.port = port; } - options.host = url.hostname; - Env.Log.info("USING_CUSTOM_ACCOUNTS_API", { - value: accountsOrigin, - }); - } catch (err) { - Env.Log.error("INVALID_CUSTOM_QUOTA_API", { - error: err.message, - value: accountsOrigin, - }); - } - } - - var req = H.request(options, function (response) { - if (!('' + response.statusCode).match(/^2\d\d$/)) { - return void cb('SERVER ERROR ' + response.statusCode); - } - var str = ''; - - response.on('data', function (chunk) { - str += chunk; - }); - - response.on('end', function () { - try { - var json = JSON.parse(str); - // don't overwrite the limits with junk data - if (json && json.message === 'EINVAL') { return void done(); } - done(void 0, json); - } catch (e) { - done(e); - } - }); - }); - - req.on('error', function (e) { - Quota.applyCustomLimits(Env); - done(e); - }); - - req.end(body); -}; -Quota.queryQuotaServer = function (Env, cb) { - Env.batchAccountQuery('', cb, function (done) { - queryQuotaServer(Env, done); - }); -}; -Quota.updateCachedLimits = function (Env, _cb) { - var cb = Util.mkAsync(_cb); - - - if (!Env.accounts_api) { - Quota.applyCustomLimits(Env); - return void cb(); - } - Quota.queryQuotaServer(Env, function (err, json) { - if (err) { return void cb(err); } - if (!json) { return void cb(); } - - for (var k in json) { - if (k.length === 44 && json[k]) { - json[k].origin = 'remote'; - } - } - - Env.limits = json; - - Quota.applyCustomLimits(Env); - cb(); - }); -}; - -// The limits object contains storage limits for all the publicKey that have paid -// To each key is associated an object containing the 'limit' value and a 'note' explaining that limit -Quota.getUpdatedLimit = function (Env, safeKey, cb) { - Quota.updateCachedLimits(Env, function (err) { - if (err) { return void cb(err); } - - var limit = Env.limits[safeKey]; - - if (limit && typeof(limit.limit) === 'number') { - return void cb(void 0, [limit.limit, limit.plan, limit.note]); - } - - return void cb(void 0, [Env.defaultStorageLimit, '', '']); - }); -}; - diff --git a/lib/commands/upload.js b/lib/commands/upload.js deleted file mode 100644 index 5f38fd50f..000000000 --- a/lib/commands/upload.js +++ /dev/null @@ -1,98 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Upload = module.exports; -const Util = require("../common-util"); -const Pinning = require("./pin-rpc"); -const nThen = require("nthen"); -const Core = require("./core"); - -Upload.status = function (Env, safeKey, data, _cb) { // FIXME FILES - const filesize = data.size; - var cb = Util.once(Util.mkAsync(_cb)); - - // validate that the provided size is actually a positive number - if (typeof(filesize) !== 'number' && - filesize >= 0) { return void cb('E_INVALID_SIZE'); } - - nThen(function (w) { - // if the proposed upload size is within the regular limit - // jump ahead to the next block - if (filesize <= Env.maxUploadSize) { return; } - - // if larger uploads aren't explicitly enabled then reject them - if (typeof(Env.premiumUploadSize) !== 'number') { - w.abort(); - return void cb('TOO_LARGE'); - } - - // otherwise go and retrieve info about the user's quota - Pinning.getLimit(Env, safeKey, w(function (err, limit) { - if (err) { - w.abort(); - return void cb("E_BAD_LIMIT"); - } - - var plan = limit[1]; - - // see if they have a special plan, reject them if not - if (plan === '') { - w.abort(); - return void cb('TOO_LARGE'); - } - - // and that they're not over the greater limit - if (filesize >= Env.premiumUploadSize) { - w.abort(); - return void cb("TOO_LARGE"); - } - - // fallthrough will proceed to the next block - })); - }).nThen(function (w) { - var abortAndCB = Util.both(w.abort, cb); - Env.blobStore.status(safeKey, w(function (err, inProgress) { - // if there's an error something is weird - if (err) { return void abortAndCB(err); } - - // we cannot upload two things at once - if (inProgress) { return void abortAndCB(void 0, true); } - })); - }).nThen(function () { - // if yuo're here then there are no pending uploads - // check if you have space in your quota to upload something of this size - Pinning.getFreeSpace(Env, safeKey, function (e, free) { - if (e) { return void cb(e); } - if (filesize >= free) { return cb('NOT_ENOUGH_SPACE'); } - - var user = Core.getSession(Env.Sessions, safeKey); - user.pendingUploadSize = filesize; - user.currentUploadSize = 0; - user.linked = data.linked; - - cb(void 0, false); - }); - }); -}; - -Upload.upload = function (Env, safeKey, data, cb) { - Env.blobStore.uploadWs(safeKey, data?.chunk, cb); -}; - -Upload.cancel = function (Env, safeKey, arg, cb) { - Env.blobStore.cancel(safeKey, arg?.size, cb); -}; - -var completeUpload = function (owned) { - return function (Env, safeKey, arg, cb) { - Env.blobStore.closeBlobstage(safeKey); - var user = Core.getSession(Env.Sessions, safeKey); - var size = user.pendingUploadSize; - var linked = user.linked; - Env.completeUpload(safeKey, arg, Boolean(owned), size, linked, cb); - }; -}; - -Upload.complete = completeUpload(false); -Upload.complete_owned = completeUpload(true); diff --git a/lib/commands/users.js b/lib/commands/users.js deleted file mode 100644 index 5b05a333b..000000000 --- a/lib/commands/users.js +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Users = module.exports; - -const User = require('../storage/user'); -const Util = require("../common-util"); - -Users.getAll = (Env, cb) => { - User.getAll(Env, (err, data) => { - if (err) { return void cb(err); } - cb(null, data); - }); -}; - -Users.add = (Env, edPublic, data, adminKey, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - data.createdBy = adminKey; - data.time = +new Date(); - const safeKey = Util.escapeKeyCharacters(edPublic); - User.write(Env, safeKey, data, (err) => { - if (err) { return void cb(err); } - cb(); - }); -}; - -Users.delete = (Env, id, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - User.delete(Env, id, (err) => { - if (err && err !== 'ENOENT') { return void cb(err); } - cb(void 0, true); - }); -}; - -Users.read = (Env, edPublic, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - User.read(Env, edPublic, (err, data) => { - if (err) { return void cb(err); } - cb(void 0, data); - }); -}; - -Users.update = (Env, edPublic, changes, _cb) => { - const cb = Util.once(Util.mkAsync(_cb)); - Users.read(Env, edPublic, (err, data) => { - if (err === 'ENOENT') { return void cb(); } - if (err) { return void cb(err); } - if (typeof(changes) !== "object") { return void cb('EINVAL'); } - // User exists, update their data - var aborted = Object.keys(changes || {}).some((key) => { - if (changes[key] === false) { - delete data[key]; - return; - } - if (String(changes[key]).length > 300) { - cb('E_TOO_LONG'); - return true; - } - data[key] = changes[key]; - }); - if (aborted) { return; } - User.update(Env, edPublic, data, cb); - }); -}; - -// On password change, update the block -Users.checkUpdate = (Env, userData, newBlock, cb) => { - if (!Array.isArray(userData)) { userData = []; } - let edPublic = userData[1]; - if (!edPublic) { return void cb('INVALID_PUBLIC_KEY'); } - Users.read(Env, edPublic, (err, data) => { - if (err === 'ENOENT') { return void cb(); } - if (err) { return void cb(err); } - // User exists, update their block - data.block = newBlock; - User.update(Env, edPublic, data, cb); - }); -}; diff --git a/lib/common-hash.js b/lib/common-hash.js deleted file mode 100644 index 20dec3f80..000000000 --- a/lib/common-hash.js +++ /dev/null @@ -1,6 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -module.exports = require("../src/common/common-hash"); - diff --git a/lib/common-util.js b/lib/common-util.js deleted file mode 100644 index 497e25b28..000000000 --- a/lib/common-util.js +++ /dev/null @@ -1,5 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -module.exports = require("../src/common/common-util"); diff --git a/lib/crypto.js b/lib/crypto.js deleted file mode 100644 index 2d8e5862c..000000000 --- a/lib/crypto.js +++ /dev/null @@ -1,28 +0,0 @@ -// SPDX-FileCopyrightText: 2024 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -const Nacl = require('tweetnacl/nacl-fast'); -const CPCrypto = module.exports; -const plugins = require('./plugin-manager'); - -CPCrypto.init = (cb) => { - const crypto = {}; - crypto.open = (signedMsg, validateKey) => { - return Nacl.sign.open(signedMsg, validateKey); - }; - crypto.detachedVerify = (signedBuffer, signatureBuffer, validateKey) => { - return Nacl.sign.detached.verify(signedBuffer, signatureBuffer, validateKey); - }; - if (plugins.SODIUM && plugins.SODIUM.crypto) { - let c = plugins.SODIUM.crypto; - if (c.open) { crypto.open = c.open; } - if (c.detachedVerify) { crypto.detachedVerify = c.detachedVerify; } - } - - // Make async because we might need it later with libsodium's promise - // libsodium.ready.then(() => {}); - setTimeout(() => { - cb(void 0, crypto); - }); -}; diff --git a/lib/decrees-core.js b/lib/decrees-core.js deleted file mode 100644 index 3a40e5d6a..000000000 --- a/lib/decrees-core.js +++ /dev/null @@ -1,140 +0,0 @@ -// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team and contributors -// -// SPDX-License-Identifier: AGPL-3.0-or-later - -var Decrees = module.exports; -var Util = require("./common-util"); -var Fs = require("fs"); -var Path = require("path"); -var readFileBin = require("./stream-file").readFileBin; -var Schedule = require("./schedule"); -var Fse = require("fs-extra"); -var nThen = require("nthen"); - - -const Utils = Decrees.Utils = {}; -var isString = (str) => { - return typeof(str) === "string"; -}; -var isInteger = function (n) { - return !(typeof(n) !== 'number' || isNaN(n) || (n % 1) !== 0); -}; -Utils.args_isBoolean = function (args) { - return !(!Array.isArray(args) || typeof(args[0]) !== 'boolean'); -}; -Utils.args_isString = function (args) { - return !(!Array.isArray(args) || !isString(args[0])); -}; -Utils.args_isInteger = function (args) { - return !(!Array.isArray(args) || !isInteger(args[0])); -}; -Utils.args_isPositiveInteger = function (args) { - return Array.isArray(args) && isInteger(args[0]) && args[0] > 0; -}; - - -Decrees.create = (name, commands) => { - // [, , ,