summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/hornchat.authentication.js74
-rw-r--r--server/hornchat.authentication.token.js34
-rw-r--r--server/hornchat.authentication.totp.js57
-rw-r--r--server/hornchat.conversation.connect.js55
-rw-r--r--server/hornchat.conversation.js74
-rw-r--r--server/hornchat.conversation.message.js56
-rw-r--r--server/hornchat.conversation.read.js52
-rw-r--r--server/hornchat.conversation.start.js38
-rw-r--r--server/hornchat.conversation.typing.js34
-rw-r--r--server/hornchat.keyserver.disconnect.js46
-rw-r--r--server/hornchat.keyserver.js79
-rw-r--r--server/hornchat.keyserver.list.js24
-rw-r--r--server/hornchat.keyserver.read.js25
-rw-r--r--server/hornchat.keyserver.write.js16
-rw-r--r--server/hornchat.presence.js69
-rw-r--r--server/hornchat.presence.process.js11
-rw-r--r--server/hornchat.presence.tracking.js18
-rw-r--r--server/hornchat.profile.js59
-rw-r--r--server/hornchat.profile.process.js17
-rw-r--r--server/hornchat.profile.tracking.js19
-rw-r--r--server/hornchat.serverlet.authentication.js80
-rw-r--r--server/hornchat.serverlet.sync.js22
-rw-r--r--server/hornchat.serverlet.timeout.js10
-rw-r--r--server/hornchat.verification.js65
-rw-r--r--server/hornchat.verification.process.js18
-rw-r--r--server/hornchat.verification.safety.js14
-rw-r--r--server/hornchat.verification.tracking.js20
-rw-r--r--server/index.js39
28 files changed, 1125 insertions, 0 deletions
diff --git a/server/hornchat.authentication.js b/server/hornchat.authentication.js
new file mode 100644
index 0000000..99ef708
--- /dev/null
+++ b/server/hornchat.authentication.js
@@ -0,0 +1,74 @@
+require('./hornchat.serverlet.sync');
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+
+global.rateLimits = {};
+global.tokenFetchrateLimits = {};
+
+const server = new WebSocket.Server({
+ port: 8301
+});
+
+global.data = {};
+
+const _totp = require('./hornchat.authentication.totp');
+const _token = require('./hornchat.authentication.token');
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+
+ require('./hornchat.serverlet.timeout')(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function(msg) {
+ let data;
+ try {
+ data = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ return;
+ }
+
+ if (rateLimits[socket.ip] && new Date() - rateLimits[socket.ip] < 15000) {
+ socket.send(JSON.stringify({error:"RATE_LIMITED", success: false, device: null}));
+ console.log("[" + socket.id + "] IP address is being rate limited");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ return;
+ }
+
+ try {
+ if (data.username && data.totp) {
+ _totp(socket, data, req);
+ } else if (data.username && data.token) {
+ _token(socket, data);
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ } catch (e) {
+ console.error(e);
+ socket.send(JSON.stringify({error:"INTERNAL_ERROR", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ });
+});
+
+console.log("Listening on port 8301"); \ No newline at end of file
diff --git a/server/hornchat.authentication.token.js b/server/hornchat.authentication.token.js
new file mode 100644
index 0000000..dac9fcf
--- /dev/null
+++ b/server/hornchat.authentication.token.js
@@ -0,0 +1,34 @@
+module.exports = (socket, data) => {
+ if (tokenFetchrateLimits[socket.ip] && new Date() - tokenFetchrateLimits[socket.ip] < 30000) {
+ socket.send(JSON.stringify({error:"RATE_LIMITED", success: false, device: null}));
+ console.log("[" + socket.id + "] IP address is being rate limited");
+ tokenFetchrateLimits[socket.ip] = new Date();
+ socket.close();
+ return;
+ }
+
+ if (userCredentials.filter((i) => i.id === data.username).length > 0) {
+ if (userCredentials.filter((i) => i.id === data.username)[0].devices) {
+ let tokens = userCredentials.filter((i) => i.id === data.username)[0].devices.map((i) => {
+ return i.token;
+ });
+
+ if (tokens.includes(data.token)) {
+ socket.send(JSON.stringify({error:null, success: true, device: null}));
+ } else {
+ socket.send(JSON.stringify({error:null, success: false, device: null}));
+ tokenFetchrateLimits[socket.ip] = new Date();
+ }
+
+ socket.close();
+ } else {
+ socket.send(JSON.stringify({error:null, success: false, device: null}));
+ tokenFetchrateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ } else {
+ socket.send(JSON.stringify({error:null, success: false, device: null}));
+ tokenFetchrateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+} \ No newline at end of file
diff --git a/server/hornchat.authentication.totp.js b/server/hornchat.authentication.totp.js
new file mode 100644
index 0000000..ab2fb3e
--- /dev/null
+++ b/server/hornchat.authentication.totp.js
@@ -0,0 +1,57 @@
+const twofactor = require("node-2fa");
+const uuid = require("uuid-v4");
+const crypto = require("crypto");
+const fs = require("fs");
+
+module.exports = (socket, data, req) => {
+ console.log("[" + socket.id + "] Username:", data.username, "TOTP:", data.totp);
+
+ if (userCredentials.filter((i) => i.id === data.username).length > 0) {
+ if (userCredentials.filter((i) => i.id === data.username)[0].totp.secret) {
+ let verify = twofactor.verifyToken(userCredentials.filter((i) => i.id === data.username)[0].totp.secret, data.totp);
+
+ if (verify !== null) {
+ if (verify.delta > -2 && verify.delta < 2) {
+ let deviceInfo = {
+ id: uuid(),
+ token: crypto.randomBytes(256).toString('hex'),
+ platform: require('ua-parser').parse(req.headers['user-agent']),
+ addresses: [socket.ip],
+ firstSeen: new Date(),
+ lastSeen: new Date()
+ }
+
+ console.log("[" + socket.id + "] Authenticated successfully, added device " + deviceInfo.id);
+
+ global.userCredentials = userCredentials.map((i) => {
+ if (i.id === data.username) {
+ i.devices.push(deviceInfo);
+ }
+
+ return i;
+ })
+
+ fs.writeFileSync(dataPath + "/users.json", JSON.stringify(userCredentials, null, 2));
+
+ socket.send(JSON.stringify({error: null, success: true, device: deviceInfo}));
+ socket.close();
+ } else {
+ socket.send(JSON.stringify({error:"INVALID_TOTP", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ } else {
+ socket.send(JSON.stringify({error:"INVALID_TOTP", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ }
+ } else {
+ socket.send(JSON.stringify({error:"USER_NOT_FOUND", success: false, device: null}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+} \ No newline at end of file
diff --git a/server/hornchat.conversation.connect.js b/server/hornchat.conversation.connect.js
new file mode 100644
index 0000000..f608ff1
--- /dev/null
+++ b/server/hornchat.conversation.connect.js
@@ -0,0 +1,55 @@
+const fs = require("fs");
+
+module.exports = (socket) => {
+ let messagesJSON = JSON.parse(fs.readFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json").toString());
+
+ let validKeys = [];
+ let validDevices = userCredentials.filter((i) => {
+ return i.id === data[socket.id].peer;
+ })[0].devices.map((i) => { return i.id; });
+ for (let keyId of Object.keys(keys[data[socket.id].peer])) {
+ if (validDevices.includes(keyId)) validKeys.push(keyId);
+ }
+
+ let otherPeerSockets = Object.keys(data).filter((i) => {
+ let user = data[i];
+ return !!(user && user.conversation === data[socket.id].conversation && user.peer === socket.authenticated.user);
+ }).map((i) => {
+ return data[i].socket;
+ })
+
+ for (let id of Object.keys(messagesJSON)) {
+ let message = messagesJSON[id];
+
+ if (message.data.recipients.includes(socket.authenticated.device)) {
+ message.data.status = 1;
+ message._callback = false;
+ message.data.recipients = message.data.recipients.filter((i) => {
+ return i !== socket.authenticated.device;
+ })
+ socket.send(JSON.stringify(message));
+
+ let update = {
+ type: "status_update",
+ data: {
+ status: message.data.status,
+ recipients: validKeys,
+ message: message.data.uuid
+ }
+ }
+
+ for (let s of otherPeerSockets) {
+ s.send(JSON.stringify(update))
+ update.data.recipients = update.data.recipients.filter((i) => {
+ return i !== s.authenticated.device;
+ })
+ }
+
+ messagesJSON[require('uuid-v4')()] = update;
+ }
+
+ messagesJSON[id] = message;
+ }
+
+ fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json", JSON.stringify(messagesJSON));
+} \ No newline at end of file
diff --git a/server/hornchat.conversation.js b/server/hornchat.conversation.js
new file mode 100644
index 0000000..99baccd
--- /dev/null
+++ b/server/hornchat.conversation.js
@@ -0,0 +1,74 @@
+require('./hornchat.serverlet.sync')
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+const _auth = require('./hornchat.serverlet.authentication');
+
+const server = new WebSocket.Server({
+ port: 8306
+});
+
+global.data = {};
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.authenticated = null;
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+ data[socket.id]["socket"] = socket;
+
+ require('./hornchat.serverlet.timeout')(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function (msg) {
+ if (socket.authenticated === null) {
+ _auth(socket, msg);
+ } else {
+ let d;
+ try {
+ d = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: false}));
+ console.log("[" + socket.id + "] Received invalid data");
+ return;
+ }
+
+ if (d.type) {
+ switch (d.type) {
+ case "start":
+ require('./hornchat.conversation.start')(socket, d);
+ break;
+
+ case "typing":
+ require('./hornchat.conversation.typing')(socket, d);
+ break;
+
+ case "message":
+ require('./hornchat.conversation.message')(socket, d);
+ break;
+
+ case "read":
+ require('./hornchat.conversation.read')(socket, d);
+ break;
+
+ default:
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'type' value");
+ break;
+ }
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'type' value");
+ }
+ }
+ });
+});
+
+console.log("Listening on port 8306"); \ No newline at end of file
diff --git a/server/hornchat.conversation.message.js b/server/hornchat.conversation.message.js
new file mode 100644
index 0000000..69ced04
--- /dev/null
+++ b/server/hornchat.conversation.message.js
@@ -0,0 +1,56 @@
+const fs = require("fs");
+
+module.exports = (socket, msg) => {
+ if (!msg.data) {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'data' value");
+ return;
+ }
+
+ if (!data[socket.id]) {
+ socket.send(JSON.stringify({error:"NOT_STARTED", fatal: false}));
+ console.log("[" + socket.id + "] Conversation not started");
+ return;
+ }
+
+ let otherPeerSockets = Object.keys(data).filter((i) => {
+ let user = data[i];
+ return !!(user && user.conversation === data[socket.id].conversation && user.peer === socket.authenticated.user);
+ }).map((i) => {
+ return data[i].socket;
+ })
+
+ let conversationJSON = JSON.parse(fs.readFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/conversation.json").toString());
+ let messagesJSON = JSON.parse(fs.readFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json").toString());
+
+ msg.data.uuid = require('uuid-v4')();
+ msg.data.id = conversationJSON["counter"] + 1;
+ msg.data.date = new Date().toISOString();
+
+ let validKeys = [];
+ let validDevices = userCredentials.filter((i) => {
+ return i.id === data[socket.id].peer;
+ })[0].devices.map((i) => { return i.id; });
+ for (let keyId of Object.keys(keys[data[socket.id].peer])) {
+ if (validDevices.includes(keyId)) validKeys.push(keyId);
+ }
+
+ msg.data.recipients = validKeys;
+
+ for (let s of otherPeerSockets) {
+ msg.data.status = 1;
+ s.send(JSON.stringify(msg))
+ msg.data.recipients = msg.data.recipients.filter((i) => {
+ return i !== s.authenticated.device;
+ })
+ }
+
+ let msg2 = msg;
+ msg2["_callback"] = true;
+ socket.send(JSON.stringify(msg2));
+
+ messagesJSON[msg.data.uuid] = msg;
+ conversationJSON.counter++;
+ fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json", JSON.stringify(messagesJSON));
+ fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/conversation.json", JSON.stringify(conversationJSON));
+} \ No newline at end of file
diff --git a/server/hornchat.conversation.read.js b/server/hornchat.conversation.read.js
new file mode 100644
index 0000000..b121a2d
--- /dev/null
+++ b/server/hornchat.conversation.read.js
@@ -0,0 +1,52 @@
+const fs = require("fs");
+
+module.exports = (socket, msg) => {
+ let messagesJSON = JSON.parse(fs.readFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json").toString());
+
+ let validKeys = [];
+ let validDevices = userCredentials.filter((i) => {
+ return i.id === data[socket.id].peer;
+ })[0].devices.map((i) => { return i.id; });
+ for (let keyId of Object.keys(keys[data[socket.id].peer])) {
+ if (validDevices.includes(keyId)) validKeys.push(keyId);
+ }
+
+ let otherPeerSockets = Object.keys(data).filter((i) => {
+ let user = data[i];
+ return !!(user && user.conversation === data[socket.id].conversation && user.peer === socket.authenticated.user);
+ }).map((i) => {
+ return data[i].socket;
+ })
+
+ for (let id of Object.keys(messagesJSON)) {
+ let message = messagesJSON[id];
+
+ if (message.data.text && Object.keys(message.data.text).includes(socket.authenticated.device) && message.data.status !== 2) {
+ message.data.status = 2;
+ message._callback = false;
+
+ let update = {
+ type: "status_update",
+ data: {
+ status: message.data.status,
+ recipients: validKeys,
+ message: message.data.uuid
+ }
+ }
+
+ for (let s of otherPeerSockets) {
+ s.send(JSON.stringify(update))
+ update.data.recipients = update.data.recipients.filter((i) => {
+ return i !== s.authenticated.device;
+ })
+ }
+
+ messagesJSON[require('uuid-v4')()] = update;
+ }
+
+ messagesJSON[id] = message;
+ }
+
+ socket.send(JSON.stringify({success: true}));
+ fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json", JSON.stringify(messagesJSON));
+} \ No newline at end of file
diff --git a/server/hornchat.conversation.start.js b/server/hornchat.conversation.start.js
new file mode 100644
index 0000000..4c9e3cd
--- /dev/null
+++ b/server/hornchat.conversation.start.js
@@ -0,0 +1,38 @@
+const fs = require("fs");
+
+function generateConversationID(user1, user2) {
+ let user1hash = require('crypto').createHash("sha256").update(user1).digest("hex").split("")
+ let user2hash = require('crypto').createHash("sha256").update(user2).digest("hex").split("")
+ let characters = [...user1hash, ...user2hash].sort();
+
+ return require('crypto').createHash("sha256").update(characters.join("")).digest("hex");
+}
+
+module.exports = (socket, msg) => {
+ if (!msg.username) {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ return;
+ }
+
+ if (!keys[msg.username]) {
+ socket.send(JSON.stringify({error:"INVALID_USER", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'username' value");
+ return;
+ }
+
+ data[socket.id] = {
+ peer: msg.username,
+ conversation: generateConversationID(msg.username, socket.authenticated.user),
+ socket
+ }
+
+ if (!fs.existsSync(dataPath + "/conversations/" + data[socket.id].conversation)) fs.mkdirSync(dataPath + "/conversations/" + data[socket.id].conversation);
+ if (!fs.existsSync(dataPath + "/conversations/" + data[socket.id].conversation + "/conversation.json")) fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/conversation.json", "{\n \"counter\": 0\n}");
+ if (!fs.existsSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json")) fs.writeFileSync(dataPath + "/conversations/" + data[socket.id].conversation + "/messages.json", "{}");
+ console.log("[" + socket.id + "] Conversation " + data[socket.id].conversation + " started");
+
+ socket.send(JSON.stringify({error: null, success: true, started: true}));
+
+ require('./hornchat.conversation.connect')(socket);
+} \ No newline at end of file
diff --git a/server/hornchat.conversation.typing.js b/server/hornchat.conversation.typing.js
new file mode 100644
index 0000000..d3c5755
--- /dev/null
+++ b/server/hornchat.conversation.typing.js
@@ -0,0 +1,34 @@
+const fs = require("fs");
+
+module.exports = (socket, msg) => {
+ if (!msg.text) {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ return;
+ }
+
+ if (!data[socket.id]) {
+ socket.send(JSON.stringify({error:"NOT_STARTED", fatal: false}));
+ console.log("[" + socket.id + "] Conversation not started");
+ return;
+ }
+
+ let otherPeerSockets = Object.keys(data).filter((i) => {
+ let user = data[i];
+ return !!(user && user.conversation === data[socket.id].conversation && user.peer === socket.authenticated.user);
+ }).map((i) => {
+ return data[i].socket;
+ })
+
+ for (let s of otherPeerSockets) {
+ s.send(JSON.stringify({
+ type: "typing",
+ text: msg.text,
+ reply: msg.reply,
+ attachments: parseInt(msg.attachments),
+ position: msg.position
+ }))
+ }
+
+ socket.send(JSON.stringify({success: true}));
+} \ No newline at end of file
diff --git a/server/hornchat.keyserver.disconnect.js b/server/hornchat.keyserver.disconnect.js
new file mode 100644
index 0000000..b1d1b35
--- /dev/null
+++ b/server/hornchat.keyserver.disconnect.js
@@ -0,0 +1,46 @@
+const fs = require("fs");
+const _list = require('./hornchat.keyserver.list');
+
+module.exports = (socket, data) => {
+ console.log("[" + socket.id + "] Disconnect method");
+
+ if (!data.device) {
+ socket.send(JSON.stringify({error: "MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'device' value");
+ return;
+ }
+
+ let candidateDevices = userCredentials.filter((i) => {
+ return i.id === socket.authenticated.user;
+ })[0].devices.filter((i) => {
+ return i.id === data.device;
+ });
+
+ if (candidateDevices.length !== 1) {
+ socket.send(JSON.stringify({error:"INVALID_DEVICE", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'device' value");
+ return;
+ }
+
+ global.userCredentials = userCredentials.map((i) => {
+ if (i.id === socket.authenticated.user) {
+ i.devices = i.devices.map((j) => {
+ if (j.id === data.device) {
+ return null;
+ } else {
+ return j;
+ }
+ }).filter((j) => {
+ return j !== null;
+ })
+
+ return i;
+ } else {
+ return i;
+ }
+ })
+ fs.writeFileSync(dataPath + "/users.json", JSON.stringify(userCredentials, null, 2));
+ fs.writeFileSync(dataPath + "/keys.json", JSON.stringify(keys, null, 2));
+
+ _list(socket, data);
+} \ No newline at end of file
diff --git a/server/hornchat.keyserver.js b/server/hornchat.keyserver.js
new file mode 100644
index 0000000..4b7bdb5
--- /dev/null
+++ b/server/hornchat.keyserver.js
@@ -0,0 +1,79 @@
+require('./hornchat.serverlet.sync');
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+const fs = require("fs");
+
+const _auth = require("./hornchat.serverlet.authentication");
+const _disconnect = require("./hornchat.keyserver.disconnect");
+const _list = require("./hornchat.keyserver.list");
+const _write = require("./hornchat.keyserver.write");
+const _read = require("./hornchat.keyserver.read");
+
+const server = new WebSocket.Server({
+ port: 8302
+});
+
+global.data = {};
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.authenticated = null;
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+
+ require("./hornchat.serverlet.timeout")(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function (msg) {
+ if (socket.authenticated === null) {
+ _auth(socket, msg);
+ } else {
+ let data;
+ try {
+ data = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: false}));
+ console.log("[" + socket.id + "] Received invalid data");
+ return;
+ }
+
+ if (data.type) {
+ switch (data.type) {
+ case "disconnect":
+ _disconnect(socket, data);
+ break;
+
+ case "list":
+ _list(socket, data);
+ break;
+
+ case "write":
+ _write(socket, data);
+ break;
+
+ case "read":
+ _read(socket, data);
+ break;
+
+ default:
+ socket.send(JSON.stringify({error:"INVALID_TYPE", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'type' value");
+ break;
+ }
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'type' value");
+ }
+ }
+ });
+});
+
+console.log("Listening on port 8302"); \ No newline at end of file
diff --git a/server/hornchat.keyserver.list.js b/server/hornchat.keyserver.list.js
new file mode 100644
index 0000000..8845e98
--- /dev/null
+++ b/server/hornchat.keyserver.list.js
@@ -0,0 +1,24 @@
+module.exports = (socket, data) => {
+ let devices = [];
+
+ for (let device of userCredentials.filter((i) => {
+ return i.id === socket.authenticated.user;
+ })[0].devices) {
+ devices.push({
+ id: device.id,
+ platform: device.platform.userAgent.family + " on " + device.platform.os.family + " on " + device.platform.device.family,
+ userAgent: device.platform.string,
+ addresses: device.addresses,
+ // If you do it the other way around, everything breaks.
+ // I have no idea why this happens, but it does.
+ // Please don't un-fix this code.
+ // - Mossy Storm, Raindrops System
+ dates: {
+ last: device.firstSeen ?? null,
+ first: device.lastSeen ?? null
+ }
+ })
+ }
+ socket.send(JSON.stringify({error:null, success:true, fatal: false, devices}));
+ console.log("[" + socket.id + "] Gathered device list");
+} \ No newline at end of file
diff --git a/server/hornchat.keyserver.read.js b/server/hornchat.keyserver.read.js
new file mode 100644
index 0000000..206511a
--- /dev/null
+++ b/server/hornchat.keyserver.read.js
@@ -0,0 +1,25 @@
+module.exports = (socket, data) => {
+ if (!data.username) {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ return;
+ }
+
+ if (!keys[data.username]) {
+ socket.send(JSON.stringify({error:"INVALID_USER", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'username' value");
+ return;
+ }
+
+ let validKeys = {};
+ let validDevices = userCredentials.filter((i) => {
+ return i.id === data.username;
+ })[0].devices.map((i) => { return i.id; });
+
+ for (let keyId of Object.keys(keys[data.username])) {
+ if (validDevices.includes(keyId)) validKeys[keyId] = keys[data.username][keyId];
+ }
+
+ socket.send(JSON.stringify({error:null, success:true, fatal: false, keys: validKeys}));
+ console.log("[" + socket.id + "] Gathered public keys for " + data.username);
+} \ No newline at end of file
diff --git a/server/hornchat.keyserver.write.js b/server/hornchat.keyserver.write.js
new file mode 100644
index 0000000..938d177
--- /dev/null
+++ b/server/hornchat.keyserver.write.js
@@ -0,0 +1,16 @@
+const fs = require("fs");
+
+module.exports = (socket, data) => {
+ if (!data.publicKey) {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'publicKey' value");
+ return;
+ }
+
+ if (!keys[socket.authenticated.user]) keys[socket.authenticated.user] = {};
+ keys[socket.authenticated.user][socket.authenticated.device] = data.publicKey;
+ fs.writeFileSync(dataPath + "/keys.json", JSON.stringify(keys, null, 2));
+
+ socket.send(JSON.stringify({error:null, success:true, fatal: false}));
+ console.log("[" + socket.id + "] Updated public key");
+} \ No newline at end of file
diff --git a/server/hornchat.presence.js b/server/hornchat.presence.js
new file mode 100644
index 0000000..82cf868
--- /dev/null
+++ b/server/hornchat.presence.js
@@ -0,0 +1,69 @@
+require("./hornchat.serverlet.sync");
+
+global.connectedDevices = {};
+
+const informTrackedUsers = require('./hornchat.presence.tracking');
+setInterval(() => {
+ informTrackedUsers();
+}, 50)
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+const _auth = require("./hornchat.serverlet.authentication");
+const _process = require('./hornchat.presence.process')
+
+const server = new WebSocket.Server({
+ port: 8305
+});
+
+global.data = {};
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.authenticated = null;
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+ data[socket.id]["socket"] = socket;
+
+ require("./hornchat.serverlet.timeout")(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ if (socket.authenticated !== null) {
+ if (!connectedDevices[socket.authenticated.user]) connectedDevices[socket.authenticated.user] = [];
+
+ connectedDevices[socket.authenticated.user] = connectedDevices[socket.authenticated.user].filter((i) => {
+ return i !== socket.authenticated.device;
+ })
+ }
+
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function (msg) {
+ if (socket.authenticated === null) {
+ _auth(socket, msg, true);
+ } else {
+ let d;
+ try {
+ d = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: false}));
+ console.log("[" + socket.id + "] Received invalid data");
+ return;
+ }
+
+ if (d.username) {
+ _process(socket, d);
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ }
+ }
+ });
+});
+
+console.log("Listening on port 8305"); \ No newline at end of file
diff --git a/server/hornchat.presence.process.js b/server/hornchat.presence.process.js
new file mode 100644
index 0000000..a54b0a2
--- /dev/null
+++ b/server/hornchat.presence.process.js
@@ -0,0 +1,11 @@
+module.exports = (socket, d) => {
+ if (!connectedDevices[d.username]) connectedDevices[d.username] = [];
+ let dev = connectedDevices[d.username];
+
+ if (!data[socket.id]["trackedUsers"]) data[socket.id]["trackedUsers"] = [];
+ if (!data[socket.id]["lastKnownTrackedUserInfo"]) data[socket.id]["lastKnownTrackedUserInfo"] = {};
+ data[socket.id]["trackedUsers"].push(d.username);
+ data[socket.id]["lastKnownTrackedUserInfo"][d.username] = JSON.stringify(dev);
+
+ socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: true, data: dev, username: d.username}));
+} \ No newline at end of file
diff --git a/server/hornchat.presence.tracking.js b/server/hornchat.presence.tracking.js
new file mode 100644
index 0000000..f8f80b9
--- /dev/null
+++ b/server/hornchat.presence.tracking.js
@@ -0,0 +1,18 @@
+module.exports = () => {
+ for (let id of Object.keys(data)) {
+ let connection = data[id];
+
+ if (connection.socket.authenticated === null) continue;
+
+ if (typeof connection.trackedUsers === "object" && connection.trackedUsers instanceof Array) {
+ for (let user of connection.trackedUsers) {
+ let dev = connectedDevices[user];
+
+ if (JSON.stringify(dev) !== connection.lastKnownTrackedUserInfo[user]) {
+ connection.lastKnownTrackedUserInfo[user] = JSON.stringify(dev);
+ connection.socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: false, data: dev, username: user}));
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/server/hornchat.profile.js b/server/hornchat.profile.js
new file mode 100644
index 0000000..9b30cf1
--- /dev/null
+++ b/server/hornchat.profile.js
@@ -0,0 +1,59 @@
+require('./hornchat.serverlet.sync')
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+const _auth = require('./hornchat.serverlet.authentication');
+const _process = require('./hornchat.profile.process');
+
+const informTrackedUsers = require('./hornchat.profile.tracking');
+setInterval(() => {
+ informTrackedUsers();
+}, 50)
+
+const server = new WebSocket.Server({
+ port: 8303
+});
+
+global.data = {};
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.authenticated = null;
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+ data[socket.id]["socket"] = socket;
+
+ require('./hornchat.serverlet.timeout')(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function (msg) {
+ if (socket.authenticated === null) {
+ _auth(socket, msg);
+ } else {
+ let d;
+ try {
+ d = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: false}));
+ console.log("[" + socket.id + "] Received invalid data");
+ return;
+ }
+
+ if (d.username) {
+ _process(socket, d);
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ }
+ }
+ });
+});
+
+console.log("Listening on port 8303"); \ No newline at end of file
diff --git a/server/hornchat.profile.process.js b/server/hornchat.profile.process.js
new file mode 100644
index 0000000..3769c4e
--- /dev/null
+++ b/server/hornchat.profile.process.js
@@ -0,0 +1,17 @@
+module.exports = (socket, d) => {
+ if (!fronters[d.username] || !pluralkit[d.username]) {
+ socket.send(JSON.stringify({error:"INVALID_USER", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'username' value");
+ return;
+ }
+
+ let pk = pluralkit[d.username];
+ pk['fronters'] = fronters[d.username];
+
+ if (!data[socket.id]["trackedUsers"]) data[socket.id]["trackedUsers"] = [];
+ if (!data[socket.id]["lastKnownTrackedUserInfo"]) data[socket.id]["lastKnownTrackedUserInfo"] = {};
+ data[socket.id]["trackedUsers"].push(d.username);
+ data[socket.id]["lastKnownTrackedUserInfo"][d.username] = JSON.stringify(pk);
+
+ socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: true, data: pk, username: d.username}));
+}
diff --git a/server/hornchat.profile.tracking.js b/server/hornchat.profile.tracking.js
new file mode 100644
index 0000000..17143e5
--- /dev/null
+++ b/server/hornchat.profile.tracking.js
@@ -0,0 +1,19 @@
+module.exports = () => {
+ for (let id of Object.keys(data)) {
+ let connection = data[id];
+
+ if (connection.socket.authenticated === null) continue;
+
+ if (typeof connection.trackedUsers === "object" && connection.trackedUsers instanceof Array) {
+ for (let user of connection.trackedUsers) {
+ let pk = pluralkit[user];
+ pk['fronters'] = fronters[user].fronters;
+
+ if (JSON.stringify(pk) !== connection.lastKnownTrackedUserInfo[user]) {
+ connection.lastKnownTrackedUserInfo[user] = JSON.stringify(pk);
+ connection.socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: false, data: pk, username: user}));
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/server/hornchat.serverlet.authentication.js b/server/hornchat.serverlet.authentication.js
new file mode 100644
index 0000000..37fb58b
--- /dev/null
+++ b/server/hornchat.serverlet.authentication.js
@@ -0,0 +1,80 @@
+const fs = require("fs");
+global.rateLimits = {};
+
+module.exports = (socket, msg, updateConnectedDevices) => {
+ if (!updateConnectedDevices) updateConnectedDevices = false;
+
+ let data;
+ try {
+ data = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: true}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ return;
+ }
+
+ try {
+ if (data.username && data.token) {
+ console.log("[" + socket.id + "] Username:", data.username, "Token:", "<redacted>");
+ let currentDevice = null;
+
+ if (userCredentials.filter((i) => i.id === data.username).length > 0) {
+ if (userCredentials.filter((i) => i.id === data.username)[0]['devices'].length > 0) {
+ for (let device of userCredentials.filter((i) => i.id === data.username)[0]['devices']) {
+ if (data.token === device.token) {
+ currentDevice = device;
+ global.userCredentials = userCredentials.map((i) => {
+ if (i.id === data.username) {
+ i.devices = i.devices.map((j) => {
+ if (data.token === j.token) {
+ j.addresses = [...new Set([...j.addresses, socket.ip])];
+ j.lastSeen = new Date();
+ }
+
+ return j;
+ })
+ }
+
+ return i;
+ })
+
+ fs.writeFileSync(dataPath + "/users.json", JSON.stringify(userCredentials, null, 2));
+ socket.send(JSON.stringify({device: device.id}));
+ console.log("[" + socket.id + "] Authenticated successfully");
+
+ if (updateConnectedDevices) {
+ if (!connectedDevices[data.username]) connectedDevices[data.username] = [];
+ connectedDevices[data.username].push(device.id);
+ }
+
+ socket.authenticated = {
+ device: device.id,
+ user: data.username
+ }
+
+ break;
+ }
+ }
+ }
+ } else {
+ socket.send(JSON.stringify({error:"USER_NOT_FOUND", fatal: true}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: true}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ } catch (e) {
+ console.error(e);
+ socket.send(JSON.stringify({error:"INTERNAL_ERROR", fatal: true}));
+ console.log("[" + socket.id + "] Unable to authenticate");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+} \ No newline at end of file
diff --git a/server/hornchat.serverlet.sync.js b/server/hornchat.serverlet.sync.js
new file mode 100644
index 0000000..1398991
--- /dev/null
+++ b/server/hornchat.serverlet.sync.js
@@ -0,0 +1,22 @@
+const fs = require('fs');
+
+global.dataPath = require('path').resolve("../data");
+global.userCredentials = require(dataPath + "/users.json");
+global.keys = require(dataPath + "/keys.json");
+global.pluralkit = require(dataPath + "/pluralkit.json");
+global.fronters = require(dataPath + "/fronters.json");
+
+setInterval(() => {
+ try {
+ global.userCredentials = JSON.parse(fs.readFileSync(dataPath + "/users.json").toString());
+ } catch (e) {}
+ try {
+ global.keys = JSON.parse(fs.readFileSync(dataPath + "/keys.json").toString());
+ } catch (e) {}
+ try {
+ global.pluralkit = JSON.parse(fs.readFileSync(dataPath + "/pluralkit.json").toString());
+ } catch (e) {}
+ try {
+ global.fronters = JSON.parse(fs.readFileSync(dataPath + "/fronters.json").toString());
+ } catch (e) {}
+}, 10) \ No newline at end of file
diff --git a/server/hornchat.serverlet.timeout.js b/server/hornchat.serverlet.timeout.js
new file mode 100644
index 0000000..13cbd5f
--- /dev/null
+++ b/server/hornchat.serverlet.timeout.js
@@ -0,0 +1,10 @@
+module.exports = (socket) => {
+ setTimeout(() => {
+ if (data[socket.id] && socket.authenticated === null) {
+ socket.send(JSON.stringify({error:"TIMED_OUT", success: false, device: null}));
+ console.log("[" + socket.id + "] Connection timed out");
+ rateLimits[socket.ip] = new Date();
+ socket.close();
+ }
+ }, 2000)
+} \ No newline at end of file
diff --git a/server/hornchat.verification.js b/server/hornchat.verification.js
new file mode 100644
index 0000000..1d94ab9
--- /dev/null
+++ b/server/hornchat.verification.js
@@ -0,0 +1,65 @@
+require('./hornchat.serverlet.sync');
+
+const informTrackedUsers = require('./hornchat.verification.tracking');
+setInterval(() => {
+ informTrackedUsers();
+}, 50)
+
+global.existingUsers = Object.keys(userCredentials).map((i) => {
+ let item = userCredentials[i];
+ return item["id"];
+});
+
+const WebSocket = require('ws');
+const uuid = require('uuid-v4');
+const fs = require("fs");
+const _process = require('./hornchat.verification.process')
+const _auth = require('./hornchat.serverlet.authentication')
+
+const server = new WebSocket.Server({
+ port: 8304
+});
+
+global.data = {};
+
+server.on('connection', function (socket, req) {
+ socket.ip = req.headers['x-forwarded-for'] ? req.headers['x-forwarded-for'].split(',')[0].trim() : req.socket.remoteAddress;
+
+ socket.authenticated = null;
+ socket.id = uuid();
+ console.log("New connection: " + socket.id);
+ data[socket.id] = {};
+ data[socket.id]["socket"] = socket;
+
+ require('./hornchat.serverlet.timeout')(socket);
+
+ socket.on('close', () => {
+ if (socket.id) {
+ delete data[socket.id];
+ }
+ })
+
+ socket.on('message', function (msg) {
+ if (socket.authenticated === null) {
+ _auth(socket, msg);
+ } else {
+ let d;
+ try {
+ d = JSON.parse(msg);
+ } catch (e) {
+ socket.send(JSON.stringify({error:"INVALID_DATA", fatal: false}));
+ console.log("[" + socket.id + "] Received invalid data");
+ return;
+ }
+
+ if (d.username) {
+ _process(socket, d);
+ } else {
+ socket.send(JSON.stringify({error:"MISSING_OPERAND", fatal: false}));
+ console.log("[" + socket.id + "] Missing 'username' value");
+ }
+ }
+ });
+});
+
+console.log("Listening on port 8304"); \ No newline at end of file
diff --git a/server/hornchat.verification.process.js b/server/hornchat.verification.process.js
new file mode 100644
index 0000000..8d93aa2
--- /dev/null
+++ b/server/hornchat.verification.process.js
@@ -0,0 +1,18 @@
+const calculateSafetyNumber = require('./hornchat.verification.safety');
+
+module.exports = (socket, d) => {
+ if (!existingUsers.includes(d.username) || !keys[d.username]) {
+ socket.send(JSON.stringify({error:"INVALID_USER", fatal: false}));
+ console.log("[" + socket.id + "] Invalid 'username' value");
+ return;
+ }
+
+ let safetyNumber = calculateSafetyNumber(d.username);
+
+ if (!data[socket.id]["trackedUsers"]) data[socket.id]["trackedUsers"] = [];
+ if (!data[socket.id]["lastKnownTrackedUserInfo"]) data[socket.id]["lastKnownTrackedUserInfo"] = {};
+ data[socket.id]["trackedUsers"].push(d.username);
+ data[socket.id]["lastKnownTrackedUserInfo"][d.username] = JSON.stringify(safetyNumber);
+
+ socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: true, data: safetyNumber, username: d.username}));
+} \ No newline at end of file
diff --git a/server/hornchat.verification.safety.js b/server/hornchat.verification.safety.js
new file mode 100644
index 0000000..59ba8bd
--- /dev/null
+++ b/server/hornchat.verification.safety.js
@@ -0,0 +1,14 @@
+module.exports = (username) => {
+ let hex = require('crypto').createHash("sha256").update(Object.keys(keys[username]).map((i) => {
+ return keys[username][i];
+ }).map((i) => {
+ return JSON.stringify(i);
+ }).join("|")).digest("hex");
+
+ return {
+ raw: hex,
+ user: BigInt("0x" + hex).toString().substring(0, 60),
+ parts: BigInt("0x" + hex).toString().substring(0, 60).match(/.{1,5}/g),
+ colors: hex.substring(0, 36).match(/.{1,6}/g)
+ };
+} \ No newline at end of file
diff --git a/server/hornchat.verification.tracking.js b/server/hornchat.verification.tracking.js
new file mode 100644
index 0000000..bf8f7e3
--- /dev/null
+++ b/server/hornchat.verification.tracking.js
@@ -0,0 +1,20 @@
+const calculateSafetyNumber = require('./hornchat.verification.safety');
+
+module.exports = () => {
+ for (let id of Object.keys(data)) {
+ let connection = data[id];
+
+ if (connection.socket.authenticated === null) continue;
+
+ if (typeof connection.trackedUsers === "object" && connection.trackedUsers instanceof Array) {
+ for (let user of connection.trackedUsers) {
+ let safetyNumber = calculateSafetyNumber(user);
+
+ if (JSON.stringify(safetyNumber) !== connection.lastKnownTrackedUserInfo[user]) {
+ connection.lastKnownTrackedUserInfo[user] = JSON.stringify(safetyNumber);
+ connection.socket.send(JSON.stringify({error:null, success:true, fatal: false, manual: false, data: safetyNumber, username: user}));
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/server/index.js b/server/index.js
new file mode 100644
index 0000000..b0bfd42
--- /dev/null
+++ b/server/index.js
@@ -0,0 +1,39 @@
+const cp = require('child_process');
+let processes = {};
+
+let serverlets = [
+ "authentication",
+ "keyserver",
+ "profile",
+ "verification",
+ "presence",
+ "conversation"
+]
+
+for (let serverlet of serverlets) {
+ startServerlet(serverlet);
+}
+
+function startServerlet(serverlet) {
+ console.log("Starting " + serverlet + "...");
+ processes[serverlet] = cp.spawn("node", [ "hornchat." + serverlet + ".js" ], { stdio: "pipe" });
+ console.log("[" + serverlet + "] (PID: " + processes[serverlet].pid + ")")
+
+ processes[serverlet].stdout.on('data', (data) => {
+ let lines = data.toString().trim().split("\n");
+ for (let line of lines) {
+ console.log("[" + serverlet + "] " + line);
+ }
+ })
+
+ processes[serverlet].stderr.on('data', (data) => {
+ let lines = data.toString().trim().split("\n");
+ for (let line of lines) {
+ console.error("[" + serverlet + "] " + line);
+ }
+ })
+
+ processes[serverlet].on('close', () => {
+ startServerlet(serverlet);
+ })
+} \ No newline at end of file