diff --git a/.gitignore b/.gitignore index 2d0cd09d9..354096b87 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +datastore www/bower_components/* node_modules /config.js diff --git a/NetfluxWebsocketSrv.js b/NetfluxWebsocketSrv.js index d92510749..b15cdd33b 100644 --- a/NetfluxWebsocketSrv.js +++ b/NetfluxWebsocketSrv.js @@ -1,7 +1,6 @@ ;(function () { 'use strict'; const Crypto = require('crypto'); -const LogStore = require('./storage/LogStore'); - +const Nacl = require('tweetnacl'); const LAG_MAX_BEFORE_DISCONNECT = 30000; const LAG_MAX_BEFORE_PING = 15000; @@ -10,12 +9,17 @@ const HISTORY_KEEPER_ID = Crypto.randomBytes(8).toString('hex'); const USE_HISTORY_KEEPER = true; const USE_FILE_BACKUP_STORAGE = true; - let dropUser; +let historyKeeperKeys = {}; const now = function () { return (new Date()).getTime(); }; +const socketSendable = function (socket) { + return socket && socket.readyState === 1; +}; + const sendMsg = function (ctx, user, msg) { + if (!socketSendable(user.socket)) { return; } try { if (ctx.config.logToStdout) { console.log('<' + JSON.stringify(msg)); } user.socket.send(JSON.stringify(msg)); @@ -25,6 +29,16 @@ const sendMsg = function (ctx, user, msg) { } }; +const storeMessage = function (ctx, channel, msg) { + ctx.store.message(channel.id, msg, function (err) { + if (err && typeof(err) !== 'function') { + // ignore functions because older datastores + // might pass waitFors into the callback + console.log("Error writing message: " + err); + } + }); +}; + const sendChannelMessage = function (ctx, channel, msgStruct) { msgStruct.unshift(0); channel.forEach(function (user) { @@ -33,7 +47,17 @@ const sendChannelMessage = function (ctx, channel, msgStruct) { } }); if (USE_HISTORY_KEEPER && msgStruct[2] === 'MSG') { - ctx.store.message(channel.id, JSON.stringify(msgStruct), function () { }); + if (historyKeeperKeys[channel.id]) { + let signedMsg = msgStruct[4].replace(/^cp\|/, ''); + signedMsg = Nacl.util.decodeBase64(signedMsg); + let validateKey = Nacl.util.decodeBase64(historyKeeperKeys[channel.id]); + let validated = Nacl.sign.open(signedMsg, validateKey); + if (!validated) { + console.log("Signed message rejected"); + return; + } + } + storeMessage(ctx, channel, JSON.stringify(msgStruct)); } }; @@ -57,11 +81,17 @@ dropUser = function (ctx, user) { let chan = ctx.channels[chanName]; let idx = chan.indexOf(user); if (idx < 0) { return; } - console.log("Removing ["+user.id+"] from channel ["+chanName+"]"); + + if (ctx.config.verbose) { + console.log("Removing ["+user.id+"] from channel ["+chanName+"]"); + } chan.splice(idx, 1); if (chan.length === 0) { - console.log("Removing empty channel ["+chanName+"]"); + if (ctx.config.verbose) { + console.log("Removing empty channel ["+chanName+"]"); + } delete ctx.channels[chanName]; + delete historyKeeperKeys[chanName]; /* Call removeChannel if it is a function and channel removal is set to true in the config file */ @@ -71,7 +101,9 @@ dropUser = function (ctx, user) { ctx.store.removeChannel(chanName, function (err) { if (err) { console.error("[removeChannelErr]: %s", err); } else { - console.log("Deleted channel [%s] history from database...", chanName); + if (ctx.config.verbose) { + console.log("Deleted channel [%s] history from database...", chanName); + } } }); }, ctx.config.channelRemovalTimeout); @@ -88,9 +120,20 @@ dropUser = function (ctx, user) { const getHistory = function (ctx, channelName, handler, cb) { var messageBuf = []; + var messageKey; ctx.store.getMessages(channelName, function (msgStr) { - messageBuf.push(JSON.parse(msgStr)); - }, function () { + var parsed = JSON.parse(msgStr); + if (parsed.validateKey) { + historyKeeperKeys[channelName] = parsed.validateKey; + handler(parsed); + return; + } + messageBuf.push(parsed); + }, function (err) { + if (err) { + console.log("Error getting messages " + err.stack); + // TODO: handle this better + } var startPoint; var cpCount = 0; var msgBuff2 = []; @@ -110,7 +153,7 @@ const getHistory = function (ctx, channelName, handler, cb) { // no checkpoints. for (var x = msgBuff2.pop(); x; x = msgBuff2.pop()) { handler(x); } } - cb(); + cb(messageBuf); }); }; @@ -156,8 +199,15 @@ const handleMessage = function (ctx, user, msg) { sendMsg(ctx, user, [seq, 'ACK']); getHistory(ctx, parsed[1], function (msg) { sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(msg)]); - }, function () { - sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, 0]); + }, function (messages) { + // parsed[2] is a validation key if it exists + if (messages.length === 0 && parsed[2] && !historyKeeperKeys[parsed[1]]) { + var key = {channel: parsed[1], validateKey: parsed[2]}; + storeMessage(ctx, ctx.channels[parsed[1]], JSON.stringify(key)); + historyKeeperKeys[parsed[1]] = parsed[2]; + } + let parsedMsg = {state: 1, channel: parsed[1]}; + sendMsg(ctx, user, [0, HISTORY_KEEPER_ID, 'MSG', user.id, JSON.stringify(parsedMsg)]); }); } return; @@ -212,7 +262,7 @@ let run = module.exports.run = function (storage, socketServer, config) { users: {}, channels: {}, timeouts: {}, - store: (USE_FILE_BACKUP_STORAGE) ? LogStore.create('messages.log', storage) : storage, + store: storage, config: config }; setInterval(function () { @@ -227,7 +277,7 @@ let run = module.exports.run = function (storage, socketServer, config) { }); }, 5000); socketServer.on('connection', function(socket) { - if(socket.upgradeReq.url !== '/cryptpad_websocket') { return; } + if(socket.upgradeReq.url !== (config.websocketPath || '/cryptpad_websocket')) { return; } let conn = socket.upgradeReq.connection; let user = { addr: conn.remoteAddress + '|' + conn.remotePort, diff --git a/bower.json b/bower.json index 60040c9ee..c7100f505 100644 --- a/bower.json +++ b/bower.json @@ -20,7 +20,9 @@ "dependencies": { "jquery": "~2.1.3", "tweetnacl": "~0.12.2", + "components-font-awesome": "^4.6.3", "ckeditor": "~4.5.6", + "codemirror": "^5.19.0", "requirejs": "~2.1.15", "reconnectingWebsocket": "", "marked": "~0.3.5", diff --git a/config.js.dist b/config.js.dist index 4b5702574..76518baea 100644 --- a/config.js.dist +++ b/config.js.dist @@ -8,61 +8,99 @@ module.exports = { httpAddress: '::', // the port on which your httpd will listen + + /* Cryptpad can be configured to send customized HTTP Headers + * These settings may vary widely depending on your needs + * Examples are provided below + */ + +/* + httpHeaders: { + "Content-Security-Policy": [ + "default-serc 'none'", + "style-src 'unsafe-inline' 'self'", + "script-src 'self' 'unsafe-eval' 'unsafe-inline'", + "child-src 'self' cryptpad.fr *.cryptpad.fr", + "font-src 'self'", + "connect-src 'self' wss://cryptpad.fr", + // data: is used by codemirror, (insecure remote) images are included by + // users of the wysiwyg who embed photos in their pads + "img-src data: *", + ].join('; '), + + "X-XSS-Protection": "1; mode=block", + "X-Content-Type-Options": "nosniff", + // 'X-Frame-Options': 'SAMEORIGIN', + },*/ + httpPort: 3000, - // the port used for websockets - websocketPort: 3000, + + /* your server's websocket url is configurable + * (default: '/cryptpad_websocket') + * + * websocketPath can be relative, of the form '/path/to/websocket' + * or absolute, specifying a particular URL + * + * 'wss://cryptpad.fr:3000/cryptpad_websocket' + */ + websocketPath: '/cryptpad_websocket', + + /* it is assumed that your websocket will bind to the same port as http + * you can override this behaviour by supplying a number via websocketPort + */ + //websocketPort: 3000, + + /* If Cryptpad is proxied without using https, the server needs to know. + * Specify 'useSecureWebsockets: true' so that it can send + * Content Security Policy Headers that prevent http and https from mixing + */ + useSecureWebsockets: false, /* Cryptpad can log activity to stdout * This may be useful for debugging */ logToStdout: false, - /* Cryptpad can be configured to remove channels some number of ms - after the last remaining client has disconnected. + /* Cryptpad supports verbose logging + * (false by default) + */ + verbose: false, - Default behaviour is to keep channels forever. - If you enable channel removal, the default removal time is one minute - */ - removeChannels: false, - channelRemovalTimeout: 60000, - // You now have a choice of storage engines + /* + You have the option of specifying an alternative storage adaptor. + These status of these alternatives are specified in their READMEs, + which are available at the following URLs: - /* amnesiadb only exists in memory. - * it will not persist across server restarts - * it will not scale well if your server stays alive for a long time. - * but it is completely dependency free - */ - //storage: './storage/amnesia', - - /* the 'lvl' storage module uses leveldb - * it persists, and will perform better than amnesiadb - * you will need to run 'npm install level' to use it - * - * you can provide a path to a database folder, which will be created - * if it does not already exist. If you use level and do not pass a path - * it will be created at cryptpad/test.level.db - * - * to delete all pads, run `rm -rf $YOUR_DB` - */ - storage: './storage/lvl', - levelPath: './test.level.db' + mongodb: a noSQL database + https://github.com/xwiki-labs/cryptpad-mongo-store + amnesiadb: in memory storage + https://github.com/xwiki-labs/cryptpad-amnesia-store + leveldb: a simple, fast, key-value store + https://github.com/xwiki-labs/cryptpad-level-store + sql: an adaptor for a variety of sql databases via knexjs + https://github.com/xwiki-labs/cryptpad-sql-store - /* mongo is the original storage engine for cryptpad - * it has been more thoroughly tested, but requires a little more setup - */ - // storage: './storage/mongo', + For the most up to date solution, use the default storage adaptor. + */ + storage: './storage/file', + + /* + Cryptpad stores each document in an individual file on your hard drive. + Specify a directory where files should be stored. + It will be created automatically if it does not already exist. + */ + filePath: './datastore/', - /* this url is accessible over the internet, it is useful for testing - * but should not be used in production + /* Cryptpad's file storage adaptor closes unused files after a configurale + * number of milliseconds (default 30000 (30 seconds)) */ - // mongoUri: "mongodb://demo_user:demo_password@ds027769.mongolab.com:27769/demo_database", + channelExpirationMs: 30000, - /* mongoUri should really be used to refer to a local installation of mongodb - * to install the mongodb client, run `npm install mongodb` + /* Cryptpad's file storage adaptor is limited by the number of open files. + * When the adaptor reaches openFileLimit, it will clean up older files */ - // mongoUri: "mongodb://localhost:27017/cryptpad", - // mongoCollectionName: 'cryptpad', + openFileLimit: 2048, /* it is recommended that you serve cryptpad over https * the filepaths below are used to configure your certificates diff --git a/customize.dist/BottomBar.html b/customize.dist/BottomBar.html index 03fdaf8b4..c8d43dfce 100644 --- a/customize.dist/BottomBar.html +++ b/customize.dist/BottomBar.html @@ -2,32 +2,14 @@
- +
-

- - An - XWiki SAS Labs Project with the support of - OpenPaaS-ng - - +

diff --git a/customize.dist/DecorateToolbar.js b/customize.dist/DecorateToolbar.js index e8d97e788..49a30c79f 100644 --- a/customize.dist/DecorateToolbar.js +++ b/customize.dist/DecorateToolbar.js @@ -2,19 +2,35 @@ globals define */ define([ + '/customize/languageSelector.js', + '/customize/messages.js', '/bower_components/jquery/dist/jquery.min.js' -], function () { +], function (LS, Messages) { var $ = window.jQuery; var main = function () { + var url = window.location.pathname; + var isHtml = /\.html/.test(url) || url === '/' || url === ''; + var isPoll = /\/poll\//.test(url); + if (!isHtml && !isPoll) { + Messages._applyTranslation(); + return; + } $.ajax({ - url: '/customize/BottomBar.html', + url: isHtml ? '/customize/BottomBar.html' : '/customize/Header.html', success: function (ret) { - $('iframe').height('96%'); - $('body').append(ret); - $('head').append($('', { - rel: 'stylesheet', - href: '/customize/main.css' - })); + var $bar = $(ret); + $('body').append($bar); + + var $sel = $bar.find('#language-selector'); + + Object.keys(Messages._languages).forEach(function (code) { + $sel.append($('