diff --git a/client/app.js b/client/app.js index 9bba7b3..a7fa04b 100644 --- a/client/app.js +++ b/client/app.js @@ -115,7 +115,7 @@ function newThread() { document.getElementById('newthread').textContent = 'create'; } else { window.threadmembers = [window.name]; - document.getElementById('threads').insertAdjacentElement('afterend', html.node` + document.getElementById('home').insertAdjacentElement('afterend', html.node`

create thread

@@ -147,8 +147,7 @@ function newThread() {
- ` - ); + `); document.getElementById('newthread').textContent = 'cancel'; } } @@ -159,11 +158,20 @@ function clickedTab(event) { } render(document.body, html` -
-

vybe

-

threads

-
loading...
- +
+
+

vybe

+

threads

+
loading...
+ +
+
+ document.getElementById('profile').classList.toggle('hidden') + }>${window.name}
+
+
@@ -171,7 +179,8 @@ render(document.body, html` thread: meow
- +
@@ -200,3 +209,16 @@ window.emit('list_threads', {}, msg => { threadlist.appendChild(makeThread(thread)); chooseThread.call(threadlist.firstChild); }); + +window.socket.on('authrequest', (msg, respond) => { + const div = html.node` +
+ + session ${msg.id} +
`; + document.getElementById('authrequests').append(div); + setTimeout(() => div.remove(), Date.now() - msg.time + 60000 * 5); +}); diff --git a/client/auth.js b/client/auth.js index b6b027a..5bd4ce9 100644 --- a/client/auth.js +++ b/client/auth.js @@ -18,7 +18,11 @@ async function auth() { }); window.socket.emit( 'authenticate', - { name: window.name, message: sig }, + { + name: window.name, + message: sig, + pubkey: window.keys.armored.publicKey + }, msg => { let register = document.getElementById('register'); if (!msg.success) { @@ -26,14 +30,46 @@ async function auth() { register.classList.remove('hidden'); return; } - if (register) { - register.remove(); - import('/app.js'); - } + localStorage.setItem('keys', JSON.stringify(window.keys.armored)); + localStorage.setItem('name', window.name); + register.classList.add('hidden'); + import('/app.js'); } ); } +async function submit(event) { + event.preventDefault(); + const name = document.getElementById('name').value; + if (!name) + return; + const keys = await openpgp.generateKey({ + userIDs: [{ name }] + }); + const priv = await openpgp.readKey({ armoredKey: keys.privateKey }); + const pub = await openpgp.readKey({ armoredKey: keys.publicKey }); + window.keys = { priv, pub, armored: keys }; + window.name = name; + if (this.id === 'registerform') { + window.emit('create_user', + { name, pubkey: keys.publicKey }, + (msg) => { + if (!msg.success) { + document.getElementById('result').innerText = msg.message; + return; + } + auth(); + } + ); + } + else { + await auth(); + document.getElementById('result').innerHTML = + `now open your profile on your other device to approve this authentication. + this session's ID is ${pub.getFingerprint().slice(0,8)}`; + } +} + render(document.body, html` `); -async function gensession() { +function gensession() { window.session = rand(); window.emit = (type, data, callback) => @@ -94,7 +110,7 @@ async function gensession() { window.onload = async () => { window.socket = io(); - await gensession(); + gensession(); let keys = localStorage.getItem('keys'); if (keys) { @@ -102,7 +118,8 @@ window.onload = async () => { keys = JSON.parse(keys); window.keys = { priv: await openpgp.readKey({ armoredKey: keys.privateKey }), - pub: await openpgp.readKey({ armoredKey: keys.publicKey }) + pub: await openpgp.readKey({ armoredKey: keys.publicKey }), + armored: keys }; await auth(); } @@ -110,8 +127,8 @@ window.onload = async () => { document.getElementById('register').classList.remove('hidden'); window.socket.io.on('reconnect', async attempt => { - await gensession(); - if (localStorage.getItem('keys')) + gensession(); + if (window.keys) await auth(); }); }; diff --git a/client/index.html b/client/index.html index 7aa67e5..4d279c5 100644 --- a/client/index.html +++ b/client/index.html @@ -54,20 +54,21 @@ background: #1b1b1b; outline: none; border: 1px solid #444; - } - input:focus { - padding-bottom: 3px; - border-bottom: 3px solid #777; - } - input::placeholder { - color: #aaa; + &:focus { + padding-bottom: 3px; + border-bottom: 3px solid #777; + } + &::placeholder { + color: #aaa; + } } #register { margin-inline: 14px; max-width: 800px; } .thread:hover, - .tab:hover { + .tab:hover, + #user:hover { background-color: #303030; } .tab.active, @@ -90,13 +91,32 @@ .column { flex: 1; overflow: hidden; - margin: 5px; } .separator { margin: 8px 2px; } - #threads { + #home { max-width: 250px; + display: flex; + flex-direction: column; + justify-content: space-between; + } + #threads { + margin: 5px; + } + #user { + margin: 3px; + padding: 6px; + background-color: #191919; + } + #profile { + max-width: 250px; + > * { + margin: 5px; + } + } + .authrequest { + margin-block: 3px; } .thread { padding: 2px 4px; @@ -113,7 +133,6 @@ #thread { display: flex; flex-direction: column; - margin: 0; } #title { margin: 4px; @@ -143,14 +162,12 @@ #msginput { display: flex; flex-direction: row; - margin: 2px; + > * { + margin: 3px; + } } #msg { flex-grow: 1; - margin: 2px; - } - #sendmsg { - margin: 2px 3px; } .message { margin-bottom: 5px; diff --git a/db/1-init.sql b/db/1-init.sql index 1832aa3..1f30536 100644 --- a/db/1-init.sql +++ b/db/1-init.sql @@ -1,14 +1,14 @@ create table user ( id integer primary key asc, name text, - pubkey text, created timestamp default current_timestamp ); -create table authentication ( +create table key ( user integer, - salt text, + pubkey text, created timestamp default current_timestamp, + active boolean, foreign key(user) references user(id) ); @@ -34,7 +34,6 @@ create table permission ( create table member ( thread integer, user integer, - key_delivery text, created timestamp default current_timestamp, foreign key(user) references user(id), foreign key(thread) references thread(id) diff --git a/index.js b/index.js index d40433e..7324e2c 100644 --- a/index.js +++ b/index.js @@ -20,21 +20,27 @@ const io = new Server(server, { }, }); -io.cache = {}; - io.on('connection', (socket) => { for (let event in events) { - socket.on(event, (msg, callback) => - events[event](msg, callback, socket, io) - ); + socket.on(event, (msg, callback) => { + if (!events[event]) { + callback('no such event ' + event); + return; + } + events[event](msg, callback, socket); + }); } socket.on('disconnect', reason => { - let sockets = io.cache[socket.username]; - if (sockets) - sockets.splice(sockets.indexOf(socket.id), 1); + let user = vybe.users[socket.username]; + if (user) + user.sockets.splice(user.sockets.indexOf(socket), 1); }) }); +global.vybe = { + users: {} +}; + server.listen(PORT, () => { console.log('server running on port ' + PORT); }); diff --git a/src/authwrap.js b/src/authwrap.js index 6fa884b..66b2d55 100644 --- a/src/authwrap.js +++ b/src/authwrap.js @@ -1,27 +1,21 @@ const db = require('./db'); -const authwrap = (fn) => async (msg, respond, socket, io) => { +const authwrap = (fn) => async (msg, respond, socket) => { if (!respond) respond = () => {}; - if (!msg || !msg.__session) { + if (!msg || !msg.__session || socket.auth !== msg.__session) { return respond({ success: false, - message: 'not authenticated', + message: 'not authenticated' }); } - const result = await db.query( - `select user.* from user join authentication - on authentication.user = user.id - where authentication.salt = ?`, - [msg.__session] - ); - if (result.rows.length === 0) { - return respond({ - success: false, - message: 'user not found', - }); - } - return await fn({ ...msg, auth_user: result.rows[0] }, respond, socket, io); + return await fn({ + ...msg, + auth_user: { + id: vybe.users[socket.username].id, + name: socket.username + } + }, respond, socket); }; module.exports = authwrap; diff --git a/src/event/authenticate.js b/src/event/authenticate.js index 8b3710a..b46fc4f 100644 --- a/src/event/authenticate.js +++ b/src/event/authenticate.js @@ -1,27 +1,32 @@ const db = require('../db'); const openpgp = require('openpgp'); -const authenticate = async (msg, respond, socket, io) => { +const authenticate = async (msg, respond, socket) => { if (!msg.name || !msg.message) { return respond({ success: false, message: 'invalid message' }); } - const result = await db.query('select * from user where name = ?', [ - msg.name - ]); - if (result.rows.length === 0) { + let userid = await db.query( + `select user.id from user where name = ?`, [msg.name]); + if (userid.rows.length === 0) { return respond({ success: false, message: 'user not found' }); } + let result = await db.query( + `select user.id from user + join key on key.user = user.id + where name = ? and pubkey = ? and active = true`, + [msg.name, msg.pubkey] + ); try { - const key = await openpgp.readKey({ armoredKey: result.rows[0].pubkey }); + const key = await openpgp.readKey({ armoredKey: msg.pubkey }); const verification = await openpgp.verify({ message: await openpgp.readCleartextMessage({ - cleartextMessage: msg.message, + cleartextMessage: msg.message }), verificationKeys: key, expectSigned: true @@ -33,31 +38,52 @@ const authenticate = async (msg, respond, socket, io) => { message: 'bad auth message' }); } - const auths = await db.query( - 'select * from authentication where user = ? and salt = ?', - [result.rows[0].id, data[1]] - ); - if (auths.rows.length === 0) { - await db.query('insert into authentication (user, salt) values (?, ?)', [ - result.rows[0].id, - data[1] - ]); - socket.username = msg.name; - if (io.cache[msg.name]) { - io.cache[msg.name].push(socket.id); - } else { - io.cache[msg.name] = [socket.id]; - } - return respond({ - success: true, - }); - } else { - return respond({ - success: false, - message: 'already authenticated with this message' - }); + let user = vybe.users[msg.name]; + if (!user) + user = vybe.users[msg.name] = { + id: userid.rows[0].id, + sockets: [], + authrequests: {} + }; + if (result.rows.length === 0) { + // request auth from logged in sessions + let id = key.getFingerprint().slice(0, 8); + let time = Date.now(); + if (!await new Promise(resolve => { + user.authrequests[id] = { time, callback: resolve }; + for (let s of user.sockets) + s.emit('authrequest', { id, time }, resolve); + setTimeout(() => { + delete user.authrequests[id]; + resolve(false); + }, 60000 * 5); + })) + return; + delete user.authrequests[id]; + if (Date.now() - time > 60000 * 5) + return; + await db.query( + 'insert into key (user, pubkey, active) values (?, ?, true)', + [user.id, msg.pubkey]); } - } catch (err) { + // this socket is now authenticated + socket.auth = data[1]; + socket.username = msg.name; + user.sockets.push(socket); + respond({ + success: true + }); + // send authenticated session any auth requests + if (result.rows.length) + setTimeout(() => { + for (let id in user.authrequests) + socket.emit('authrequest', { + id, + time: user.authrequests[id].time + }, user.authrequests[id].callback); + }, 1000); // todo: do this better + } + catch (err) { console.error('error in authentication: ' + err); return respond({ success: false, diff --git a/src/event/create_thread.js b/src/event/create_thread.js index 4403d96..67bac8f 100644 --- a/src/event/create_thread.js +++ b/src/event/create_thread.js @@ -1,18 +1,18 @@ const db = require('../db'); const authwrap = require('../authwrap'); -const create_thread = async (msg, respond, socket, io) => { +const create_thread = async (msg, respond, socket) => { // validate inputs if (typeof msg.name !== 'string') { return respond({ success: false, - message: 'thread name required', + message: 'thread name required' }); } if (msg.name.length > 200) { return respond({ success: false, - message: 'thread name 200 chars max', + message: 'thread name 200 chars max' }); } // add to db @@ -63,38 +63,37 @@ const create_thread = async (msg, respond, socket, io) => { if (id.rows.length > 0) { const user_id = id.rows[0].id; await db.query( - 'insert into member (thread, user, key_delivery) values (?, ?, ?)', - [thread_id, user_id, user.key] + 'insert into member (thread, user) values (?, ?)', + [thread_id, user_id] ); } } if (!msg.permissions || !msg.permissions.view_limited) { - for (let username in io.cache) { - for (let socket of io.cache[username]) { - io.to(socket).emit('new_thread', { + for (let username in vybe.users) { + for (let socket of vybe.users[username].sockets) { + socket.emit('new_thread', { name: msg.name, id: insert.rows[0].id, permissions: { is_member: false, view: true, post: !msg.permissions || !msg.permissions.post_limited - }, + } }); } } } else { for (let member of msg.members) { - for (let socket of io.cache[member.name]) { - io.to(socket).emit('new_thread', { + for (let socket of vybe.users[member.name].sockets) { + socket.emit('new_thread', { name: msg.name, id: insert.rows[0].id, permissions: { is_member: true, view: true, post: true - }, - key: member.key + } }); } } @@ -102,7 +101,7 @@ const create_thread = async (msg, respond, socket, io) => { // respond return respond({ success: true, - id: insert.rows[0].id, + id: insert.rows[0].id }); }; diff --git a/src/event/create_user.js b/src/event/create_user.js index ede70fc..b4528fd 100644 --- a/src/event/create_user.js +++ b/src/event/create_user.js @@ -6,45 +6,49 @@ const create_user = async (msg, respond) => { if (!msg.name) { return respond({ success: false, - message: 'username required', + message: 'username required' + }); + } + if (!msg.pubkey) { + return respond({ + success: false, + message: 'public key required' }); } // ensure username is not taken const result = await db.query('select * from user where name = ?', [ - msg.name, + msg.name ]); if (result.rows.length > 0) { console.log(`username already exists: ${result}`); return respond({ success: false, - message: 'a user with this name already exists on this server', + message: 'a user with this name already exists on this server' }); } // validate public key - if (!msg.pubkey) { - return respond({ - success: false, - message: 'public key required', - }); - } try { await openpgp.readKey({ armoredKey: msg.pubkey }); } catch (err) { console.err('error in create_user readkey: ' + err); return respond({ success: false, - message: 'public key invalid', + message: 'public key invalid' }); } // add to db const insert = await db.query( - 'insert into user (name, pubkey) values (?, ?) returning id', - [msg.name, msg.pubkey] + 'insert into user (name) values (?) returning id', + [msg.name] ); + await db.query( + 'insert into key (user, pubkey, active) values (?, ?, true)', + [insert.rows[0].id, msg.pubkey] + ) // respond return respond({ success: true, - id: insert.rows[0].id, + id: insert.rows[0].id }); }; diff --git a/src/event/get_keys.js b/src/event/get_keys.js index f07688c..7b7e1aa 100644 --- a/src/event/get_keys.js +++ b/src/event/get_keys.js @@ -1,30 +1,29 @@ const db = require('../db'); const authwrap = require('../authwrap'); -const get_keys = async (msg, respond, socket, io) => { +const get_keys = async (msg, respond) => { // validate inputs if (!msg.names) { return respond({ success: false, - message: 'user names required', + message: 'user names required' }); } if (typeof msg.names !== 'object') { return respond({ success: false, - message: "can't iterate user names", + message: "can't iterate user names" }); } const keys = await db.query( - `select name, pubkey from user where name in (${msg.names - .map((i) => '?') - .join(',')})`, + `select name, pubkey from user where name in + (${msg.names.map((i) => '?').join(',')})`, msg.names ); // respond return respond({ success: true, - keys: keys.rows, + keys: keys.rows }); }; diff --git a/src/event/get_space.js b/src/event/get_space.js index b500bdf..f92dd17 100644 --- a/src/event/get_space.js +++ b/src/event/get_space.js @@ -6,17 +6,17 @@ const get_space = async (msg, respond) => { if (!msg.thread) { return respond({ success: false, - message: 'thread ID required', + message: 'thread ID required' }); } if (!(await check_permission(msg.auth_user.id, msg.thread)).view) { return respond({ success: false, - message: "you can't view this thread", + message: "you can't view this thread" }); } const spans = await db.query( - 'select id, content, x, y, scale from span where thread=? and deleted=false', + 'select id, content, x, y, scale from span where thread = ? and deleted = false', [msg.thread] ); return respond({ diff --git a/src/event/list_threads.js b/src/event/list_threads.js index 4140104..60a50ec 100644 --- a/src/event/list_threads.js +++ b/src/event/list_threads.js @@ -4,7 +4,7 @@ const check_permission = require('../check_permission'); const list_threads = async (msg, respond) => { const threads = await db.query( - `select name, id, member.key_delivery as key from thread + `select name, id from thread join permission on thread.id = permission.thread left join member on thread.id = member.thread where permission.permission = 'view' @@ -20,12 +20,12 @@ const list_threads = async (msg, respond) => { for (let thread of threads.rows) { rows.push({ ...thread, - permissions: await check_permission(msg.auth_user.id, thread.id), + permissions: await check_permission(msg.auth_user.id, thread.id) }); } return respond({ success: true, - threads: rows, + threads: rows }); }; diff --git a/src/event/save_span.js b/src/event/save_span.js index d3a1c17..e6fc332 100644 --- a/src/event/save_span.js +++ b/src/event/save_span.js @@ -2,7 +2,7 @@ const db = require('../db'); const authwrap = require('../authwrap'); const check_permission = require('../check_permission'); -const save_span = async (msg, respond, socket, io) => { +const save_span = async (msg, respond, socket) => { if (!msg.thread) { return respond({ success: false, @@ -43,12 +43,11 @@ const save_span = async (msg, respond, socket, io) => { "select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'", [msg.thread] ); - for (let username in io.cache) { + for (let username in vybe.users) { if (permissions.rows.length > 0 || members.includes(username)) { - const sockets = io.cache[username]; - for (let s of sockets) { - if (s !== socket.id) - io.to(s).emit('span', { + for (let s of vybe.users[username].sockets) { + if (s !== socket) + s.emit('span', { id, thread: msg.thread, content: msg.content, diff --git a/src/event/send_message.js b/src/event/send_message.js index b2debeb..5e7d104 100644 --- a/src/event/send_message.js +++ b/src/event/send_message.js @@ -2,17 +2,17 @@ const db = require('../db'); const authwrap = require('../authwrap'); const check_permission = require('../check_permission'); -const send_message = async (msg, respond, socket, io) => { +const send_message = async (msg, respond) => { if (!msg.thread) { return respond({ success: false, - message: 'thread ID required', + message: 'thread ID required' }); } if (!(await check_permission(msg.auth_user.id, msg.thread)).post) { return respond({ success: false, - message: "you can't post to this thread", + message: "you can't post to this thread" }); } // add message and send it to everyone @@ -32,22 +32,21 @@ const send_message = async (msg, respond, socket, io) => { "select * from permission where thread = ? and type = 'everyone' and value = 'true' and permission = 'view'", [msg.thread] ); - for (let username in io.cache) { + for (let username in vybe.users) { if (permissions.rows.length > 0 || members.includes(username)) { - const sockets = io.cache[username]; - for (let s of sockets) { - io.to(s).emit('new_message', { + for (let s of vybe.users[username].sockets) { + s.emit('new_message', { id: id.rows[0].id, name: msg.auth_user.name, message: msg.message, - thread: msg.thread, + thread: msg.thread }); } } } return respond({ success: true, - id: id.rows[0].id, + id: id.rows[0].id }); };