Skip to content
Permalink
Browse files

and so it begins

  • Loading branch information...
cjdelisle committed Oct 31, 2014
0 parents commit 1508c7ba71f5de5e51f061fbef45bc1f18493832
Showing with 7,627 additions and 0 deletions.
  1. +3 −0 .bowerrc
  2. +2 −0 .gitignore
  3. +189 −0 ChainPadSrv.js
  4. +55 −0 Storage.js
  5. BIN and_so_it_begins.png
  6. +26 −0 bower.json
  7. +9 −0 package.json
  8. +17 −0 readme.md
  9. +24 −0 server.js
  10. +1,434 −0 www/chainpad.js
  11. +483 −0 www/html-patcher.js
  12. +16 −0 www/index.html
  13. +52 −0 www/main.js
  14. +1,003 −0 www/otaml.js
  15. +3,738 −0 www/rangy.js
  16. +576 −0 www/realtime-wysiwyg.js
@@ -0,0 +1,3 @@
{
"directory" : "www/bower"
}
@@ -0,0 +1,2 @@
www/bower/*
node_modules
@@ -0,0 +1,189 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var WebSocket = require('ws');

var REGISTER = 0;
var REGISTER_ACK = 1;
var PATCH = 2;
var DISCONNECT = 3;
var PING = 4;
var PONG = 5;

var parseMessage = function (msg) {
var passLen = msg.substring(0,msg.indexOf(':'));
msg = msg.substring(passLen.length+1);
var pass = msg.substring(0,Number(passLen));
msg = msg.substring(pass.length);

var unameLen = msg.substring(0,msg.indexOf(':'));
msg = msg.substring(unameLen.length+1);
var userName = msg.substring(0,Number(unameLen));
msg = msg.substring(userName.length);

var channelIdLen = msg.substring(0,msg.indexOf(':'));
msg = msg.substring(channelIdLen.length+1);
var channelId = msg.substring(0,Number(channelIdLen));
msg = msg.substring(channelId.length);

var contentStrLen = msg.substring(0,msg.indexOf(':'));
msg = msg.substring(contentStrLen.length+1);
var contentStr = msg.substring(0,Number(contentStrLen));

return {
user: userName,
pass: pass,
channelId: channelId,
content: JSON.parse(contentStr)
};
};

// get the password off the message before sending it to other clients.
var popPassword = function (msg) {
var passLen = msg.substring(0,msg.indexOf(':'));
return msg.substring(passLen.length+1 + Number(passLen));
};

var sendMsg = function (msg, socket) {
socket.send(msg);
};

var sendChannelMessage = function (ctx, channel, msg, cb) {
ctx.store.message(channel.name, msg, function () {
channel.forEach(function (user) {
try {
sendMsg(msg, user.socket);
} catch (e) {
console.log(e.stack);
dropClient(ctx, userPass);
}
});
cb && cb();
});
};

var mkMessage = function (user, channel, content) {
content = JSON.stringify(content);
return user.length + ':' + user +
channel.length + ':' + channel +
content.length + ':' + content;
};

var dropClient = function (ctx, userpass) {
var client = ctx.registeredClients[userpass];
if (client.socket.readyState !== WebSocket.CLOSING
&& client.socket.readyState !== WebSocket.CLOSED)
{
try {
client.socket.close();
} catch (e) {
console.log("Failed to disconnect ["+client.userName+"], attempting to terminate");
try {
client.socket.terminate();
} catch (ee) {
console.log("Failed to terminate ["+client.userName+"] *shrug*");
}
}
}

for (var i = 0; i < client.channels.length; i++) {
var chanName = client.channels[i];
var chan = ctx.channels[chanName];
var idx = chan.indexOf(client);
if (idx < 0) { throw new Error(); }
console.log("Removing ["+client.userName+"] from channel ["+chanName+"]");
chan.splice(idx, 1);
if (chan.length === 0) {
console.log("Removing empty channel ["+chanName+"]");
delete ctx.channels[chanName];
} else {
sendChannelMessage(ctx, chan, mkMessage(client.userName, chanName, [DISCONNECT,0]));
}
}
delete ctx.registeredClients[userpass];
};

var handleMessage = function (ctx, socket, msg) {
var parsed = parseMessage(msg);
var userPass = parsed.user + ':' + parsed.pass;
msg = popPassword(msg);

if (parsed.content[0] === REGISTER) {
console.log("[" + userPass + "] registered");
var client = ctx.registeredClients[userPass] = ctx.registeredClients[userPass] || {
channels: [parsed.channelId],
userName: parsed.user
};
if (client.socket && client.socket !== socket) { client.socket.close(); }
client.socket = socket;

var chan = ctx.channels[parsed.channelId] = ctx.channels[parsed.channelId] || [];
chan.name = parsed.channelId;
chan.push(client);

// we send a register ack right away but then we fallthrough
// to let other users know that we were registered.
sendMsg(mkMessage('', parsed.channelId, [1,0]), socket);
sendChannelMessage(ctx, chan, msg, function () {
ctx.store.getMessages(chan.name, function (msg) {
sendMsg(msg, socket);
});
});
}

if (parsed.content[0] === PING) {
// 31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[5,1414752676547]
// 1:y31:xwiki:XWiki.Admin-141475016907510:RWJ5xF2+SL17:[4,1414752676547]
sendMsg(mkMessage(parsed.user, parsed.channelId, [ PONG, parsed.content[1] ]), socket);
return;
}

var client = ctx.registeredClients[userPass];
if (typeof(client) === 'undefined') { throw new Error('unregistered'); }

var channel = ctx.channels[parsed.channelId];
if (typeof(channel) === 'undefined') { throw new Error('no such channel'); }

if (channel.indexOf(client) === -1) { throw new Error('client not in channel'); }

sendChannelMessage(ctx, channel, msg);
};

var create = module.exports.create = function (socketServer, store) {
var ctx = {
registeredClients: {},
channels: {},
store: store
};

socketServer.on('connection', function(socket) {
socket.on('message', function(message) {
try {
handleMessage(ctx, socket, message);
} catch (e) {
console.log(e.stack);
socket.close();
}
});
socket.on('close', function (evt) {
for (client in ctx.registeredClients) {
if (ctx.registeredClients[client].socket === socket) {
dropClient(ctx, client);
}
}
});
});
};
@@ -0,0 +1,55 @@
/*
* Copyright 2014 XWiki SAS
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
var MongoClient = require('mongodb').MongoClient;

var MONGO_URI = "mongodb://demo_user:demo_password@ds027769.mongolab.com:27769/demo_database";
var COLLECTION_NAME = 'cryptpad';

var insert = function (coll, channelName, content, cb) {
var val = {chan: channelName, msg:content, time: (new Date()).getTime()};
coll.insertOne(val, {}, function (err, r) {
if (err || (r.insertedCount !== 1)) {
console.log('failed to insert ' + err);
return;
}
cb();
});
};

var getMessages = function (coll, channelName, cb) {
coll.find({chan:channelName}).forEach(function (doc) {
cb(doc.msg);
}, function (err) {
if (!err) { return; }
console.log('error ' + err);
});
};

module.exports.create = function (conf, cb) {
MongoClient.connect(conf.mongoUri, function(err, db) {
var coll = db.collection(conf.mongoCollectionName);
if (err) { throw err; }
cb({
message: function (channelName, content, cb) {
insert(coll, channelName, content, cb);
},
getMessages: function (channelName, msgHandler) {
getMessages(coll, channelName, msgHandler);
}
});
});
};
BIN +58.2 KB and_so_it_begins.png
Binary file not shown.
@@ -0,0 +1,26 @@
{
"name": "cryptpad",
"version": "0.1.0",
"authors": [
"Caleb James DeLisle <cjd@cjdns.fr>"
],
"description": "realtime collaborative visual editor with zero knowlege server",
"main": "www/index.html",
"moduleType": [
"node"
],
"license": "AGPLv3",
"ignore": [
"**/.*",
"node_modules",
"bower_components",
"test",
"tests"
],
"dependencies": {
"jquery": "~2.1.1",
"tweetnacl": "~0.12.2",
"ckeditor": "~4.4.5",
"requirejs": "~2.1.15"
}
}
@@ -0,0 +1,9 @@
{
"name": "cryptpad",
"description": "realtime collaborative visual editor with zero knowlege server",
"dependencies": {
"express": "~4.10.1",
"ws": "~0.4.32",
"mongodb": "~2.0.5"
}
}
@@ -0,0 +1,17 @@
# CryptPad

Unity is Strength - Collaboration is Key

![and_so_it_begins.png](https://github.com/cjdelisle/cryptpad/raw/master/and_so_it_begins.png "We are the 99%")

CryptPad is a **zero knowledge** realtime collaborative editor.
Encryption carried out in your web browser protects the data from the server, the cloud
and the NSA. This project uses the [CKEdit] Visual Editor and the [ChainPad] realtime
engine. The secret key is stored in the URL [fragment identifier] which is never sent to
the server but is available to javascript so by sharing the URL, you give authorization


Realtime Collaboration with


[fragment identifier]: http://en.wikipedia.org/wiki/Fragment_identifier
@@ -0,0 +1,24 @@
var Express = require('express');
var Http = require('http');
var WebSocketServer = require('ws').Server;
var ChainPadSrv = require('./ChainPadSrv');
var Storage = require('./Storage');

var config = {
httpPort: 3000,
mongoUri: "mongodb://demo_user:demo_password@ds027769.mongolab.com:27769/demo_database",
mongoCollectionName: 'cryptpad'
};

var app = Express();
app.use(Express.static(__dirname + '/www'));

var httpServer = Http.createServer(app);
httpServer.listen(config.httpPort);
console.log('listening on port ' + config.httpPort);

var wsSrv = new WebSocketServer({server: httpServer});
Storage.create(config, function (store) {
console.log('DB connected');
ChainPadSrv.create(wsSrv, store);
});

0 comments on commit 1508c7b

Please sign in to comment.
You can’t perform that action at this time.