mirror of https://github.com/xwiki-labs/cryptpad
438 lines
14 KiB
JavaScript
438 lines
14 KiB
JavaScript
// SPDX-FileCopyrightText: 2023 XWiki CryptPad Team <contact@cryptpad.org> and contributors
|
|
//
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
const { existsSync, readdirSync } = require('node:fs');
|
|
|
|
const Crypto = require('crypto');
|
|
const WriteQueue = require("./write-queue");
|
|
const BatchRead = require("./batch-read");
|
|
|
|
const Keys = require("./keys");
|
|
const Core = require("./commands/core");
|
|
|
|
const Quota = require("./commands/quota");
|
|
const Util = require("./common-util");
|
|
const Package = require("../package.json");
|
|
const Default = require("./defaults");
|
|
const Path = require("path");
|
|
|
|
const plugins = require('./plugin-manager');
|
|
|
|
const Nacl = require("tweetnacl/nacl-fast");
|
|
|
|
var canonicalizeOrigin = function (s) {
|
|
if (typeof(s) === 'undefined') { return; }
|
|
return (s || '').trim().replace(/\/+$/, '');
|
|
};
|
|
|
|
var isValidPort = function (p) {
|
|
return typeof(p) === 'number' && p < 65535;
|
|
};
|
|
|
|
var deriveSandboxOrigin = function (unsafe, port) {
|
|
var url = new URL(unsafe);
|
|
url.port = port;
|
|
return url.origin;
|
|
};
|
|
|
|
var isRecentVersion = function () {
|
|
var R = Default.recommendedVersion;
|
|
var V = process.version;
|
|
if (typeof(V) !== 'string') { return false; }
|
|
var parts = V.replace(/^v/, '').split('.').map(Number);
|
|
if (parts.length < 3) { return false; }
|
|
if (!parts.every(n => typeof(n) === 'number' && !isNaN(n))) {
|
|
return false;
|
|
}
|
|
if (parts[0] < R[0]) { return false; }
|
|
if (parts[0] > R[0]) { return true; }
|
|
|
|
// v16
|
|
if (parts[1] < R[1]) { return false; }
|
|
if (parts[1] > R[1]) { return true; }
|
|
if (parts[2] >= R[2]) { return true; }
|
|
|
|
return false;
|
|
};
|
|
|
|
const getInstalledOOVersions = function() {
|
|
if (!existsSync('www/common/onlyoffice/dist')) {
|
|
return [];
|
|
}
|
|
|
|
return readdirSync('www/common/onlyoffice/dist');
|
|
};
|
|
|
|
module.exports.create = function (config) {
|
|
var httpUnsafeOrigin = canonicalizeOrigin(config.httpUnsafeOrigin);
|
|
|
|
var httpSafeOrigin;
|
|
var NO_SANDBOX = false;
|
|
var httpSafePort;
|
|
var httpPort = isValidPort(config.httpPort)? config.httpPort: 3000;
|
|
|
|
if (typeof(config.httpSafeOrigin) !== 'string') {
|
|
NO_SANDBOX = true;
|
|
if (typeof(config.httpSafePort) !== 'number') { httpSafePort = httpPort + 1; }
|
|
httpSafeOrigin = deriveSandboxOrigin(httpUnsafeOrigin, httpSafePort);
|
|
} else {
|
|
httpSafeOrigin = canonicalizeOrigin(config.httpSafeOrigin);
|
|
}
|
|
|
|
if (typeof(config.websocketPort) !== 'number') {
|
|
config.websocketPort = 3003;
|
|
}
|
|
|
|
var permittedEmbedders = config.permittedEmbedders;
|
|
if (typeof(permittedEmbedders) === 'string') {
|
|
permittedEmbedders = permittedEmbedders.trim();
|
|
}
|
|
|
|
const curve = Nacl.box.keyPair();
|
|
|
|
const Env = {
|
|
plugins: plugins,
|
|
logFeedback: Boolean(config.logFeedback),
|
|
mainPages: config.mainPages || Default.mainPages(),
|
|
|
|
protocol: new URL(httpUnsafeOrigin).protocol,
|
|
|
|
fileHost: config.fileHost || undefined,
|
|
NO_SANDBOX: NO_SANDBOX,
|
|
httpSafePort: httpSafePort,
|
|
websocketPort: config.websocketPort,
|
|
|
|
accounts_api: config.accounts_api || undefined, // this simplifies integration with an accounts page
|
|
|
|
shouldUpdateNode: !isRecentVersion(),
|
|
|
|
version: Package.version,
|
|
installMethod: config.installMethod || undefined,
|
|
|
|
httpUnsafeOrigin: httpUnsafeOrigin,
|
|
httpSafeOrigin: httpSafeOrigin,
|
|
permittedEmbedders: typeof(permittedEmbedders) === 'string' && permittedEmbedders? permittedEmbedders: httpSafeOrigin,
|
|
|
|
removeDonateButton: config.removeDonateButton,
|
|
httpPort: isValidPort(config.httpPort)? config.httpPort: 3000,
|
|
httpAddress: typeof(config.httpAddress) === 'string'? config.httpAddress: 'localhost',
|
|
websocketPath: config.externalWebsocketURL,
|
|
logIP: config.logIP,
|
|
|
|
OFFLINE_MODE: false,
|
|
FRESH_KEY: '',
|
|
FRESH_MODE: true,
|
|
DEV_MODE: false,
|
|
configCache: {},
|
|
broadcastCache: {},
|
|
|
|
officeHeadersCache: undefined,
|
|
standardHeadersCache: undefined,
|
|
apiHeadersCache: undefined,
|
|
|
|
flushCache: function () {
|
|
Env.FRESH_KEY = +new Date();
|
|
if (!(Env.DEV_MODE || Env.FRESH_MODE)) { Env.FRESH_MODE = true; }
|
|
Env.cacheFlushed.fire();
|
|
if (!Env.Log) { return; }
|
|
Env.Log.info("UPDATING_FRESH_KEY", Env.FRESH_KEY);
|
|
},
|
|
|
|
Log: undefined,
|
|
// store
|
|
id: Crypto.randomBytes(8).toString('hex'),
|
|
|
|
launchTime: +new Date(),
|
|
|
|
enableProfiling: false,
|
|
profilingWindow: 10000,
|
|
bytesWritten: 0,
|
|
|
|
inactiveTime: config.inactiveTime,
|
|
archiveRetentionTime: config.archiveRetentionTime,
|
|
accountRetentionTime: config.accountRetentionTime,
|
|
|
|
adminEmail: config.adminEmail,
|
|
supportMailbox: config.supportMailboxPublicKey,
|
|
supportMailboxKey: undefined,
|
|
|
|
metadata_cache: {},
|
|
channel_cache: {},
|
|
cache_checks: {},
|
|
|
|
queueStorage: WriteQueue(),
|
|
queueDeletes: WriteQueue(),
|
|
queueValidation: WriteQueue(),
|
|
queueMetadata: WriteQueue(),
|
|
|
|
batchIndexReads: BatchRead("HK_GET_INDEX"),
|
|
batchMetadata: BatchRead('GET_METADATA'),
|
|
batchRegisteredUsers: BatchRead("GET_REGISTERED_USERS"),
|
|
batchDiskUsage: BatchRead('GET_DISK_USAGE'),
|
|
batchUserPins: BatchRead('LOAD_USER_PINS'),
|
|
batchTotalSize: BatchRead('GET_TOTAL_SIZE'),
|
|
batchAccountQuery: BatchRead("QUERY_ACCOUNT_SERVER"),
|
|
|
|
intervals: {},
|
|
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
|
|
premiumUploadSize: false, // overridden below...
|
|
Sessions: {},
|
|
paths: {},
|
|
//msgStore: config.store,
|
|
|
|
// /api/broadcast
|
|
lastBroadcastHash: '',
|
|
surveyURL: undefined,
|
|
maintenance: undefined,
|
|
|
|
netfluxUsers: {},
|
|
|
|
pinStore: undefined,
|
|
|
|
limits: {},
|
|
admins: [],
|
|
installToken: undefined,
|
|
WARN: function (e, output) { // TODO deprecate this
|
|
if (!Env.Log) { return; }
|
|
if (e && output) {
|
|
Env.Log.warn(e, {
|
|
output: output,
|
|
message: String(e),
|
|
stack: new Error(e).stack,
|
|
});
|
|
}
|
|
},
|
|
|
|
// as of 4.14.0 you need to opt-in to remote embedding.
|
|
enableEmbedding: false,
|
|
|
|
/* FIXME restrictRegistration is initialized as false and then overridden by admin decree
|
|
There is a narrow window in which someone could register before the server updates this value.
|
|
See also the cached 'restrictRegistration' value in server.js#serveConfig
|
|
*/
|
|
restrictRegistration: false,
|
|
allowSubscriptions: config.allowSubscriptions === true,
|
|
blockDailyCheck: config.blockDailyCheck === true,
|
|
|
|
consentToContact: false,
|
|
listMyInstance: false,
|
|
provideAggregateStatistics: false,
|
|
updateAvailable: undefined,
|
|
|
|
instanceName: {},
|
|
instanceDescription: {},
|
|
instanceJurisdiction: {},
|
|
instanceNotice: {},
|
|
|
|
myDomain: config.myDomain,
|
|
mySubdomain: config.mySubdomain, // only exists for the accounts integration
|
|
customLimits: {},
|
|
// FIXME this attribute isn't in the default conf
|
|
// but it is referenced in Quota
|
|
domain: config.domain,
|
|
|
|
maxWorkers: undefined,
|
|
disableIntegratedTasks: config.disableIntegratedTasks || false,
|
|
disableIntegratedEviction: typeof(config.disableIntegratedEviction) === 'undefined'? true: config.disableIntegratedEviction,
|
|
lastEviction: +new Date(),
|
|
evictionReport: {},
|
|
commandTimers: {},
|
|
|
|
sso: config.sso,
|
|
enforceMFA: config.enforceMFA,
|
|
|
|
onlyOffice: {
|
|
availableVersions: getInstalledOOVersions()
|
|
},
|
|
|
|
// initialized as undefined
|
|
bearerSecret: void 0,
|
|
curvePrivate: curve.secretKey,
|
|
curvePublic: Nacl.util.encodeBase64(curve.publicKey),
|
|
|
|
selfDestructTo: {},
|
|
};
|
|
|
|
(function () {
|
|
var max = config.maxWorkers;
|
|
// if the supplied value is not a positive number, leave maxWorkers undefined
|
|
// one worker will be created for each CPU core
|
|
if (typeof(max) !== 'number' || isNaN(max) || max < 1) { return; }
|
|
Env.maxWorkers = max;
|
|
}());
|
|
|
|
(function () {
|
|
// mode can be FRESH (default), DEV, or PACKAGE
|
|
if (process.env.PACKAGE) {
|
|
// `PACKAGE=1 node server` uses the version string from package.json as the cache string
|
|
//console.log("PACKAGE MODE ENABLED");
|
|
Env.FRESH_MODE = false;
|
|
Env.DEV_MODE = false;
|
|
} else if (process.env.DEV) {
|
|
// `DEV=1 node server` will use a random cache string on every page reload
|
|
//console.log("DEV MODE ENABLED");
|
|
Env.FRESH_MODE = false;
|
|
Env.DEV_MODE = true;
|
|
} else {
|
|
// `FRESH=1 node server` will set a random cache string when the server is launched
|
|
// and use it for the process lifetime or until it is reset from the admin panel
|
|
//console.log("FRESH MODE ENABLED");
|
|
Env.FRESH_KEY = +new Date();
|
|
}
|
|
|
|
// Offline mode is mostly for development. It lets us test clientside cache and offline support
|
|
if (process.env.OFFLINE) { Env.OFFLINE_MODE = true; }
|
|
}());
|
|
|
|
Env.checkCache = function (channel) {
|
|
var f = Env.cache_checks[channel] || Util.throttle(function () {
|
|
delete Env.cache_checks[channel];
|
|
if (Env.channel_cache[channel]) { return; }
|
|
delete Env.metadata_cache[channel];
|
|
}, 30000);
|
|
f();
|
|
};
|
|
|
|
(function () {
|
|
var custom = config.customLimits;
|
|
if (!custom) { return; }
|
|
|
|
var stored = Env.customLimits;
|
|
|
|
Object.keys(custom).forEach(function (k) {
|
|
var unsafeKey = Keys.canonicalize(k);
|
|
|
|
if (!unsafeKey) {
|
|
console.log("INVALID_CUSTOM_LIMIT_ID", {
|
|
message: "A custom quota upgrade was provided via your config with an invalid identifier. It will be ignored.",
|
|
key: k,
|
|
value: custom[k],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (stored[unsafeKey]) {
|
|
console.log("INVALID_CUSTOM_LIMIT_DUPLICATED", {
|
|
message: "A duplicated custom quota upgrade was provided via your config which would have overridden an existing value. It will be ignored.",
|
|
key: k,
|
|
value: custom[k],
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (!Quota.isValidLimit(custom[k])) {
|
|
console.log("INVALID_CUSTOM_LIMIT_VALUE", {
|
|
message: "A custom quota upgrade was provided via your config with an invalid value. It will be ignored.",
|
|
key: k,
|
|
value: custom[k],
|
|
});
|
|
return;
|
|
}
|
|
|
|
var limit = stored[unsafeKey] = Util.clone(custom[k]);
|
|
limit.origin = 'config';
|
|
});
|
|
}());
|
|
|
|
(function () {
|
|
var pes = config.premiumUploadSize;
|
|
if (!isNaN(pes) && pes >= Env.maxUploadSize) {
|
|
Env.premiumUploadSize = pes;
|
|
}
|
|
}());
|
|
|
|
var paths = Env.paths;
|
|
|
|
var keyOrDefaultString = function (key, def) {
|
|
return Path.resolve(typeof(config[key]) === 'string'? config[key]: def);
|
|
};
|
|
|
|
Env.incrementBytesWritten = function (n) {
|
|
if (!Env.enableProfiling) { return; }
|
|
if (!n || typeof(n) !== 'number' || n < 0) { return; }
|
|
Env.bytesWritten += n;
|
|
setTimeout(function () {
|
|
Env.bytesWritten -= n;
|
|
}, Env.profilingWindow);
|
|
};
|
|
|
|
paths.pin = keyOrDefaultString('pinPath', './pins');
|
|
paths.block = keyOrDefaultString('blockPath', './block');
|
|
paths.data = keyOrDefaultString('filePath', './datastore');
|
|
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
|
|
paths.blob = keyOrDefaultString('blobPath', './blob');
|
|
paths.decree = keyOrDefaultString('decreePath', './data/');
|
|
paths.base = keyOrDefaultString('base', './data');
|
|
paths.archive = keyOrDefaultString('archivePath', './data/archive');
|
|
paths.task = keyOrDefaultString('taskPath', './tasks');
|
|
|
|
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit >= 0?
|
|
config.defaultStorageLimit:
|
|
Core.DEFAULT_LIMIT;
|
|
|
|
try {
|
|
Env.admins = (config.adminKeys || []).map(function (k) {
|
|
try {
|
|
return Keys.canonicalize(k);
|
|
} catch (err) {
|
|
return;
|
|
}
|
|
}).filter(Boolean);
|
|
} catch (e) {
|
|
console.error("Can't parse admin keys. Please update or fix your config.js file!");
|
|
}
|
|
|
|
try {
|
|
let moderators = require('./commands/moderators').getKeysSync(Env);
|
|
Env.moderators = moderators.map(safeKey => {
|
|
return Util.unescapeKeyCharacters(safeKey);
|
|
}) || [];
|
|
} catch (e) {
|
|
console.log(e);
|
|
console.error("Can't parse support keys.");
|
|
}
|
|
|
|
Env.envUpdated = Util.mkEvent();
|
|
Env.cacheFlushed = Util.mkEvent();
|
|
|
|
return Env;
|
|
};
|
|
|
|
// don't serialize these things
|
|
const BAD = [
|
|
'Log',
|
|
'envUpdated',
|
|
'cacheFlushed',
|
|
'evictionReports',
|
|
'commandTimers',
|
|
'metadata_cache',
|
|
'channel_cache',
|
|
'cache_checks',
|
|
'intervals',
|
|
'Sessions',
|
|
'netfluxUsers',
|
|
'limits',
|
|
'customLimits',
|
|
'scheduleDecree',
|
|
|
|
'httpServer',
|
|
|
|
'pinStore',
|
|
'msgStore',
|
|
'store',
|
|
'blobStore',
|
|
];
|
|
|
|
module.exports.serialize = function (Env) {
|
|
return JSON.stringify(Env, function (key, value) {
|
|
if (value === Env) { return value; }
|
|
if (BAD.includes(key)) { return; }
|
|
|
|
if (typeof(value) === 'function') { return; }
|
|
//console.log('serializing', { key, value, });
|
|
if (Util.isCircular(value)) { return; }
|
|
return value;
|
|
});
|
|
};
|