diff --git a/historyKeeper.js b/historyKeeper.js index 10137dd06..170de6233 100644 --- a/historyKeeper.js +++ b/historyKeeper.js @@ -400,6 +400,11 @@ module.exports.create = function (cfg) { * writes messages to the store */ const onChannelMessage = function (ctx, channel, msgStruct) { + // TODO our usage of 'channel' here looks prone to errors + // we only use it for its 'id', but it can contain other stuff + // also, we're using this RPC from both the RPC and Netflux-server + // we should probably just change this to expect a channel id directly + // don't store messages if the channel id indicates that it's an ephemeral message if (!channel.id || channel.id.length === EPHEMERAL_CHANNEL_LENGTH) { return; } @@ -986,7 +991,6 @@ module.exports.create = function (cfg) { onChannelCleared(ctx, msg[4]); } - // FIXME METADATA CHANGE if (msg[3] === 'SET_METADATA') { // or whatever we call the RPC???? // make sure we update our cache of metadata // or at least invalidate it and force other mechanisms to recompute its state @@ -994,6 +998,12 @@ module.exports.create = function (cfg) { onChannelMetadataChanged(ctx, msg[4].channel, output[1]); } + // unauthenticated RPC calls have a different message format + if (msg[0] === "WRITE_PRIVATE_MESSAGE" && output) { + historyKeeperBroadcast(ctx, output.channel, output.message); + } + + // finally, send a response to the client that sent the RPC sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify([parsed[0]].concat(output))]); }); } catch (e) { diff --git a/lib/client/index.js b/lib/client/index.js new file mode 100644 index 000000000..b2cfec437 --- /dev/null +++ b/lib/client/index.js @@ -0,0 +1,119 @@ +var Netflux = require("netflux-websocket"); +var WebSocket = require("ws"); // jshint ignore:line +var nThen = require("nthen"); + +var Util = require("../../www/common/common-util"); +var Rpc = require("../../www/common/rpc"); + +var Nacl = require("tweetnacl"); + +var makeKeys = function () { + var keys = Nacl.sign.keyPair.fromSeed(Nacl.randomBytes(Nacl.sign.seedLength)); + return { + secret: Nacl.util.encodeBase64(keys.secretKey), + public: Nacl.util.encodeBase64(keys.publicKey), + }; +}; + + +var Client = module.exports; + +var createNetwork = Client.createNetwork = function (url, cb) { + var CB = Util.once(cb); + + var info = {}; + + Netflux.connect(url, function (url) { + info.websocket = new WebSocket(url) + .on('error', function (err) { + console.log(err); + }) + .on('close', function (err) { + console.log("close"); + console.log(err); + }); + return info.websocket; + }).then(function (network) { + info.network = network; + CB(void 0, info); + }, function (err) { + CB(err); + }); +}; + +var die = function (client) { + var disconnect = Util.find(client, ['config', 'network', 'disconnect']); + if (typeof(disconnect) === 'function') { + disconnect(); + } else { + console.error("disconnect was not a function"); + } + var close = Util.find(client, ['config', 'websocket', 'close']); + if (typeof(close) === 'function') { + client.config.websocket.close(); + } else { + console.error("close was not a function"); + } +}; + +Client.create = function (config, cb) { + if (typeof(config) === 'function') { + cb = config; + config = {}; + } + var client = { + config: config, + }; + var CB = Util.once(function (err, arg) { + if (err) { die(client); } + cb(err, arg); + }); + + client.shutdown = function () { + die(client); + }; + + nThen(function (w) { + if (config.network) { return; } + // connect to the network... + createNetwork('ws://localhost:3000/cryptpad_websocket', w(function (err, info) { + //console.log(_network); + config.network = info.network; + config.websocket = info.websocket; + })); + }).nThen(function (w) { + // make sure the network has a historyKeeper id on it + // we're responsible for adding it + if (config.network.historyKeeper) { return; } + var channel = Util.uint8ArrayToHex(Nacl.randomBytes(16)); + config.network.join(channel).then(w(function (wc) { + wc.members.some(function (member) { + if (member.length !== 16) { return; } + config.network.historyKeeper = member; + return true; + }); + wc.leave(); + }), function (err) { + w.abort(); + CB(err); + }); + }).nThen(function (w) { + // connect to the anonRpc + Rpc.createAnonymous(config.network, w(function (err, rpc) { + if (err) { + return void CB('ANON_RPC_CONNECT_ERR'); + } + client.anonRpc = rpc; + })); + var keys = makeKeys(); + Rpc.create(config.network, keys.secret, keys.public, w(function (err, rpc) { + if (err) { + return void CB('RPC_CONNECT_ERR'); + } + client.rpc = rpc; + })); + }).nThen(function () { + CB(void 0, client); + }); +}; + diff --git a/package-lock.json b/package-lock.json index 0fb182b2d..4c32aff68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cryptpad", - "version": "3.0.0", + "version": "3.0.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -800,6 +800,11 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, + "netflux-websocket": { + "version": "0.1.20", + "resolved": "https://registry.npmjs.org/netflux-websocket/-/netflux-websocket-0.1.20.tgz", + "integrity": "sha512-svFkw4ol4gmkcXKnx5kF/8tR9mmtTCDzUlLy4mSlcNl/4iWlbDmgwp/+aJ3nqtv8fg12m+DAFGX2+fbC0//dcg==" + }, "nthen": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/nthen/-/nthen-0.1.8.tgz", diff --git a/package.json b/package.json index 8034e1b33..8bd93b82d 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ "chainpad-server": "~3.0.2", "express": "~4.16.0", "fs-extra": "^7.0.0", + "get-folder-size": "^2.0.1", + "netflux-websocket": "^0.1.20", "nthen": "0.1.8", "pull-stream": "^3.6.1", "replify": "^1.2.0", @@ -18,8 +20,7 @@ "sortify": "^1.0.4", "stream-to-pull-stream": "^1.7.2", "tweetnacl": "~0.12.2", - "ws": "^3.3.1", - "get-folder-size": "^2.0.1" + "ws": "^3.3.1" }, "devDependencies": { "flow-bin": "^0.59.0", @@ -38,6 +39,7 @@ "lint:less": "./node_modules/lesshint/bin/lesshint -c ./.lesshintrc ./customize.dist/src/less2/", "flow": "./node_modules/.bin/flow", "test": "node scripts/TestSelenium.js", + "test-rpc": "cd scripts && node test-rpc", "template": "cd customize.dist/src && for page in ../index.html ../privacy.html ../terms.html ../about.html ../contact.html ../what-is-cryptpad.html ../features.html ../../www/login/index.html ../../www/register/index.html ../../www/user/index.html;do echo $page; cp template.html $page; done;" } } diff --git a/rpc.js b/rpc.js index a204df7ab..fd0c11f35 100644 --- a/rpc.js +++ b/rpc.js @@ -1555,6 +1555,57 @@ var isNewChannel = function (Env, channel, cb) { }); }; +/* writePrivateMessage + allows users to anonymously send a message to the channel + prevents their netflux-id from being stored in history + and from being broadcast to anyone that might currently be in the channel + + Otherwise behaves the same as sending to a channel +*/ +var writePrivateMessage = function (Env, args, nfwssCtx, cb) { + var channelId = args[0]; + var msg = args[1]; + + // don't bother handling empty messages + if (!msg) { return void cb("INVALID_MESSAGE"); } + + // don't support anything except regular channels + if (!isValidId(channelId) || channelId.length !== 32) { + return void cb("INVALID_CHAN"); + } + + // We expect a modern netflux-websocket-server instance + // if this API isn't here everything will fall apart anyway + if (!(nfwssCtx && nfwssCtx.historyKeeper && typeof(nfwssCtx.historyKeeper.onChannelMessage) === 'function')) { + return void cb("NOT_IMPLEMENTED"); + } + + // historyKeeper expects something with an 'id' attribute + // it will fail unless you provide it, but it doesn't need anything else + var channelStruct = { + id: channelId, + }; + + // construct a message to store and broadcast + var fullMessage = [ + 0, // idk + null, // normally the netflux id, null isn't rejected, and it distinguishes messages written in this way + "MSG", // indicate that this is a MSG + channelId, // channel id + msg // the actual message content. Generally a string + ]; + + // store the message and do everything else that is typically done when going through historyKeeper + nfwssCtx.historyKeeper.onChannelMessage(nfwssCtx, channelStruct, fullMessage); + + // call back with the message and the target channel. + // historyKeeper will take care of broadcasting it if anyone is in the channel + cb(void 0, { + channel: channelId, + message: fullMessage + }); +}; + var getDiskUsage = function (Env, cb) { var data = {}; nThen(function (waitFor) { @@ -1654,6 +1705,7 @@ var isUnauthenticatedCall = function (call) { 'IS_NEW_CHANNEL', 'GET_HISTORY_OFFSET', 'GET_DELETED_PADS', + 'WRITE_PRIVATE_MESSAGE', ].indexOf(call) !== -1; }; @@ -1821,6 +1873,10 @@ RPC.create = function ( return void isNewChannel(Env, msg[1], function (e, isNew) { respond(e, [null, isNew, null]); }); + case 'WRITE_PRIVATE_MESSAGE': + return void writePrivateMessage(Env, msg[1], nfwssCtx, function (e, output) { + respond(e, output); + }); default: Log.warn("UNSUPPORTED_RPC_CALL", msg); return respond('UNSUPPORTED_RPC_CALL', msg); diff --git a/scripts/test-rpc.js b/scripts/test-rpc.js new file mode 100644 index 000000000..79177464d --- /dev/null +++ b/scripts/test-rpc.js @@ -0,0 +1,43 @@ +/* globals process */ +var Client = require("../lib/client/"); +var Mailbox = require("../www/bower_components/chainpad-crypto").Mailbox; +var Nacl = require("tweetnacl"); + +var makeKeys = function () { + var pair = Nacl.box.keyPair(); + return { + curvePrivate: Nacl.util.encodeBase64(pair.secretKey), + curvePublic: Nacl.util.encodeBase64(pair.publicKey), + }; +}; + +Client.create(function (err, client) { + if (err) { + console.error(err); + process.exit(1); + } + + var channel = "d34ebe83931382fcad9fe2e2d0e2cb5f"; // channel + var recipient = "e8jvf36S3chzkkcaMrLSW7PPrz7VDp85lIFNI26dTmw="; // curvePublic + + var keys = makeKeys(); + var cryptor = Mailbox.createEncryptor(keys); + + var message = cryptor.encrypt(JSON.stringify({ + type: "CHEESE", + author: keys.curvePublic, + content: { + text: "CAMEMBERT", + } + }), recipient); + + client.anonRpc.send('WRITE_PRIVATE_MESSAGE', [channel, message], function (err, response) { + if (err) { + return void console.error(err); + } + + response = response; + // shutdown doesn't work, so we need to do this instead + client.shutdown(); + }); +}); diff --git a/www/common/common-util.js b/www/common/common-util.js index 262de76e9..22f20f710 100644 --- a/www/common/common-util.js +++ b/www/common/common-util.js @@ -1,7 +1,5 @@ (function (window) { -define([], function () { - var Util = window.CryptPad_Util = {}; - + var Util = {}; Util.mkAsync = function (f) { return function () { var args = Array.prototype.slice.call(arguments); @@ -349,6 +347,14 @@ define([], function () { return false; }; - return Util; -}); -}(self)); + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = Util; + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([], function () { + window.CryptPad_Util = Util; + return Util; + }); + } else { + window.CryptPad_Util = Util; + } +}(typeof(self) !== 'undefined'? self: this)); diff --git a/www/common/rpc.js b/www/common/rpc.js index c0452b80a..b82345a24 100644 --- a/www/common/rpc.js +++ b/www/common/rpc.js @@ -1,9 +1,5 @@ -define([ - '/common/common-util.js', - '/bower_components/tweetnacl/nacl-fast.min.js', -], function (Util) { - var Nacl = window.nacl; - +(function () { +var factory = function (Util, Nacl) { var uid = Util.uid; var signMsg = function (data, signKey) { var buffer = Nacl.util.decodeUTF8(JSON.stringify(data)); @@ -337,4 +333,18 @@ types of messages: }; return { create: create, createAnonymous: createAnonymous }; -}); +}; + + if (typeof(module) !== 'undefined' && module.exports) { + module.exports = factory(require("./common-util"), require("tweetnacl")); + } else if ((typeof(define) !== 'undefined' && define !== null) && (define.amd !== null)) { + define([ + '/common/common-util.js', + '/bower_components/tweetnacl/nacl-fast.min.js', + ], function (Util) { + return factory(Util, window.Nacl); + }); + } else { + // I'm not gonna bother supporting any other kind of instanciation + } +}());