Merge branch 'staging' into communities-trim

pull/1/head
yflory 5 years ago
commit 9a857ea058

@ -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

@ -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");
}

@ -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);

@ -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];

@ -184,5 +184,7 @@ Core.isPendingOwner = function (metadata, unsafeKey) {
return metadata.pending_owners.indexOf(unsafeKey) !== -1;
};
Core.haveACookie = function (Env, safeKey, cb) {
cb();
};

@ -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);

@ -199,7 +199,8 @@ Pinning.removePins = function (Env, safeKey, cb) {
status: err? String(err): 'SUCCESS',
});
cb(err);
if (err) { return void cb(err); }
cb(void 0, 'OK');
});
};
@ -453,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);
}
});
};

@ -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, '', '']);
});
};

@ -595,7 +595,9 @@ module.exports.create = function (cfg, cb) {
const start = (beforeHash) ? 0 : offset;
store.readMessagesBin(channelName, start, (msgObj, readMore, abort) => {
if (beforeHash && msgObj.offset >= offset) { return void abort(); }
handler(tryParse(msgObj.buff.toString('utf8')), readMore);
var parsed = tryParse(msgObj.buff.toString('utf8'));
if (!parsed) { return void readMore(); }
handler(parsed, readMore);
}, waitFor(function (err) {
return void cb(err);
}));
@ -749,7 +751,6 @@ module.exports.create = function (cfg, cb) {
// TODO compute lastKnownHash in a manner such that it will always skip past the metadata line?
getHistoryAsync(channelName, lastKnownHash, false, (msg, readMore) => {
if (!msg) { return; } // XXX
msgCount++;
// avoid sending the metadata message a second time
if (isMetadataMessage(msg) && metadata_cache[channelName]) { return readMore(); }
@ -869,7 +870,6 @@ module.exports.create = function (cfg, cb) {
// FIXME should we send metadata here too?
// none of the clientside code which uses this API needs metadata, but it won't hurt to send it (2019-08-22)
return void getHistoryAsync(parsed[1], -1, false, (msg, readMore) => {
if (!msg) { return; }
Server.send(userId, [0, HISTORY_KEEPER_ID, 'MSG', userId, JSON.stringify(['FULL_HISTORY', msg])], readMore);
}, (err) => {
let parsedMsg = ['FULL_HISTORY_END', parsed[1]];

@ -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 handleAuthenticatedMessage = function (Env, map) {
var msg = map.msg;
var safeKey = map.safeKey;
var Respond = map.Respond;
var Server = map.Server;
var isAuthenticatedCall = function (call) {
if (call === 'UPLOAD') { return false; }
return typeof(AUTHENTICATED_USER_TARGETED[call] || AUTHENTICATED_USER_SCOPED[call]) === 'function';
};
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);
}
};
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");
var command = msg[1];
if (command === 'UPLOAD') {
// UPLOAD is a special case that skips signature validation
// intentional fallthrough behaviour
return void handleAuthenticatedMessage(Env, publicKey, msg, respond, Server);
}
} else if (msg[1] !== 'UPLOAD') {
Env.Log.warn('INVALID_RPC_CALL', msg[1]);
return void respond("INVALID_RPC_CALL");
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);
}
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: []));
};
if (typeof(msg) !== 'object' || !msg.length) {
return void Respond('INVALID_MSG');
return void respond("INVALID_SIGNATURE_OR_PUBLIC_KEY");
}
handleAuthenticatedMessage(Env, {
msg: msg,
safeKey: safeKey,
Respond: Respond,
Server: Server,
});
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);
}

@ -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) {

@ -3386,10 +3386,7 @@ define([
if (sfId) {
var sfData = manager.getSharedFolderData(sfId);
var parsed = Hash.parsePadUrl(sfData.href);
sframeChan.event('EV_DRIVE_SET_HASH', parsed.hash || '');
createShareButton(sfId, $toolbar.find('.cp-app-drive-toolbar-leftside'));
} else {
sframeChan.event('EV_DRIVE_SET_HASH', '');
}

@ -97,18 +97,6 @@ define([
cb(obj);
});
});
sframeChan.on('EV_DRIVE_SET_HASH', function (/*hash*/) {
// Update the hash in the address bar
// XXX Hidden hash: don't put the shared folder href in the address bar
/*
if (!Utils.LocalStore.isLoggedIn()) { return; }
var ohc = window.onhashchange;
window.onhashchange = function () {};
window.location.hash = hash || '';
window.onhashchange = ohc;
ohc({reset:true});
*/
});
Cryptpad.onNetworkDisconnect.reg(function () {
sframeChan.event('EV_NETWORK_DISCONNECT');
});

@ -72,9 +72,6 @@ define([
cb(obj);
});
});
sframeChan.on('EV_DRIVE_SET_HASH', function () {
return;
});
Cryptpad.onNetworkDisconnect.reg(function () {
sframeChan.event('EV_NETWORK_DISCONNECT');
});

Loading…
Cancel
Save