cryptpad/www/common/outer/team.js

493 lines
18 KiB
JavaScript
Raw Normal View History

2019-09-04 14:37:52 +00:00
define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
2019-09-04 15:59:44 +00:00
2019-09-10 16:32:12 +00:00
'/common/proxy-manager.js',
'/common/userObject.js',
2019-09-04 15:59:44 +00:00
'/common/outer/sharedfolder.js',
2019-09-04 14:37:52 +00:00
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
2019-09-10 16:32:12 +00:00
'/bower_components/nthen/index.js',
'/bower_components/tweetnacl/nacl-fast.min.js',
2019-09-04 15:59:44 +00:00
], function (Util, Hash, Constants, Realtime,
ProxyManager, UserObject, SF,
2019-09-10 16:32:12 +00:00
Listmap, Crypto, ChainPad, nThen) {
2019-09-04 14:37:52 +00:00
var Team = {};
2019-09-10 16:32:12 +00:00
var Nacl = window.nacl;
2019-09-04 14:37:52 +00:00
var initializeTeams = function (ctx, cb) {
cb();
};
var registerChangeEvents = function (ctx, team, proxy, fId) {
if (!team) { return; }
proxy.on('change', [], function (o, n, p) {
if (fId) {
// Pin the new pads
if (p[0] === UserObject.FILES_DATA && typeof(n) === "object" && n.channel && !n.owners) {
var toPin = [n.channel];
// Also pin the onlyoffice channels if they exist
if (n.rtChannel) { toPin.push(n.rtChannel); }
if (n.lastVersion) { toPin.push(n.lastVersion); }
team.pin(toPin, function (obj) { console.error(obj); });
}
// Unpin the deleted pads (deleted <=> changed to undefined)
if (p[0] === UserObject.FILES_DATA && typeof(o) === "object" && o.channel && !n) {
var toUnpin = [o.channel];
var c = team.manager.findChannel(o.channel);
var exists = c.some(function (data) {
return data.fId !== fId;
});
if (!exists) { // Unpin
// Also unpin the onlyoffice channels if they exist
if (o.rtChannel) { toUnpin.push(o.rtChannel); }
if (o.lastVersion) { toUnpin.push(o.lastVersion); }
team.unpin(toUnpin, function (obj) { console.error(obj); });
}
}
}
team.sendEvent('DRIVE_CHANGE', {
id: fId,
old: o,
new: n,
path: p
});
});
proxy.on('remove', [], function (o, p) {
team.sendEvent('DRIVE_REMOVE', {
id: fId,
old: o,
path: p
});
});
};
var getTeamChannelList = function (ctx, id) {
// Get the list of pads' channel ID in your drive
// This list is filtered so that it doesn't include pad owned by other users
// It now includes channels from shared folders
var store = ctx.teams[id];
if (!store) { return null; }
var list = store.manager.getChannelsList('pin');
// Teams will always be owned for now
// XXX if the team is not owned, add the teamChannel to the list
/*
var _team = Util.find(ctx, ['store', 'proxy', 'teams', id]);
var secret = Hash.getSecrets('team', _team.hash, _team.password);
var teamChannel = secret.channel;
list.push(userChannel);
*/
// XXX Add the team mailbox
/*
if (store.proxy.mailboxes) {
var mList = Object.keys(store.proxy.mailboxes).map(function (m) {
return store.proxy.mailboxes[m].channel;
});
list = list.concat(mList);
}
*/
list.sort();
return list;
};
var handleSharedFolder = function (ctx, id, sfId, rt) {
var t = ctx.teams[id];
if (!t) { return; }
t.sharedFolders[sfId] = rt;
registerChangeEvents(ctx, t, rt.proxy, sfId);
};
2019-09-10 16:32:12 +00:00
var initRpc = function (ctx, team, data, cb) {
if (team.rpc) { return void cb(); }
if (!data.edPrivate || !data.edPublic) { return void cb('EFORBIDDEN'); }
require(['/common/pinpad.js'], function (Pinpad) {
Pinpad.create(ctx.store.network, data, function (e, call) {
if (e) { return void cb(e); }
team.rpc = call;
2019-09-11 14:23:58 +00:00
cb();
2019-09-10 16:32:12 +00:00
});
});
};
var onReady = function (ctx, id, lm, keys, cId, cb) {
2019-09-10 16:32:12 +00:00
var proxy = lm.proxy;
var team = {
id: id,
proxy: proxy,
listmap: lm,
clients: [],
realtime: lm.realtime,
handleSharedFolder: function (sfId, rt) { handleSharedFolder(ctx, id, sfId, rt); },
sharedFolders: {}, // equivalent of store.sharedFolders in async-store
};
if (cId) { team.clients.push(cId); }
2019-09-10 16:32:12 +00:00
team.sendEvent = function (q, data, sender) {
ctx.emit(q, data, team.clients.filter(function (cId) {
return cId !== sender;
}));
};
2019-09-17 09:05:32 +00:00
team.getChatData = function () {
var hash = Util.find(proxy, ['metadata', 'chat']);
if (!hash) {
hash = proxy.metadata.chat = Hash.createRandomHash('chat');
}
var secret = Hash.getSecrets('chat', hash);
return {
channel: secret.channel,
secret: secret,
validateKey: secret.keys.validateKey
};
};
team.pin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
team.unpin = function (data, cb) { return void cb({error: 'EFORBIDDEN'}); };
2019-09-10 16:32:12 +00:00
nThen(function (waitFor) {
if (!keys.edPrivate) { return; }
initRpc(ctx, team, keys, waitFor(function (err) {
if (err) { return; }
team.pin = function (data, cb) {
2019-09-10 16:32:12 +00:00
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.pin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
team.unpin = function (data, cb) {
2019-09-10 16:32:12 +00:00
if (!team.rpc) { return void cb({error: 'TEAM_RPC_NOT_READY'}); }
if (typeof(cb) !== 'function') { console.error('expected a callback'); }
team.rpc.unpin(data, function (e, hash) {
if (e) { return void cb({error: e}); }
cb({hash: hash});
});
};
}));
2019-09-10 16:37:11 +00:00
}).nThen(function () {
2019-09-10 16:32:12 +00:00
var loadSharedFolder = function (id, data, cb) {
SF.load({
network: ctx.store.network,
store: team
}, id, data, cb);
};
var manager = team.manager = ProxyManager.create(proxy.drive, {
onSync: function (cb) { ctx.Store.onSync(id, cb); },
edPublic: proxy.edPublic,
pin: team.pin,
unpin: team.unpin,
2019-09-10 16:32:12 +00:00
loadSharedFolder: loadSharedFolder,
settings: {
drive: Util.find(ctx.store, ['proxy', 'settings', 'drive'])
}
}, {
outer: true,
removeOwnedChannel: function (channel, cb) {
var data;
if (typeof(channel) === "object") {
channel.teamId = id;
data = channel;
} else {
data = {
channel: channel,
teamId: id
};
}
ctx.Store.removeOwnedChannel('', data, cb);
},
edPublic: proxy.edPublic,
loggedIn: true,
log: function (msg) {
// broadcast to all drive apps
team.sendEvent("DRIVE_LOG", msg);
}
});
team.userObject = manager.user.userObject;
team.userObject.fixFiles();
}).nThen(function (waitFor) {
ctx.teams[id] = team;
registerChangeEvents(ctx, team, proxy);
SF.loadSharedFolders(ctx.Store, ctx.store.network, team, team.userObject, waitFor);
2019-09-10 16:32:12 +00:00
// XXX
// Load members pad
}).nThen(function () {
if (!team.rpc) { return; }
var list = getTeamChannelList(ctx, id);
var local = Hash.hashChannelList(list);
// Check pin list
team.rpc.getServerHash(function (e, hash) {
if (e) { return void console.warn(e); }
if (hash !== local) {
// Reset pin list
team.rpc.reset(list, function (e/*, hash*/) {
if (e) { console.warn(e); }
});
}
});
}).nThen(function () {
2019-09-10 16:32:12 +00:00
if (ctx.onReadyHandlers[id]) {
ctx.onReadyHandlers[id].forEach(function (obj) {
// Callback and subscribe the client to new notifications
if (typeof (obj.cb) === "function") { obj.cb(); }
if (!obj.cId) { return; }
var idx = team.clients.indexOf(obj.cId);
if (idx === -1) {
team.clients.push(obj.cId);
}
});
}
delete ctx.onReadyHandlers[id];
cb();
});
2019-09-04 15:59:44 +00:00
};
var openChannel = function (ctx, teamData, id, cb) {
2019-09-10 16:32:12 +00:00
var secret = Hash.getSecrets('team', teamData.hash, teamData.password);
2019-09-04 14:37:52 +00:00
var crypto = Crypto.createEncryptor(secret.keys);
var cfg = {
data: {},
2019-09-11 14:23:58 +00:00
readOnly: !Boolean(secret.keys.signKey),
2019-09-04 14:37:52 +00:00
network: ctx.store.network,
channel: secret.channel,
crypto: crypto,
ChainPad: ChainPad,
metadata: {
validateKey: secret.keys.validateKey || undefined,
},
userName: 'team',
classic: true
};
var lm = Listmap.create(cfg);
lm.proxy.on('create', function () {
}).on('ready', function () {
onReady(ctx, id, lm, teamData.keys, null, cb);
2019-09-10 16:32:12 +00:00
});
};
2019-09-05 15:55:23 +00:00
2019-09-10 16:32:12 +00:00
var createTeam = function (ctx, data, cId, _cb) {
var cb = Util.once(_cb);
2019-09-10 16:32:12 +00:00
var password = Hash.createChannelId();
var hash = Hash.createRandomHash('team', password);
var secret = Hash.getSecrets('team', hash, password);
var keyPair = Nacl.sign.keyPair(); // keyPair.secretKey , keyPair.publicKey
var membersSecret = Hash.getSecrets('members');
2019-09-11 14:23:58 +00:00
var membersHashes = Hash.getHashes(membersSecret);
2019-09-10 16:32:12 +00:00
2019-09-17 09:05:32 +00:00
var chatSecret = Hash.getSecrets('chat');
var chatHashes = Hash.getHashes(chatSecret);
2019-09-10 16:32:12 +00:00
var config = {
network: ctx.store.network,
channel: secret.channel,
data: {},
validateKey: secret.keys.validateKey, // derived validation key
crypto: Crypto.createEncryptor(secret.keys),
logLevel: 1,
classic: true,
ChainPad: ChainPad,
owners: [ctx.store.proxy.edPublic]
};
nThen(function (waitFor) {
console.log('pin..');
ctx.pinPads([secret.channel, membersSecret.channel], waitFor(function (obj) {
if (obj && obj.error) {
waitFor.abort();
return void cb(obj);
}
}));
// XXX initialize the members channel with yourself, and mark it as owned!
}).nThen(function () {
console.log('init proxy');
var lm = Listmap.create(config);
var proxy = lm.proxy;
proxy.on('ready', function () {
console.log('ready');
// Store keys in our drive
var id = Util.createRandomInteger();
var keys = {
2019-09-11 14:23:58 +00:00
edPrivate: Nacl.util.encodeBase64(keyPair.secretKey),
edPublic: Nacl.util.encodeBase64(keyPair.publicKey)
2019-09-10 16:32:12 +00:00
};
ctx.store.proxy.teams[id] = {
hash: hash,
password: password,
keys: keys,
members: membersHashes.editHash,
name: data.name
};
// Initialize the team drive
proxy.drive = {};
// Create metadata
proxy.metadata = {
2019-09-17 09:05:32 +00:00
chat: chatHashes.editHash,
2019-09-10 16:32:12 +00:00
name: name,
members: membersHashes.viewHash,
};
// Add rpc key
2019-09-11 14:23:58 +00:00
proxy.edPublic = Nacl.util.encodeBase64(keyPair.publicKey);
2019-09-10 16:32:12 +00:00
onReady(ctx, id, lm, {
edPrivate: keyPair.secretKey,
edPublic: keyPair.publicKey
}, cId, function () {
2019-09-10 16:32:12 +00:00
cb();
2019-09-04 14:37:52 +00:00
});
2019-09-10 16:32:12 +00:00
}).on('error', function (info) {
if (info && typeof (info.loaded) !== "undefined" && !info.loaded) {
cb({error:'ECONNECT'});
}
});
2019-09-04 14:37:52 +00:00
});
};
var subscribe = function (ctx, id, cId, cb) {
// Unsubscribe from other teams: one tab can only receive events about one team
Object.keys(ctx.teams).forEach(function (teamId) {
var c = ctx.teams[teamId].clients;
var idx = c.indexOf(cId);
if (idx !== -1) {
c.splice(idx, 1);
}
});
// Also remove from pending subscriptions
Object.keys(ctx.onReadyHandlers).forEach(function (teamId) {
var idx = -1;
ctx.onReadyHandlers[teamId].some(function (obj, _idx) {
if (obj.cId === cId) {
idx = _idx;
return true;
}
});
if (idx !== -1) {
ctx.onReadyHandlers[teamId].splice(idx, 1);
}
});
2019-09-17 09:09:40 +00:00
if (!id) { return void cb(); }
2019-09-10 16:32:12 +00:00
// If the team is loading, as ourselves in the list
if (ctx.onReadyHandlers[id]) {
var _idx = ctx.onReadyHandlers[id].indexOf(cId);
if (_idx === -1) {
ctx.onReadyHandlers[id].push({
cId: cId,
cb: cb
});
}
return;
}
// Otherwise, subscribe to new notifications
if (!ctx.teams[id]) {
2019-09-04 14:37:52 +00:00
return void cb({error: 'EINVAL'});
}
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
if (idx === -1) {
clients.push(cId);
}
cb();
};
// Remove a client from all the team they're subscribed to
var removeClient = function (ctx, cId) {
Object.keys(ctx.teams).forEach(function (id) {
2019-09-10 16:32:12 +00:00
// Remove from the subscribers
2019-09-04 14:37:52 +00:00
var clients = ctx.teams[id].clients;
var idx = clients.indexOf(cId);
2019-09-10 16:32:12 +00:00
if (idx !== -1) { clients.splice(idx, 1); }
// And remove from the onReady handlers in case they haven't finished loading
if (ctx.onReadyHandlers[id]) {
var idx2 = ctx.onReadyHandlers.indexOf(cId);
if (idx2 !== -1) { ctx.onReadyHandlers.splice(idx2, 1); }
}
2019-09-04 14:37:52 +00:00
});
};
Team.init = function (cfg, waitFor, emit) {
var team = {};
var store = cfg.store;
if (!store.loggedIn || !store.proxy.edPublic) { return; }
var ctx = {
store: store,
2019-09-10 16:32:12 +00:00
Store: cfg.Store,
2019-09-04 14:37:52 +00:00
pinPads: cfg.pinPads,
emit: emit,
2019-09-10 16:32:12 +00:00
onReadyHandlers: {},
2019-09-04 14:37:52 +00:00
teams: {}
};
var teams = store.proxy.teams = store.proxy.teams || {};
initializeTeams(ctx, waitFor(function (err) {
if (err) { return; }
}));
Object.keys(teams).forEach(function (id) {
2019-09-10 16:32:12 +00:00
ctx.onReadyHandlers[id] = [];
2019-09-06 16:47:18 +00:00
openChannel(ctx, teams[id], id, waitFor(function () {
2019-09-17 09:05:32 +00:00
console.debug('Team '+id+' ready');
2019-09-06 16:47:18 +00:00
}));
2019-09-04 14:37:52 +00:00
});
2019-09-04 15:59:44 +00:00
team.getTeam = function (id) {
return ctx.teams[id];
};
2019-09-12 15:54:50 +00:00
team.getTeamsData = function () {
var t = {};
Object.keys(teams).forEach(function (id) {
t[id] = {
name: teams[id].name,
edPublic: Util.find(teams[id], ['keys', 'edPublic'])
};
});
return t;
};
team.getTeams = function () {
return Object.keys(ctx.teams);
};
2019-09-04 14:37:52 +00:00
team.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
team.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
// Only the team app will subscribe to events?
return void subscribe(ctx, data, clientId, cb);
}
2019-09-06 16:47:18 +00:00
if (cmd === 'LIST_TEAMS') {
2019-09-10 16:32:12 +00:00
return void cb(store.proxy.teams);
}
2019-09-17 09:05:32 +00:00
if (cmd === 'OPEN_TEAM_CHAT') {
return void ctx.store.messenger.openTeamChat(data, clientId, cb);
}
2019-09-10 16:32:12 +00:00
if (cmd === 'CREATE_TEAM') {
return void createTeam(ctx, data, clientId, cb);
2019-09-06 16:47:18 +00:00
}
2019-09-04 14:37:52 +00:00
};
return team;
};
return Team;
});