From 50c10f818ea726249cdb75307c76d8b69bf9301e Mon Sep 17 00:00:00 2001 From: Caleb James DeLisle Date: Thu, 29 Jan 2015 17:55:18 +0100 Subject: [PATCH] Added cryptsheet to cryptpad suite! --- .bowerrc | 2 +- .gitignore | 2 +- bower.json | 3 +- server.js | 6 + www/{ => common}/chainpad.js | 25 ++-- www/{ => common}/messages.js | 0 www/{ => common}/otaml.js | 0 www/common/toolbar.js | 172 +++++++++++++++++++++++++ www/index.html | 45 ++++--- www/{ => pad}/errorbox.js | 8 +- www/{ => pad}/html-patcher.js | 4 +- www/pad/index.html | 11 ++ www/{ => pad}/main.js | 17 ++- www/{ => pad}/rangy.js | 0 www/{ => pad}/realtime-wysiwyg.js | 18 +-- www/sheet/index.html | 29 +++++ www/sheet/inner.html | 36 ++++++ www/sheet/main.js | 201 ++++++++++++++++++++++++++++++ 18 files changed, 525 insertions(+), 54 deletions(-) rename www/{ => common}/chainpad.js (98%) rename www/{ => common}/messages.js (100%) rename www/{ => common}/otaml.js (100%) create mode 100644 www/common/toolbar.js rename www/{ => pad}/errorbox.js (93%) rename www/{ => pad}/html-patcher.js (99%) create mode 100644 www/pad/index.html rename www/{ => pad}/main.js (81%) rename www/{ => pad}/rangy.js (100%) rename www/{ => pad}/realtime-wysiwyg.js (98%) create mode 100644 www/sheet/index.html create mode 100644 www/sheet/inner.html create mode 100644 www/sheet/main.js diff --git a/.bowerrc b/.bowerrc index 67b62e774..6a424c379 100644 --- a/.bowerrc +++ b/.bowerrc @@ -1,3 +1,3 @@ { - "directory" : "www/bower" + "directory" : "www/bower_components" } diff --git a/.gitignore b/.gitignore index 1157b14cc..aa661d690 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ -www/bower/* +www/bower_components/* node_modules /config.js diff --git a/bower.json b/bower.json index e7f0fc2c4..34511bc3e 100644 --- a/bower.json +++ b/bower.json @@ -18,7 +18,8 @@ "tests" ], "dependencies": { - "jquery": "~2.1.1", + "jquery.sheet": "master", + "jquery": "~2.1.3", "tweetnacl": "~0.12.2", "ckeditor": "~4.4.5", "requirejs": "~2.1.15", diff --git a/server.js b/server.js index bc5b41c0e..4a1f8e39a 100644 --- a/server.js +++ b/server.js @@ -12,6 +12,12 @@ config.websocketPort = config.websocketPort || config.httpPort; var app = Express(); app.use(Express.static(__dirname + '/www')); +// Bower is broken and does not allow components nested within components... +// And jquery.sheet expects it! +// *Workaround* +app.use("/bower_components/jquery.sheet/bower_components", + Express.static(__dirname + '/www/bower_components')); + var httpsOpts; if (config.privKeyAndCertFiles) { var privKeyAndCerts = ''; diff --git a/www/chainpad.js b/www/common/chainpad.js similarity index 98% rename from www/chainpad.js rename to www/common/chainpad.js index 82e8102b4..b872b17f0 100644 --- a/www/chainpad.js +++ b/www/common/chainpad.js @@ -589,6 +589,18 @@ var unschedule = function (realtime, schedule) { clearTimeout(schedule); }; +var onMessage = function (realtime, message, callback) { + if (!realtime.messageHandlers.length) { + callback("no onMessage() handler registered"); + } + for (var i = 0; i < realtime.messageHandlers.length; i++) { + realtime.messageHandlers[i](message, function () { + callback.apply(null, arguments); + callback = function () { }; + }); + } +}; + var sync = function (realtime) { if (Common.PARANOIA) { check(realtime); } if (realtime.syncSchedule) { @@ -622,7 +634,7 @@ var sync = function (realtime) { var strMsg = Message.toString(msg); - realtime.onMessage(strMsg, function (err) { + onMessage(realtime, strMsg, function (err) { if (err) { debug(realtime, "Posting to server failed [" + err + "]"); } @@ -657,7 +669,7 @@ var getMessages = function (realtime) { realtime.authToken, realtime.channelId, Message.REGISTER); - realtime.onMessage(Message.toString(msg), function (err) { + onMessage(realtime, Message.toString(msg), function (err) { if (err) { throw err; } }); }; @@ -670,7 +682,7 @@ var sendPing = function (realtime) { realtime.channelId, Message.PING, realtime.lastPingTime); - realtime.onMessage(Message.toString(msg), function (err) { + onMessage(realtime, Message.toString(msg), function (err) { if (err) { throw err; } }); }; @@ -706,9 +718,7 @@ var create = ChainPad.create = function (userName, authToken, channelId, initial patchHandlers: [], opHandlers: [], - onMessage: function (message, callback) { - callback("no onMessage() handler registered"); - }, + messageHandlers: [], schedules: [], @@ -1127,7 +1137,8 @@ module.exports.create = function (userName, authToken, channelId, initialState, doOperation(realtime, Operation.create(offset, 0, str)); }), onMessage: enterChainPad(realtime, function (handler) { - realtime.onMessage = handler; + Common.assert(typeof(handler) === 'function'); + realtime.messageHandlers.push(handler); }), message: enterChainPad(realtime, function (message) { handleMessage(realtime, message); diff --git a/www/messages.js b/www/common/messages.js similarity index 100% rename from www/messages.js rename to www/common/messages.js diff --git a/www/otaml.js b/www/common/otaml.js similarity index 100% rename from www/otaml.js rename to www/common/otaml.js diff --git a/www/common/toolbar.js b/www/common/toolbar.js new file mode 100644 index 000000000..3399e804b --- /dev/null +++ b/www/common/toolbar.js @@ -0,0 +1,172 @@ +var Toolbar = function ($, container, Messages, myUserName, realtime) { + + /** Id of the element for getting debug info. */ + var DEBUG_LINK_CLS = 'rtwysiwyg-debug-link'; + + /** Id of the div containing the user list. */ + var USER_LIST_CLS = 'rtwysiwyg-user-list'; + + /** Id of the div containing the lag info. */ + var LAG_ELEM_CLS = 'rtwysiwyg-lag'; + + /** The toolbar class which contains the user list, debug link and lag. */ + var TOOLBAR_CLS = 'rtwysiwyg-toolbar'; + + /** Key in the localStore which indicates realtime activity should be disallowed. */ + var LOCALSTORAGE_DISALLOW = 'rtwysiwyg-disallow'; + + var SPINNER_DISAPPEAR_TIME = 3000; + var SPINNER = [ '-', '\\', '|', '/' ]; + + var uid = function () { + return 'rtwysiwyg-uid-' + String(Math.random()).substring(2); + }; + + var createRealtimeToolbar = function (container) { + var id = uid(); + $(container).prepend( + '
' + + '
' + + '
' + + '
' + ); + var toolbar = $('#'+id); + toolbar.append([ + '' + ].join('\n')); + return toolbar; + }; + + var createSpinner = function (container) { + var id = uid(); + $(container).append('
'); + return $('#'+id)[0]; + }; + + var kickSpinner = function (spinnerElement, reversed) { + var txt = spinnerElement.textContent || '-'; + var inc = (reversed) ? -1 : 1; + spinnerElement.textContent = SPINNER[(SPINNER.indexOf(txt) + inc) % SPINNER.length]; + spinnerElement.timeout && clearTimeout(spinnerElement.timeout); + spinnerElement.timeout = setTimeout(function () { + spinnerElement.textContent = ''; + }, SPINNER_DISAPPEAR_TIME); + }; + + var createUserList = function (container) { + var id = uid(); + $(container).prepend('
'); + return $('#'+id)[0]; + }; + + var updateUserList = function (myUserName, listElement, userList) { + var meIdx = userList.indexOf(myUserName); + if (meIdx === -1) { + listElement.textContent = Messages.synchronizing; + return; + } + if (userList.length === 1) { + listElement.textContent = Messages.editingAlone; + } else if (userList.length === 2) { + listElement.textContent = Messages.editingWithOneOtherPerson; + } else { + listElement.textContent = Messages.editingWith + ' ' + (userList.length - 1) + ' ' + Messages.otherPeople; + } + }; + + var createLagElement = function (container) { + var id = uid(); + $(container).append('
'); + return $('#'+id)[0]; + }; + + var checkLag = function (realtime, lagElement) { + var lag = realtime.getLag(); + var lagSec = lag.lag/1000; + var lagMsg = Messages.lag + ' '; + if (lag.waiting && lagSec > 1) { + lagMsg += "?? " + Math.floor(lagSec); + } else { + lagMsg += lagSec; + } + lagElement.textContent = lagMsg; + }; + + + var toolbar = createRealtimeToolbar(container); + var userListElement = createUserList(toolbar.find('.rtwysiwyg-toolbar-leftside')); + var spinner = createSpinner(toolbar.find('.rtwysiwyg-toolbar-rightside')); + var lagElement = createLagElement(toolbar.find('.rtwysiwyg-toolbar-rightside')); + + var connected = false; + + realtime.onUserListChange(function (userList) { + if (userList.indexOf(myUserName) !== -1) { connected = true; } + if (!connected) { return; } + updateUserList(myUserName, userListElement, userList); + }); + + var ks = function () { + if (connected) { kickSpinner(spinner, false); } + }; + + realtime.onPatch(ks); + realtime.onMessage(ks); + + setInterval(function () { + if (!connected) { return; } + checkLag(realtime, lagElement); + }, 3000); + + return { + reconnecting: function () { + connected = false; + userListElement.textContent = Messages.reconnecting; + lagElement.textContent = ''; + }, + connected: function () { + connected = true; + } + }; +}; diff --git a/www/index.html b/www/index.html index 631638b55..ce3cbc7a6 100644 --- a/www/index.html +++ b/www/index.html @@ -3,25 +3,32 @@ - - + @@ -48,16 +55,14 @@ get caught and laughed at and humiliated in front of the whole world (again). If you're making the NSA mad enough for them to use an active attack against you, Great Success Highfive, now take the battery out of your computer before it spawns Agent Smith.

+
+
Try it out!
+ +
-
-
-
Try it out!
-
- -
- -
diff --git a/www/errorbox.js b/www/pad/errorbox.js similarity index 93% rename from www/errorbox.js rename to www/pad/errorbox.js index 4e06e62b0..f0c214225 100644 --- a/www/errorbox.js +++ b/www/pad/errorbox.js @@ -16,12 +16,14 @@ */ require.config({ 'shim': { - 'bower/modalBox/modalBox-min': ['bower/jquery/dist/jquery.min'], + '/bower_components/modalBox/modalBox-min.js': [ + '/bower_components/jquery/dist/jquery.min.js' + ], } }); define([ - 'messages', - 'bower/modalBox/modalBox-min' + '/common/messages.js', + '/bower_components/modalBox/modalBox-min.js' ], function (Messages) { var STYLE = [ diff --git a/www/html-patcher.js b/www/pad/html-patcher.js similarity index 99% rename from www/html-patcher.js rename to www/pad/html-patcher.js index 3ac77d154..8859c668e 100644 --- a/www/html-patcher.js +++ b/www/pad/html-patcher.js @@ -15,8 +15,8 @@ * along with this program. If not, see . */ define([ - 'bower/jquery/dist/jquery.min', - 'otaml' + '/bower_components/jquery/dist/jquery.min.js', + '/common/otaml.js' ], function () { var $ = jQuery; diff --git a/www/pad/index.html b/www/pad/index.html new file mode 100644 index 000000000..b5f3f1d87 --- /dev/null +++ b/www/pad/index.html @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/www/main.js b/www/pad/main.js similarity index 81% rename from www/main.js rename to www/pad/main.js index b909200dc..b09453399 100644 --- a/www/main.js +++ b/www/pad/main.js @@ -1,10 +1,10 @@ define([ - 'api/config?cb=' + Math.random().toString(16).substring(2), - 'realtime-wysiwyg', - 'messages', - 'bower/jquery/dist/jquery.min', - 'bower/ckeditor/ckeditor', - 'bower/tweetnacl/nacl-fast.min', + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/pad/realtime-wysiwyg.js', + '/common/messages.js', + '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/ckeditor/ckeditor.js', + '/bower_components/tweetnacl/nacl-fast.min.js', ], function (Config, RTWysiwyg, Messages) { var Ckeditor = window.CKEDITOR; var Nacl = window.nacl; @@ -31,10 +31,7 @@ define([ window.location.reload(); }); if (window.location.href.indexOf('#') === -1) { - $('#create-pad').click(function (ev) { - ev.preventDefault(); - window.location.href = window.location.href + '#' + genKey(); - }); + window.location.href = window.location.href + '#' + genKey(); return; } var key = parseKey(window.location.hash.substring(1)); diff --git a/www/rangy.js b/www/pad/rangy.js similarity index 100% rename from www/rangy.js rename to www/pad/rangy.js diff --git a/www/realtime-wysiwyg.js b/www/pad/realtime-wysiwyg.js similarity index 98% rename from www/realtime-wysiwyg.js rename to www/pad/realtime-wysiwyg.js index 0117fccb9..54b2d4bf9 100644 --- a/www/realtime-wysiwyg.js +++ b/www/pad/realtime-wysiwyg.js @@ -15,15 +15,15 @@ * along with this program. If not, see . */ define([ - 'html-patcher', - 'errorbox', - 'messages', - 'bower/reconnectingWebsocket/reconnecting-websocket', - 'rangy', - 'chainpad', - 'otaml', - 'bower/jquery/dist/jquery.min', - 'bower/tweetnacl/nacl-fast.min' + '/pad/html-patcher.js', + '/pad/errorbox.js', + '/common/messages.js', + '/bower_components/reconnectingWebsocket/reconnecting-websocket.js', + '/pad/rangy.js', + '/common/chainpad.js', + '/common/otaml.js', + '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/tweetnacl/nacl-fast.min.js' ], function (HTMLPatcher, ErrorBox, Messages, ReconnectingWebSocket) { window.ErrorBox = ErrorBox; diff --git a/www/sheet/index.html b/www/sheet/index.html new file mode 100644 index 000000000..9cad4f2bb --- /dev/null +++ b/www/sheet/index.html @@ -0,0 +1,29 @@ + + + + + + + + + + + + diff --git a/www/sheet/inner.html b/www/sheet/inner.html new file mode 100644 index 000000000..3b07cddea --- /dev/null +++ b/www/sheet/inner.html @@ -0,0 +1,36 @@ + + + + + + + + + jQuery.sheet - The Ajax Spreadsheet DTS Lazy Loading + + + + + + +
+ + diff --git a/www/sheet/main.js b/www/sheet/main.js new file mode 100644 index 000000000..749e187ad --- /dev/null +++ b/www/sheet/main.js @@ -0,0 +1,201 @@ +define([ + '/api/config?cb=' + Math.random().toString(16).substring(2), + '/common/messages.js', + '/common/toolbar.js', + '/common/chainpad.js', + '/bower_components/jquery/dist/jquery.min.js', + '/bower_components/tweetnacl/nacl-fast.min.js', + '/common/otaml.js' +], function (Config, Messages) { + var Nacl = window.nacl; + var $ = jQuery; + var ChainPad = window.ChainPad; + var Otaml = window.Otaml; + var Toolbar = window.Toolbar; + + var module = { exports: {} }; + + var parseKey = function (str) { + var array = Nacl.util.decodeBase64(str); + var hash = Nacl.hash(array); + return { lookupKey: hash.subarray(32), cryptKey: hash.subarray(0,32) }; + }; + + var genKey = function () { + return Nacl.util.encodeBase64(Nacl.randomBytes(18)); + }; + + var userName = function () { + return 'Other-' + Nacl.util.encodeBase64(Nacl.randomBytes(8)); + }; + + var sheetToJson = function (ifrWindow) { + var xx = ifrWindow.sh[0].jS + var m = []; + for (var i = 0; i < xx.spreadsheets.length; i++) { + m[i]=[]; + var sheet = xx.spreadsheets[i]; + for (var j = 1; j < sheet.length; j++) { + m[i][j]=[]; + var row = sheet[j]; + for (var k = 1; k < row.length; k++) { + var col = row[k]; + m[i][j][k] = { value: col.value, formula: col.formula }; + } + } + } + return m; + }; + + var jsonToSheet = function (ifrWindow, json) { + var xx = ifrWindow.sh[0].jS; + for (var i = 0; i < xx.spreadsheets.length; i++) { + var sheet = xx.spreadsheets[i]; + for (var j = 1; j < sheet.length; j++) { + var row = sheet[j]; + for (var k = 1; k < row.length; k++) { + var col = row[k]; + var jcol = json[i][j][k]; + if (jcol.value === col.value && jcol.formula === col.formula) { continue; } + col.value = jcol.value; + col.formula = jcol.formula; + col.displayValue(); + } + } + } + }; + + var encryptStr = function (str, key) { + var array = Nacl.util.decodeUTF8(str); + var nonce = Nacl.randomBytes(24); + var packed = Nacl.secretbox(array, nonce, key); + if (!packed) { throw new Error(); } + return Nacl.util.encodeBase64(nonce) + "|" + Nacl.util.encodeBase64(packed); + }; + var decryptStr = function (str, key) { + var arr = str.split('|'); + if (arr.length !== 2) { throw new Error(); } + var nonce = Nacl.util.decodeBase64(arr[0]); + var packed = Nacl.util.decodeBase64(arr[1]); + var unpacked = Nacl.secretbox.open(packed, nonce, key); + if (!unpacked) { throw new Error(); } + return Nacl.util.encodeUTF8(unpacked); + }; + + // this is crap because of bencoding messages... it should go away.... + var splitMessage = function (msg, sending) { + var idx = 0; + var nl; + for (var i = ((sending) ? 0 : 1); i < 3; i++) { + nl = msg.indexOf(':',idx); + idx = nl + Number(msg.substring(idx,nl)) + 1; + } + return [ msg.substring(0,idx), msg.substring(msg.indexOf(':',idx) + 1) ]; + }; + + var encrypt = function (msg, key) { + var spl = splitMessage(msg, true); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + json[1] = encryptStr(JSON.stringify(json[1]), key); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; + }; + + var decrypt = function (msg, key) { + var spl = splitMessage(msg, false); + var json = JSON.parse(spl[1]); + // non-patches are not encrypted. + if (json[0] !== 2) { return msg; } + if (typeof(json[1]) !== 'string') { throw new Error(); } + json[1] = JSON.parse(decryptStr(json[1], key)); + var res = JSON.stringify(json); + return spl[0] + res.length + ':' + res; + }; + + var applyChange = function(ctx, oldval, newval) { + if (oldval === newval) return; + + var commonStart = 0; + while (oldval.charAt(commonStart) === newval.charAt(commonStart)) { + commonStart++; + } + + var commonEnd = 0; + while (oldval.charAt(oldval.length - 1 - commonEnd) === newval.charAt(newval.length - 1 - commonEnd) && + commonEnd + commonStart < oldval.length && commonEnd + commonStart < newval.length) { + commonEnd++; + } + + if (oldval.length !== commonStart + commonEnd) { + ctx.remove(commonStart, oldval.length - commonStart - commonEnd); + } + if (newval.length !== commonStart + commonEnd) { + ctx.insert(commonStart, newval.slice(commonStart, newval.length - commonEnd)); + } + }; + + $(function () { + + if (window.location.href.indexOf('#') === -1) { + window.location.href = window.location.href + '#' + genKey(); + } + $(window).on('hashchange', function() { + window.location.reload(); + }); + + var $sheetJson = $('#sheet-json'); + var ifrw = $('iframe')[0].contentWindow; + var sheetEvent = function (realtime) { + var sheetJson = JSON.stringify(sheetToJson(ifrw)); + applyChange(realtime, realtime.getUserDoc(), sheetJson); + $sheetJson.text(sheetJson); + }; + + var eventPending = false; + var realtimeEvent = function (realtime) { + if (eventPending) { return; } + eventPending = true; + setTimeout(function () { + eventPending = false; + try{ + var data = window.data = realtime.getUserDoc(); + $sheetJson.text(data); + var json = JSON.parse(data); + jsonToSheet(ifrw, json); + }catch(e) { console.log(e.stack); } + }, 0); + }; + + var key = parseKey(window.location.hash.substring(1)); + var channel = Nacl.util.encodeBase64(key.lookupKey).substring(0,10); + var myUserName = userName(); + + var socket = new WebSocket(Config.websocketURL); + socket.onopen = function () { + var realtime = ChainPad.create( + myUserName, 'x', channel, '', { transformFunction: Otaml.transform }); + socket.onmessage = function (evt) { + var message = decrypt(evt.data, key.cryptKey); + realtime.message(message); + }; + realtime.onMessage(function (message) { + message = encrypt(message, key.cryptKey); + try { + socket.send(message); + } catch (e) { + console.log(e.stack); + } + }); + ifrw.sh.on('sheetCellEdited', function () { sheetEvent(realtime); }); + sheetEvent(realtime); + realtime.onPatch(function () { realtimeEvent(realtime); }); + + ifrw.$('.jSTitle').html(''); + Toolbar(ifrw.$, ifrw.$('.jSTitle'), Messages, myUserName, realtime); + + realtime.start(); + }; + }); +});