diff --git a/CHANGELOG.md b/CHANGELOG.md index fcfdee9b1..41659802b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,42 @@ +# L release (3.11.0) + +## Goals + +* major server refactor to prepare for: + * trim-history + * allow lists + +## Update notes + +* dropped support for retainData + * archives are on by default +* you will need a new chainpad server + +## Features + +* restyled corner popup +* cool new scheduler library + * operations on channels are queued +* trim-history rpc +* unified historykeeper and rpc +* more visible styles for unanswered support tickets +* hidden hashes/safe links + * new "security" tab in settings +* queue'd popups + * reconnect alert +* link to user profile in notifications +* prompt anonymous users to register when viewing a profile +* spreadsheets + * reconnecting spreadsheets + * faster spreadsheets +* don't hijack chat cursor +* friends are now "contacts" + +## Bug fixes + +* friend request/accept race condition +* throw errors in 'mkAsync' if no function is passed + # Kouprey release (3.10.0) ## Goals diff --git a/lib/commands/admin-rpc.js b/lib/commands/admin-rpc.js index e3f0c6c58..3c328611b 100644 --- a/lib/commands/admin-rpc.js +++ b/lib/commands/admin-rpc.js @@ -2,6 +2,8 @@ const BatchRead = require("../batch-read"); const nThen = require("nthen"); const getFolderSize = require("get-folder-size"); +const Util = require("../common-util"); + var Fs = require("fs"); var Admin = module.exports; @@ -90,9 +92,10 @@ var getDiskUsage = function (Env, cb) { }); }; -Admin.command = function (Env, Server, publicKey, data, cb) { +Admin.command = function (Env, safeKey, data, cb, Server) { var admins = Env.admins; - if (admins.indexOf(publicKey) === -1) { + var unsafeKey = Util.unescapeKeyCharacters(safeKey); + if (admins.indexOf(unsafeKey) === -1) { return void cb("FORBIDDEN"); } diff --git a/lib/commands/block.js b/lib/commands/block.js index 90837745e..3a264c167 100644 --- a/lib/commands/block.js +++ b/lib/commands/block.js @@ -31,7 +31,7 @@ const Util = require("../common-util"); 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) { // FIXME BLOCKS +var validateLoginBlock = function (Env, publicKey, signature, block, cb) { // FIXME BLOCKS // convert the public key to a Uint8Array and validate it if (typeof(publicKey) !== 'string') { return void cb('E_INVALID_KEY'); } @@ -86,13 +86,13 @@ var createLoginBlockPath = function (Env, publicKey) { // FIXME BLOCKS return Path.join(Env.paths.block, safeKey.slice(0, 2), safeKey); }; -Block.writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS +Block.writeLoginBlock = function (Env, safeKey, msg, cb) { // FIXME BLOCKS //console.log(msg); var publicKey = msg[0]; var signature = msg[1]; var block = msg[2]; - Block.validateLoginBlock(Env, publicKey, signature, block, function (e, validatedBlock) { + validateLoginBlock(Env, publicKey, signature, block, function (e, validatedBlock) { if (e) { return void cb(e); } if (!(validatedBlock instanceof Uint8Array)) { return void cb('E_INVALID_BLOCK'); } @@ -141,12 +141,12 @@ Block.writeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS information, we can just sign some constant and use that as proof. */ -Block.removeLoginBlock = function (Env, msg, cb) { // FIXME BLOCKS +Block.removeLoginBlock = function (Env, safeKey, msg, cb) { // FIXME BLOCKS var publicKey = msg[0]; var signature = msg[1]; var block = Nacl.util.decodeUTF8('DELETE_BLOCK'); // clients and the server will have to agree on this constant - Block.validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) { + validateLoginBlock(Env, publicKey, signature, block, function (e /*::, validatedBlock */) { if (e) { return void cb(e); } // derive the filepath var path = createLoginBlockPath(Env, publicKey); diff --git a/lib/commands/channel.js b/lib/commands/channel.js index 318911bd8..bbb83b45a 100644 --- a/lib/commands/channel.js +++ b/lib/commands/channel.js @@ -160,7 +160,7 @@ Channel.isNewChannel = function (Env, channel, cb) { Otherwise behaves the same as sending to a channel */ -Channel.writePrivateMessage = function (Env, args, Server, cb) { +Channel.writePrivateMessage = function (Env, args, cb, Server) { // XXX odd signature var channelId = args[0]; var msg = args[1]; diff --git a/lib/commands/core.js b/lib/commands/core.js index cbcf291bb..d7add69b4 100644 --- a/lib/commands/core.js +++ b/lib/commands/core.js @@ -184,5 +184,7 @@ Core.isPendingOwner = function (metadata, unsafeKey) { return metadata.pending_owners.indexOf(unsafeKey) !== -1; }; - +Core.haveACookie = function (Env, safeKey, cb) { + cb(); +}; diff --git a/lib/commands/metadata.js b/lib/commands/metadata.js index fe05aa40d..09cf1f1d6 100644 --- a/lib/commands/metadata.js +++ b/lib/commands/metadata.js @@ -8,10 +8,12 @@ const Core = require("./core"); const Util = require("../common-util"); const batchMetadata = BatchRead("GET_METADATA"); -Data.getMetadata = function (Env, channel, cb) { +Data.getMetadata = function (Env, channel, cb/* , Server */) { if (!Core.isValidId(channel)) { return void cb('INVALID_CHAN'); } if (channel.length !== 32) { return cb("INVALID_CHAN_LENGTH"); } + // XXX get metadata from the server cache if it is available + // Server isn't always passed, though... batchMetadata(channel, cb, function (done) { var ref = {}; var lineHandler = Meta.createLineHandler(ref, Env.Log.error); diff --git a/lib/commands/pin-rpc.js b/lib/commands/pin-rpc.js index 663faa58b..2478910a7 100644 --- a/lib/commands/pin-rpc.js +++ b/lib/commands/pin-rpc.js @@ -454,10 +454,10 @@ Pinning.loadChannelPins = function (Env) { Pinning.isChannelPinned = function (Env, channel, cb) { Env.evPinnedPadsReady.reg(() => { if (Env.pinnedPads[channel] && Object.keys(Env.pinnedPads[channel]).length) { - cb(true); + cb(void 0, true); } else { - delete Env.pinnedPads[channel]; - cb(false); + delete Env.pinnedPads[channel]; // XXX WAT + cb(void 0, false); } }); }; diff --git a/lib/commands/quota.js b/lib/commands/quota.js index b74195821..654802f35 100644 --- a/lib/commands/quota.js +++ b/lib/commands/quota.js @@ -2,7 +2,6 @@ /* globals Buffer*/ const Quota = module.exports; -const Core = require("./core"); const Util = require("../common-util"); const Package = require('../../package.json'); const Https = require("https"); @@ -35,25 +34,12 @@ Quota.applyCustomLimits = function (Env) { }); }; -// 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 -// XXX maybe the use case with a publicKey should be a different command that calls this? -Quota.updateLimits = function (Env, publicKey, cb) { // FIXME BATCH?S - +Quota.updateCachedLimits = function (Env, cb) { if (Env.adminEmail === false) { Quota.applyCustomLimits(Env); if (Env.allowSubscriptions === false) { return; } throw new Error("allowSubscriptions must be false if adminEmail is false"); } - if (typeof cb !== "function") { cb = function () {}; } - - var defaultLimit = typeof(Env.defaultStorageLimit) === 'number'? - Env.defaultStorageLimit: Core.DEFAULT_LIMIT; - - var userId; - if (publicKey) { - userId = Util.unescapeKeyCharacters(publicKey); - } var body = JSON.stringify({ domain: Env.myDomain, @@ -86,14 +72,7 @@ Quota.updateLimits = function (Env, publicKey, cb) { // FIXME BATCH?S var json = JSON.parse(str); Env.limits = json; Quota.applyCustomLimits(Env); - - var l; - if (userId) { - var limit = Env.limits[userId]; - l = limit && typeof limit.limit === "number" ? - [limit.limit, limit.plan, limit.note] : [defaultLimit, '', '']; - } - cb(void 0, l); + cb(void 0); } catch (e) { cb(e); } @@ -109,4 +88,19 @@ Quota.updateLimits = function (Env, publicKey, cb) { // FIXME BATCH?S req.end(body); }; +// 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) { // FIXME BATCH?S + 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/rpc.js b/lib/rpc.js index 006c9b1af..e7a8242d3 100644 --- a/lib/rpc.js +++ b/lib/rpc.js @@ -18,98 +18,30 @@ var RPC = module.exports; const Store = require("../storage/file"); const BlobStore = require("../storage/blob"); -const UNAUTHENTICATED_CALLS = [ - 'GET_FILE_SIZE', - 'GET_METADATA', - 'GET_MULTIPLE_FILE_SIZE', - 'IS_CHANNEL_PINNED', - 'IS_NEW_CHANNEL', - 'GET_DELETED_PADS', - 'WRITE_PRIVATE_MESSAGE', -]; - -var isUnauthenticatedCall = function (call) { - return UNAUTHENTICATED_CALLS.indexOf(call) !== -1; -}; - -const AUTHENTICATED_CALLS = [ - 'COOKIE', - 'RESET', - 'PIN', - 'UNPIN', - 'GET_HASH', - 'GET_TOTAL_SIZE', - 'UPDATE_LIMITS', - 'GET_LIMIT', - 'UPLOAD_STATUS', - 'UPLOAD_COMPLETE', - 'OWNED_UPLOAD_COMPLETE', - 'UPLOAD_CANCEL', - 'EXPIRE_SESSION', - 'TRIM_HISTORY', - 'CLEAR_OWNED_CHANNEL', - 'REMOVE_OWNED_CHANNEL', - 'REMOVE_PINS', - 'TRIM_PINS', - 'WRITE_LOGIN_BLOCK', - 'REMOVE_LOGIN_BLOCK', - 'ADMIN', - 'SET_METADATA' -]; - -var isAuthenticatedCall = function (call) { - return AUTHENTICATED_CALLS.indexOf(call) !== -1; +const UNAUTHENTICATED_CALLS = { + GET_FILE_SIZE: Pinning.getFileSize, // XXX TEST + GET_MULTIPLE_FILE_SIZE: Pinning.getMultipleFileSize, + GET_DELETED_PADS: Pinning.getDeletedPads, + IS_CHANNEL_PINNED: Pinning.isChannelPinned, + IS_NEW_CHANNEL: Channel.isNewChannel, + WRITE_PRIVATE_MESSAGE: Channel.writePrivateMessage, }; var isUnauthenticateMessage = function (msg) { - return msg && msg.length === 2 && isUnauthenticatedCall(msg[0]); + return msg && msg.length === 2 && typeof(UNAUTHENTICATED_CALLS[msg[0]]) === 'function'; }; var handleUnauthenticatedMessage = function (Env, msg, respond, Server) { Env.Log.silly('LOG_RPC', msg[0]); - switch (msg[0]) { - case 'GET_FILE_SIZE': - return void Pinning.getFileSize(Env, msg[1], function (e, size) { - Env.WARN(e, msg[1]); - respond(e, [null, size, null]); - }); - case 'GET_METADATA': - return void Metadata.getMetadata(Env, msg[1], function (e, data) { - Env.WARN(e, msg[1]); - respond(e, [null, data, null]); - }); - case 'GET_MULTIPLE_FILE_SIZE': // XXX not actually used on the client? - return void Pinning.getMultipleFileSize(Env, msg[1], function (e, dict) { - if (e) { - Env.WARN(e, dict); - return respond(e); - } - respond(e, [null, dict, null]); - }); - case 'GET_DELETED_PADS': - return void Pinning.getDeletedPads(Env, msg[1], function (e, list) { - if (e) { - Env.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], Server, function (e, output) { - respond(e, output); - }); - default: - Env.Log.warn("UNSUPPORTED_RPC_CALL", msg); - return respond('UNSUPPORTED_RPC_CALL', msg); - } + + var method = UNAUTHENTICATED_CALLS[msg[0]]; + method(Env, msg[1], function (err, value) { + if (err) { + Env.WARN(err, msg[1]); + return void respond(err); + } + respond(err, [null, value, null]); + }, Server); }; const AUTHENTICATED_USER_TARGETED = { @@ -124,24 +56,47 @@ const AUTHENTICATED_USER_TARGETED = { UPLOAD_COMPLETE: Upload.complete, UPLOAD_CANCEL: Upload.cancel, OWNED_UPLOAD_COMPLETE: Upload.complete_owned, + WRITE_LOGIN_BLOCK: Block.writeLoginBlock, + REMOVE_LOGIN_BLOCK: Block.removeLoginBlock, + ADMIN: Admin.command, }; const AUTHENTICATED_USER_SCOPED = { GET_HASH: Pinning.getHash, GET_TOTAL_SIZE: Pinning.getTotalSize, - UPDATE_LIMITS: Quota.updateLimits, + UPDATE_LIMITS: Quota.getUpdatedLimit, GET_LIMIT: Pinning.getLimit, EXPIRE_SESSION: Core.expireSessionAsync, REMOVE_PINS: Pinning.removePins, TRIM_PINS: Pinning.trimPins, SET_METADATA: Metadata.setMetadata, + COOKIE: Core.haveACookie, +}; + +var isAuthenticatedCall = function (call) { + if (call === 'UPLOAD') { return false; } + return typeof(AUTHENTICATED_USER_TARGETED[call] || AUTHENTICATED_USER_SCOPED[call]) === 'function'; }; -var handleAuthenticatedMessage = function (Env, map) { - var msg = map.msg; - var safeKey = map.safeKey; - var Respond = map.Respond; - var Server = map.Server; +var handleAuthenticatedMessage = function (Env, unsafeKey, msg, respond, Server) { + /* If you have gotten this far, you have signed the message with the + public key which you provided. + */ + + var safeKey = Util.escapeKeyCharacters(unsafeKey); + + var Respond = function (e, value) { + var session = Env.Sessions[safeKey]; + var token = session? session.tokens.slice(-1)[0]: ''; + var cookie = Core.makeCookie(token).join('|'); + respond(e ? String(e): e, [cookie].concat(typeof(value) !== 'undefined' ?value: [])); + }; + + msg.shift(); + // discard validated cookie from message + if (!msg.length) { + return void Respond('INVALID_MSG'); + } var TYPE = msg[0]; @@ -151,7 +106,7 @@ var handleAuthenticatedMessage = function (Env, map) { return void AUTHENTICATED_USER_TARGETED[TYPE](Env, safeKey, msg[1], function (e, value) { Env.WARN(e, value); return void Respond(e, value); - }); + }, Server); } if (typeof(AUTHENTICATED_USER_SCOPED[TYPE]) === 'function') { @@ -164,35 +119,7 @@ var handleAuthenticatedMessage = function (Env, map) { }); } - switch (msg[0]) { - case 'COOKIE': return void Respond(void 0); - case 'WRITE_LOGIN_BLOCK': - return void Block.writeLoginBlock(Env, msg[1], function (e) { // XXX SPECIAL - if (e) { - Env.WARN(e, 'WRITE_LOGIN_BLOCK'); - return void Respond(e); - } - Respond(e); - }); - case 'REMOVE_LOGIN_BLOCK': - return void Block.removeLoginBlock(Env, msg[1], function (e) { // XXX SPECIAL - if (e) { - Env.WARN(e, 'REMOVE_LOGIN_BLOCK'); - return void Respond(e); - } - Respond(e); - }); - case 'ADMIN': - return void Admin.command(Env, Server, safeKey, msg[1], function (e, result) { // XXX SPECIAL - if (e) { - Env.WARN(e, result); - return void Respond(e); - } - Respond(void 0, result); - }); - default: - return void Respond('UNSUPPORTED_RPC_CALL', msg); - } + return void Respond('UNSUPPORTED_RPC_CALL', msg); }; var rpc = function (Env, Server, data, respond) { @@ -241,45 +168,23 @@ var rpc = function (Env, Server, data, respond) { return void respond('INVALID_MESSAGE_OR_PUBLIC_KEY'); } - if (isAuthenticatedCall(msg[1])) { - if (Core.checkSignature(Env, serialized, signature, publicKey) !== true) { - return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); - } - } else if (msg[1] !== 'UPLOAD') { - Env.Log.warn('INVALID_RPC_CALL', msg[1]); - return void respond("INVALID_RPC_CALL"); - } - - 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 = Env.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: [])); - }; + var command = msg[1]; - if (typeof(msg) !== 'object' || !msg.length) { - return void Respond('INVALID_MSG'); + if (command === 'UPLOAD') { + // UPLOAD is a special case that skips signature validation + // intentional fallthrough behaviour + return void handleAuthenticatedMessage(Env, publicKey, msg, respond, Server); } - - handleAuthenticatedMessage(Env, { - msg: msg, - safeKey: safeKey, - Respond: Respond, - Server: Server, - }); + if (isAuthenticatedCall(command)) { + // check the signature on the message + // refuse the command if it doesn't validate + if (Core.checkSignature(Env, serialized, signature, publicKey) === true) { + return void handleAuthenticatedMessage(Env, publicKey, msg, respond, Server); + } + return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY"); + } + Env.Log.warn('INVALID_RPC_CALL', command); + return void respond("INVALID_RPC_CALL"); }; RPC.create = function (config, cb) { @@ -302,10 +207,13 @@ RPC.create = function (config, cb) { } }; + if (typeof(config.domain) !== 'undefined') { + throw new Error('fuck'); + } + var Env = { historyKeeper: config.historyKeeper, intervals: config.intervals || {}, - defaultStorageLimit: config.defaultStorageLimit, maxUploadSize: config.maxUploadSize || (20 * 1024 * 1024), Sessions: {}, paths: {}, @@ -326,6 +234,10 @@ RPC.create = function (config, cb) { domain: config.domain // XXX }; + Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit > 0? + config.defaultStorageLimit: + Core.DEFAULT_LIMIT; + try { Env.admins = (config.adminKeys || []).map(function (k) { k = k.replace(/\/+$/, ''); @@ -345,7 +257,7 @@ RPC.create = function (config, cb) { paths.blob = keyOrDefaultString('blobPath', './blob'); var updateLimitDaily = function () { - Quota.updateLimits(Env, undefined, function (e) { + Quota.updateCachedLimits(Env, function (e) { if (e) { WARN('limitUpdate', e); } diff --git a/scripts/tests/test-rpc.js b/scripts/tests/test-rpc.js index 70ea13053..2e956cb88 100644 --- a/scripts/tests/test-rpc.js +++ b/scripts/tests/test-rpc.js @@ -159,6 +159,13 @@ var createUser = function (config, cb) { } wc.leave(); })); + }).nThen(function (w) { + // give the server time to write your mailbox data before checking that it's correct + // XXX chainpad-server sends an ACK before the channel has actually been created + // causing you to think that everything is good. + // without this timeout the GET_METADATA rpc occasionally returns before + // the metadata has actually been written to the disk. + setTimeout(w(), 500); }).nThen(function (w) { // confirm that you own your mailbox user.anonRpc.send("GET_METADATA", user.mailboxChannel, w(function (err, data) {