diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index b15cdd33b..0108659fb 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -209,6 +209,21 @@ const handleMessage = function (ctx, user, msg) { let parsedMsg = {state: 1, channel: parsed[1]}; sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(parsedMsg)]); }); + } else if (ctx.rpc) { + /* RPC Calls... */ + var rpc_call = parsed.slice(1); + + // slice off the sequence number and pass in the rest of the message + ctx.rpc(rpc_call, function (err, output) { + if (err) { + console.error('[' + err + ']', output); // TODO make this disableable + sendMsg(ctx, user, [seq, 'ACK']); + sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify([parsed[0], 'ERROR', err])]); + return + } + sendMsg(ctx, user, [seq, 'ACK']); + sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify([parsed[0]].concat(output))]); + }); } return; } @@ -251,7 +266,7 @@ const handleMessage = function (ctx, user, msg) { } }; -let run = module.exports.run = function (storage, socketServer, config) { +let run = module.exports.run = function (storage, socketServer, config, rpc) { /* Channel removal timeout defaults to 60000ms (one minute) */ config.channelRemovalTimeout = typeof(config.channelRemovalTimeout) === 'number'? @@ -263,7 +278,8 @@ let run = module.exports.run = function (storage, socketServer, config) { channels: {}, timeouts: {}, store: storage, - config: config + config: config, + rpc: rpc, }; setInterval(function () { Object.keys(ctx.users).forEach(function (userId) { diff --git a/config.js.dist b/config.js.dist index da80bb374..3818ae002 100644 --- a/config.js.dist +++ b/config.js.dist @@ -130,6 +130,13 @@ module.exports = { */ openFileLimit: 2048, + /* Cryptpad's socket server can be extended to respond to RPC calls + * you can configure it to respond to custom RPC calls if you like. + * provide the path to your RPC module here, or `false` if you would + * like to disable the RPC interface completely + */ + rpc: './rpc.js', + /* it is recommended that you serve cryptpad over https * the filepaths below are used to configure your certificates */ diff --git a/rpc.js b/rpc.js new file mode 100644 index 000000000..66973039b --- /dev/null +++ b/rpc.js @@ -0,0 +1,37 @@ +/* Use Nacl for checking signatures of messages + +*/ +var Nacl = require("tweetnacl"); + +var RPC = module.exports; + +var pin = function (ctx, cb) { }; +var unpin = function (ctx, cb) { }; +var getHash = function (ctx, cb) { }; +var getTotalSize = function (ctx, cb) { }; +var getFileSize = function (ctx, cb) { }; + +RPC.create = function (config, cb) { + // load pin-store... + + console.log('loading rpc module...'); + rpc = function (msg, respond) { + switch (msg[0]) { + case 'ECHO': + respond(void 0, msg); + break; + case 'PIN': + case 'UNPIN': + case 'GET_HASH': + case 'GET_TOTAL_SIZE': + case 'GET_FILE_SIZE': + + default: + respond('UNSUPPORTED_RPC_CALL', msg); + break; + } + }; + + cb(void 0, rpc); +}; + diff --git a/server.js b/server.js index 34f8cfedf..ea26778f9 100644 --- a/server.js +++ b/server.js @@ -117,13 +117,32 @@ httpServer.listen(config.httpPort,config.httpAddress,function(){ var wsConfig = { server: httpServer }; -if(!config.useExternalWebsocket) { - if (websocketPort !== config.httpPort) { - console.log("setting up a new websocket server"); - wsConfig = { port: websocketPort}; +var createSocketServer = function (err, rpc) { + if(!config.useExternalWebsocket) { + if (websocketPort !== config.httpPort) { + console.log("setting up a new websocket server"); + wsConfig = { port: websocketPort}; + } + var wsSrv = new WebSocketServer(wsConfig); + Storage.create(config, function (store) { + NetfluxSrv.run(store, wsSrv, config, rpc); + }); } - var wsSrv = new WebSocketServer(wsConfig); - Storage.create(config, function (store) { - NetfluxSrv.run(store, wsSrv, config); - }); -} +}; + +var loadRPC = function (cb) { + config.rpc = typeof(config.rpc) === 'undefined'? './rpc.js' : config.rpc; + + if (typeof(config.rpc) === 'string') { + // load pin store... + var Rpc = require(config.rpc); + Rpc.create(config, function (e, rpc) { + if (e) { throw e; } + cb(void 0, rpc); + }); + } else { + cb(); + } +}; + +loadRPC(createSocketServer); diff --git a/www/common/encode.js b/www/common/encode.js new file mode 100644 index 000000000..44b2071ba --- /dev/null +++ b/www/common/encode.js @@ -0,0 +1,19 @@ +define([], function () { + var exports = {}; + + var hexToUint8Array = exports.hexToUint8Array = function (s) { + // if not hex or odd number of characters + if (!/[a-fA-F0-9]+/.test(s) || s.length % 2) { throw new Error("string is not hex"); } + return s.split(/([0-9a-fA-F]{2})/) + .filter(function (x) { return x; }) + .map(function (x) { return Number('0x' + x); }); + }; + + var uint8ArrayToHex = exports.uint8ArrayToHex = function (a) { + return a.reduce(function(memo, i) { + return memo + ((i < 16) ? '0' : '') + i.toString(16); + }, ''); + }; + + return exports; +}); diff --git a/www/common/pinpad.js b/www/common/pinpad.js new file mode 100644 index 000000000..b3285b8f8 --- /dev/null +++ b/www/common/pinpad.js @@ -0,0 +1,57 @@ +define([ + '/common/cryptpad-common.js', + '/common/rpc.js', + + '/bower_components/tweetnacl/nacl-fast.min.js' +], function (Cryptpad, Rpc) { + + var Nacl = window.nacl; + + var deduplicate = function (array) { + var a = array.slice(); + for(var i=0; i hash + unpin -> hash + getHash -> hash + getTotalSize -> bytes + getFileSize -> bytes + + */ + +/* RPC communicates only with the history keeper + messages have the format: + [TYPE, txid, msg] +*/ + var sendMsg = function (ctx, type, msg, cb) { + var network = ctx.network; + var hkn = network.historyKeeper; + var txid = uid(); + + ctx.pending[txid] = cb; + + return network.sendto(hkn, JSON.stringify([txid, type, msg])); + }; + + var parse = function (msg) { + try { + return JSON.parse(msg); + } catch (e) { + return null; + } + }; + +/* Returning messages have the format: + [txid, {}] +*/ + var onMsg = function (ctx, msg) { + var parsed = parse(msg); + + if (!parsed) { + // TODO handle error + console.log(msg); + return; + } + + var txid = parsed[0]; + var pending = ctx.pending[txid]; + var response = parsed.slice(1); + + if (typeof(pending) === 'function') { + if (response[0] === 'ERROR') { + return void pending(response[1]); + } + pending(void 0, response); + } else { + console.log("No callback provided"); + } + }; + + var cookie = function (ctx, cb) { + // TODO txid + }; + + var signMsg = function (msg, secKey) { + // TODO + }; + + var create = function (network, edPrivateKey) { + if (!/[0-9a-f]{64}/.test(edPrivateKey)) { + //throw new Error("private signing key is not valid"); + } + var ctx = { + //privateKey: Encode.hexToUint8Array(edPrivateKey), + seq: new Date().getTime(), + network: network, + timeouts: {}, // timeouts + pending: {}, // callbacks + }; + + var pin = function (channel, cb) { }; + + var send = function (type, msg, cb) { + return sendMsg(ctx, type, msg, cb); + }; + network.on('message', function (msg, sender) { + onMsg(ctx, msg); + }); + return { + cookie: function (cb) { cookie(ctx, cb); }, + send: send, + }; + }; + + return { create: create }; +}); diff --git a/www/examples/rpc/index.html b/www/examples/rpc/index.html new file mode 100644 index 000000000..e17a68143 --- /dev/null +++ b/www/examples/rpc/index.html @@ -0,0 +1,13 @@ + + + + CryptPad + + + + +
+ +
+ + diff --git a/www/examples/rpc/inner.html b/www/examples/rpc/inner.html new file mode 100644 index 000000000..9680685b7 --- /dev/null +++ b/www/examples/rpc/inner.html @@ -0,0 +1,8 @@ + + + + + + + +

PEWPEW diff --git a/www/examples/rpc/main.js b/www/examples/rpc/main.js new file mode 100644 index 000000000..528290d66 --- /dev/null +++ b/www/examples/rpc/main.js @@ -0,0 +1,35 @@ +require.config({ paths: { 'json.sortify': '/bower_components/json.sortify/dist/JSON.sortify' } }); +define([ + '/common/cryptpad-common.js', + '/common/rpc.js', + '/bower_components/jquery/dist/jquery.min.js', +], function (Cryptpad, RPC) { + var $ = window.jQuery; + var APP = window.APP = { + Cryptpad: Cryptpad, + }; + + $(function () { + Cryptpad.ready(function (err, env) { + var network = Cryptpad.getNetwork(); + var rpc = RPC.create(network); // TODO signing key + + var payload = { + a: Math.floor(Math.random() * 1000), + b: 7, + }; + + // console.log(payload); + rpc.send('ECHO', payload, function (e, msg) { + if (e) { return void console.error(e); } + console.log(msg); + }); + + // test a non-existent RPC call + rpc.send('PEWPEW', ['pew'], function (e, msg) { + if (e) { return void console.error(e); } + console.log(msg); + }); + }); + }); +});