const uWS = require('uWebSockets.js');
const argon2 = require('argon2');
const { Pool } = require('pg');
const pool = new Pool();
async function return_key(query, values) {
return (await pool.query({
text: query,
values: values,
rowMode: 'array'
})).rows[0][0];
}
function isLoggedIn(ws) {
return ws.user_ID !== undefined;
}
function is(ws, type) {
return ws.user_type === type;
}
const crypto = require('crypto');
const randomBytes = require('util').promisify(crypto.randomBytes);
require('dotenv').config();
const app = uWS.App()
.ws('/*', {
/* Options */
compression: 0,
maxPayloadLength: 16 * 1024 * 1024,
idleTimeout: 10000,
/* Handlers */
open: (ws, req) => {
console.log('A WebSocket connected via URL: ' + req.getUrl() + '!');
},
message: async (ws, message, isBinary) => {
let {what, parameters, request_ID} = JSON.parse(String.fromCharCode.apply(null, new Uint8Array(message)));
switch(what) {
case 'register':
if(isLoggedIn(ws)) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "already logged in"
}));
} else {
try {
await pool.query('insert into user_account (email, password_hash) values ($1, $2)', [parameters.email, await argon2.hash(parameters.password)]);
ws.send(JSON.stringify({
response_ID: request_ID
}));
} catch(e) {
let error = 'user already exists';
if(e.constraint !== 'user_account_email_key') {
console.error(e);
error = 'error';
}
ws.send(JSON.stringify({
response_ID: request_ID,
data: error
}));
}
}
break;
case 'login':
if(isLoggedIn(ws)) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "already logged in"
}));
} else {
let user = (await pool.query('select * from user_account where email = $1', [parameters.email])).rows[0];
if(user === undefined) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "user does not exist"
}));
} else {
try {
if(await argon2.verify(user.password_hash, parameters.password)) {
ws.user_ID = user.id;
ws.user_type = user.type;
let token = await randomBytes(128);
await pool.query('update user_account set token_hash = $1 where id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.id]);//update to blake3 once it's available in openSSL
delete user.password_hash;
delete user.token_hash;
delete user.user_type;
user.token = token.toString('base64');//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
Object.keys(user).forEach((key) => (user[key] == null) && delete user[key]);
ws.send(JSON.stringify({
response_ID: request_ID,
data: user
}));
} else {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "wrong password"
}));
}
} catch(e) {
console.error(e);
ws.send(JSON.stringify({
response_ID: request_ID,
data: 'error'
}));
}
}
}
break;
case 'auto_login':
if(isLoggedIn(ws)) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "already logged in"
}));
} else {
let user = (await pool.query('select * from user_account where token_hash = $1', [crypto.createHash('BLAKE2b512').update(Buffer.from(parameters.token, 'base64')).digest()])).rows[0];
if(user === undefined) {
ws.send(JSON.stringify({
response_ID: request_ID,
data: "invalid token"
}));
} else {
ws.user_ID = user.id;
ws.user_type = user.type;
//maybe push expiry? lol don't even have a column for that
//let token = await randomBytes(128);
//await pool.query('update user_account set token_hash = $1 where id = $2', [crypto.createHash('BLAKE2b512').update(token).digest(), user.id]);//update to blake3 once it's available in openSSL
delete user.password_hash;
delete user.token_hash;
delete user.user_type;
//user.token = token.toString('base64');//yuck, since json can't send binary, need to base64 encode. fyi base64 is smaller than hex(base16)
Object.keys(user).forEach((key) => (user[key] == null) && delete user[key]);
ws.send(JSON.stringify({
response_ID: request_ID,
data: user
}));
}
}
break;
case 'logout':
delete ws.user;
//clear token_hash in db, etc
break;
case 'list_users':
//401 , 403
if(ws.user && ws.user.type === 'god') {
let result = (await pool.query('select * from user_account')).rows;
ws.send(JSON.stringify(result));
} else {
ws.send('401 or 403');//probably want to obfuscate to prevent reverse engineering?? but security through obscurity is stupid
}
break;
case 'update_user' for god, case 'update_profile' for self
default:
console.log('unknown operation');
}
/* Ok is false if backpressure was built up, wait for drain */
//let ok = ws.send(message, isBinary);
},
drain: (ws) => {
console.log('WebSocket backpressure: ' + ws.getBufferedAmount());
},
close: (ws, code, message) => {
console.log('WebSocket closed');
}
}).any('/*', (res, req) => {
res.end('<div>Nothing to see here!</div><script>const socket = new WebSocket("ws://" + location.hostname);</script>');
}).listen(Number(process.env.PORT), token => {
if(token) {
console.log('Listening to port ' + process.env.PORT);
} else {
console.log('Failed to listen to port ' + process.env.PORT);
}
});