diff options
Diffstat (limited to 'server')
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 |