cryptpad/lib/rpc.js

462 lines
16 KiB
JavaScript
Raw Normal View History

/*jshint esversion: 6 */
2018-01-29 11:40:09 +00:00
const nThen = require("nthen");
2019-03-29 12:45:54 +00:00
const Util = require("./common-util");
const mkEvent = Util.mkEvent;
const Core = require("./commands/core");
const Admin = require("./commands/admin-rpc");
const Pinning = require("./commands/pin-rpc");
const Quota = require("./commands/quota");
const Block = require("./commands/block");
const Metadata = require("./commands/metadata");
const Channel = require("./commands/channel");
const Upload = require("./commands/upload");
var RPC = module.exports;
const Store = require("../storage/file");
const BlobStore = require("../storage/blob");
var isUnauthenticatedCall = function (call) {
return [
'GET_FILE_SIZE',
2018-03-20 14:09:31 +00:00
'GET_METADATA',
'GET_MULTIPLE_FILE_SIZE',
'IS_CHANNEL_PINNED',
'IS_NEW_CHANNEL',
2018-01-29 11:40:09 +00:00
'GET_HISTORY_OFFSET',
'GET_DELETED_PADS',
'WRITE_PRIVATE_MESSAGE',
].indexOf(call) !== -1;
};
2017-05-31 10:51:26 +00:00
var isAuthenticatedCall = function (call) {
return [
'COOKIE',
2017-05-31 10:51:26 +00:00
'RESET',
'PIN',
'UNPIN',
'GET_HASH',
'GET_TOTAL_SIZE',
'UPDATE_LIMITS',
'GET_LIMIT',
'UPLOAD_STATUS',
2017-05-31 10:51:26 +00:00
'UPLOAD_COMPLETE',
2018-03-12 16:50:47 +00:00
'OWNED_UPLOAD_COMPLETE',
2017-05-31 10:51:26 +00:00
'UPLOAD_CANCEL',
'EXPIRE_SESSION',
'TRIM_OWNED_CHANNEL_HISTORY',
'CLEAR_OWNED_CHANNEL',
'REMOVE_OWNED_CHANNEL',
2018-03-20 10:16:18 +00:00
'REMOVE_PINS',
'TRIM_PINS',
'WRITE_LOGIN_BLOCK',
'REMOVE_LOGIN_BLOCK',
2019-03-27 16:00:28 +00:00
'ADMIN',
2019-08-28 10:40:35 +00:00
'SET_METADATA'
2017-05-31 10:51:26 +00:00
].indexOf(call) !== -1;
};
2020-01-23 19:32:10 +00:00
RPC.create = function (config, cb) {
var Log = config.log;
2019-04-08 16:11:36 +00:00
// load pin-store...
2019-04-08 16:11:36 +00:00
Log.silly('LOADING RPC MODULE');
2017-03-27 16:15:15 +00:00
2017-05-04 09:36:56 +00:00
var keyOrDefaultString = function (key, def) {
return typeof(config[key]) === 'string'? config[key]: def;
};
var WARN = function (e, output) {
if (e && output) {
Log.warn(e, {
output: output,
message: String(e),
stack: new Error(e).stack,
});
}
};
var Env = {
defaultStorageLimit: config.defaultStorageLimit,
maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024),
Sessions: {},
paths: {},
msgStore: config.store,
2020-01-23 19:32:10 +00:00
pinStore: undefined,
pinnedPads: {},
evPinnedPadsReady: mkEvent(true),
2019-04-08 16:11:36 +00:00
limits: {},
admins: [],
sessionExpirationInterval: undefined,
Log: Log,
WARN: WARN,
};
2019-04-08 16:11:36 +00:00
try {
Env.admins = (config.adminKeys || []).map(function (k) {
k = k.replace(/\/+$/, '');
var s = k.split('/');
return s[s.length-1];
});
} catch (e) {
console.error("Can't parse admin keys. Please update or fix your config.js file!");
}
var Sessions = Env.Sessions;
var paths = Env.paths;
2017-05-10 13:36:14 +00:00
var pinPath = paths.pin = keyOrDefaultString('pinPath', './pins');
paths.block = keyOrDefaultString('blockPath', './block');
2019-03-27 16:00:28 +00:00
paths.data = keyOrDefaultString('filePath', './datastore');
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.blob = keyOrDefaultString('blobPath', './blob');
2017-05-04 09:36:56 +00:00
var isUnauthenticateMessage = function (msg) {
2017-07-03 14:43:49 +00:00
return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]);
};
var handleUnauthenticatedMessage = function (msg, respond, nfwssCtx) {
2019-09-06 12:36:22 +00:00
Log.silly('LOG_RPC', msg[0]);
switch (msg[0]) {
case 'GET_HISTORY_OFFSET': {
if (typeof(msg[1]) !== 'object' || typeof(msg[1].channelName) !== 'string') {
return respond('INVALID_ARG_FORMAT', msg);
}
const msgHash = typeof(msg[1].msgHash) === 'string' ? msg[1].msgHash : undefined;
nfwssCtx.getHistoryOffset(nfwssCtx, msg[1].channelName, msgHash, (e, ret) => {
if (e) {
if (e.code !== 'ENOENT') {
WARN(e.stack, msg);
}
return respond(e.message);
}
respond(e, [null, ret, null]);
});
break;
}
case 'GET_FILE_SIZE':
return void Pinning.getFileSize(Env, msg[1], function (e, size) {
WARN(e, msg[1]);
respond(e, [null, size, null]);
});
2018-03-20 14:09:31 +00:00
case 'GET_METADATA':
return void Metadata.getMetadata(Env, msg[1], function (e, data) {
2018-03-20 14:09:31 +00:00
WARN(e, msg[1]);
respond(e, [null, data, null]);
});
case 'GET_MULTIPLE_FILE_SIZE':
return void Pinning.getMultipleFileSize(Env, msg[1], function (e, dict) {
if (e) {
WARN(e, dict);
return respond(e);
}
respond(e, [null, dict, null]);
});
2018-01-29 11:40:09 +00:00
case 'GET_DELETED_PADS':
return void Pinning.getDeletedPads(Env, msg[1], function (e, list) {
2018-01-29 11:40:09 +00:00
if (e) {
WARN(e, msg[1]);
return respond(e);
}
respond(e, [null, list, null]);
});
case 'IS_CHANNEL_PINNED':
return void Pinning.isChannelPinned(Env, msg[1], function (isPinned) {
respond(null, [null, isPinned, null]);
});
case 'IS_NEW_CHANNEL':
return void Channel.isNewChannel(Env, msg[1], function (e, isNew) {
respond(e, [null, isNew, null]);
});
case 'WRITE_PRIVATE_MESSAGE':
return void Channel.writePrivateMessage(Env, msg[1], nfwssCtx, function (e, output) {
respond(e, output);
});
default:
2019-04-08 16:11:36 +00:00
Log.warn("UNSUPPORTED_RPC_CALL", msg);
return respond('UNSUPPORTED_RPC_CALL', msg);
}
};
var rpc0 = function (ctx, data, respond) {
2017-04-24 10:10:12 +00:00
if (!Array.isArray(data)) {
2019-04-08 16:11:36 +00:00
Log.debug('INVALID_ARG_FORMET', data);
return void respond('INVALID_ARG_FORMAT');
2017-04-24 10:10:12 +00:00
}
2017-03-27 16:15:15 +00:00
if (!data.length) {
return void respond("INSUFFICIENT_ARGS");
2017-03-27 16:15:15 +00:00
} else if (data.length !== 1) {
2019-04-08 16:11:36 +00:00
Log.debug('UNEXPECTED_ARGUMENTS_LENGTH', data);
}
2017-03-27 16:15:15 +00:00
var msg = data[0].slice(0);
2017-04-03 17:24:57 +00:00
2017-04-24 10:10:12 +00:00
if (!Array.isArray(msg)) {
return void respond('INVALID_ARG_FORMAT');
}
if (isUnauthenticateMessage(msg)) {
return handleUnauthenticatedMessage(msg, respond, ctx);
}
2017-03-27 16:15:15 +00:00
var signature = msg.shift();
var publicKey = msg.shift();
// make sure a user object is initialized in the cookie jar
if (publicKey) {
Core.getSession(Sessions, publicKey);
} else {
2019-04-08 16:11:36 +00:00
Log.debug("NO_PUBLIC_KEY_PROVIDED", publicKey);
}
var cookie = msg[0];
if (!Core.isValidCookie(Sessions, publicKey, cookie)) {
2017-03-27 16:15:15 +00:00
// no cookie is fine if the RPC is to get a cookie
if (msg[1] !== 'COOKIE') {
2017-03-27 16:15:15 +00:00
return void respond('NO_COOKIE');
}
}
var serialized = JSON.stringify(msg);
2017-04-05 15:28:04 +00:00
if (!(serialized && typeof(publicKey) === 'string')) {
2017-03-27 16:15:15 +00:00
return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY');
}
2017-05-31 10:51:26 +00:00
if (isAuthenticatedCall(msg[1])) {
if (Core.checkSignature(Env, serialized, signature, publicKey) !== true) {
2017-05-31 10:51:26 +00:00
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
}
} else if (msg[1] !== 'UPLOAD') {
2019-04-08 16:11:36 +00:00
Log.warn('INVALID_RPC_CALL', msg[1]);
return void respond("INVALID_RPC_CALL");
2017-03-27 16:15:15 +00:00
}
var safeKey = Util.escapeKeyCharacters(publicKey);
/* If you have gotten this far, you have signed the message with the
public key which you provided.
We can safely modify the state for that key
OR it's an unauthenticated call, which must not modify the state
for that key in a meaningful way.
*/
// discard validated cookie from message
msg.shift();
var Respond = function (e, msg) {
var session = Sessions[safeKey];
var token = session? session.tokens.slice(-1)[0]: '';
var cookie = Core.makeCookie(token).join('|');
respond(e ? String(e): e, [cookie].concat(typeof(msg) !== 'undefined' ?msg: []));
};
if (typeof(msg) !== 'object' || !msg.length) {
return void Respond('INVALID_MSG');
}
var handleMessage = function () {
2019-04-08 16:11:36 +00:00
Log.silly('LOG_RPC', msg[0]);
switch (msg[0]) {
2017-04-07 09:37:19 +00:00
case 'COOKIE': return void Respond(void 0);
case 'RESET':
return Pinning.resetUserPins(Env, safeKey, msg[1], function (e, hash) {
//WARN(e, hash);
2017-04-07 09:37:19 +00:00
return void Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'PIN':
return Pinning.pinChannel(Env, safeKey, msg[1], function (e, hash) {
WARN(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'UNPIN':
return Pinning.unpinChannel(Env, safeKey, msg[1], function (e, hash) {
WARN(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
case 'GET_HASH':
return void Pinning.getHash(Env, safeKey, function (e, hash) {
WARN(e, hash);
2017-04-07 08:30:40 +00:00
Respond(e, hash);
2017-04-03 17:24:57 +00:00
});
2017-04-28 09:46:13 +00:00
case 'GET_TOTAL_SIZE': // TODO cache this, since it will get called quite a bit
return Pinning.getTotalSize(Env, safeKey, function (e, size) {
if (e) {
WARN(e, safeKey);
return void Respond(e);
}
2017-04-07 08:09:59 +00:00
Respond(e, size);
2017-03-15 14:55:25 +00:00
});
2017-05-11 14:12:44 +00:00
case 'UPDATE_LIMITS':
return void Quota.updateLimits(Env, config, safeKey, function (e, limit) {
if (e) {
WARN(e, limit);
return void Respond(e);
}
2017-05-11 14:12:44 +00:00
Respond(void 0, limit);
});
case 'GET_LIMIT':
return void Pinning.getLimit(Env, safeKey, function (e, limit) {
if (e) {
WARN(e, limit);
return void Respond(e);
}
2017-05-11 14:12:44 +00:00
Respond(void 0, limit);
2017-04-28 09:46:13 +00:00
});
2017-07-05 14:00:54 +00:00
case 'EXPIRE_SESSION':
return void setTimeout(function () {
Core.expireSession(Sessions, safeKey);
2017-07-05 14:00:54 +00:00
Respond(void 0, "OK");
});
2017-07-12 16:54:08 +00:00
case 'CLEAR_OWNED_CHANNEL':
return void Channel.clearOwnedChannel(Env, msg[1], publicKey, function (e, response) {
2017-07-12 16:54:08 +00:00
if (e) { return void Respond(e); }
Respond(void 0, response);
});
case 'REMOVE_OWNED_CHANNEL':
return void Channel.removeOwnedChannel(Env, msg[1], publicKey, function (e) {
if (e) { return void Respond(e); }
Respond(void 0, "OK");
});
case 'TRIM_OWNED_CHANNEL_HISTORY':
return void Channel.removeOwnedChannelHistory(Env, msg[1], publicKey, msg[2], function (e) {
if (e) { return void Respond(e); }
Respond(void 0, 'OK');
});
2018-03-20 10:16:18 +00:00
case 'REMOVE_PINS':
return void Pinning.removePins(Env, safeKey, function (e) {
2018-03-20 10:16:18 +00:00
if (e) { return void Respond(e); }
2018-03-21 17:27:20 +00:00
Respond(void 0, "OK");
2018-03-20 10:16:18 +00:00
});
case 'TRIM_PINS':
return void Pinning.trimPins(Env, safeKey, function (e) {
if (e) { return void Respond(e); }
Respond(void 0, "OK");
});
2017-04-28 15:11:50 +00:00
case 'UPLOAD':
return void Env.blobStore.upload(safeKey, msg[1], function (e, len) {
WARN(e, len);
2017-05-04 09:36:56 +00:00
Respond(e, len);
});
case 'UPLOAD_STATUS':
var filesize = msg[1];
return void Upload.upload_status(Env, safeKey, filesize, function (e, yes) {
if (!e && !yes) {
// no pending uploads, set the new size
var user = Core.getSession(Sessions, safeKey);
user.pendingUploadSize = filesize;
user.currentUploadSize = 0;
}
Respond(e, yes);
2017-05-04 09:36:56 +00:00
});
case 'UPLOAD_COMPLETE':
return void Env.blobStore.complete(safeKey, msg[1], function (e, hash) {
WARN(e, hash);
2017-05-04 09:36:56 +00:00
Respond(e, hash);
2017-04-28 15:11:50 +00:00
});
2018-03-12 16:50:47 +00:00
case 'OWNED_UPLOAD_COMPLETE':
return void Env.blobStore.completeOwned(safeKey, msg[1], function (e, blobId) {
2018-03-12 16:50:47 +00:00
WARN(e, blobId);
Respond(e, blobId);
});
2017-05-04 09:36:56 +00:00
case 'UPLOAD_CANCEL':
// msg[1] is fileSize
// if we pass it here, we can start an upload right away without calling
// UPLOAD_STATUS again
return void Env.blobStore.cancel(safeKey, msg[1], function (e) {
WARN(e, 'UPLOAD_CANCEL');
2017-04-28 15:11:50 +00:00
Respond(e);
});
case 'WRITE_LOGIN_BLOCK':
return void Block.writeLoginBlock(Env, msg[1], function (e) {
if (e) {
WARN(e, 'WRITE_LOGIN_BLOCK');
return void Respond(e);
}
Respond(e);
});
case 'REMOVE_LOGIN_BLOCK':
return void Block.removeLoginBlock(Env, msg[1], function (e) {
2018-06-19 15:17:56 +00:00
if (e) {
WARN(e, 'REMOVE_LOGIN_BLOCK');
return void Respond(e);
}
Respond(e);
});
2019-03-27 16:00:28 +00:00
case 'ADMIN':
return void Admin.command(Env, ctx, safeKey, config, msg[1], function (e, result) {
2019-03-27 16:00:28 +00:00
if (e) {
WARN(e, result);
return void Respond(e);
}
Respond(void 0, result);
});
2019-08-28 10:40:35 +00:00
case 'SET_METADATA':
return void Metadata.setMetadata(Env, msg[1], publicKey, function (e, data) {
2019-08-28 10:40:35 +00:00
if (e) {
WARN(e, data);
return void Respond(e);
}
Respond(void 0, data);
});
default:
return void Respond('UNSUPPORTED_RPC_CALL', msg);
}
};
handleMessage(true);
};
2020-01-23 19:32:10 +00:00
var rpc = function (ctx, data, respond) {
try {
return rpc0(ctx, data, respond);
} catch (e) {
console.log("Error from RPC with data " + JSON.stringify(data));
console.log(e.stack);
}
};
2017-05-11 14:12:44 +00:00
var updateLimitDaily = function () {
Quota.updateLimits(Env, config, undefined, function (e) {
if (e) {
WARN('limitUpdate', e);
}
2017-05-11 14:12:44 +00:00
});
};
Quota.applyCustomLimits(Env, config);
2017-05-11 14:12:44 +00:00
updateLimitDaily();
setInterval(updateLimitDaily, 24*3600*1000);
Pinning.loadChannelPins(Env);
nThen(function (w) {
Store.create({
filePath: pinPath,
}, w(function (s) {
Env.pinStore = s;
}));
BlobStore.create({
blobPath: config.blobPath,
blobStagingPath: config.blobStagingPath,
archivePath: config.archivePath,
getSession: function (safeKey) {
return Core.getSession(Sessions, safeKey);
},
}, w(function (err, blob) {
if (err) { throw new Error(err); }
Env.blobStore = blob;
}));
}).nThen(function () {
cb(void 0, rpc);
// expire old sessions once per minute
// XXX allow for graceful shutdown
Env.sessionExpirationInterval = setInterval(function () {
Core.expireSessions(Sessions);
}, Core.SESSION_EXPIRATION_TIME);
2017-04-03 17:24:57 +00:00
});
};