Skip to content

Instantly share code, notes, and snippets.

@LuD1161
Created June 25, 2018 10:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LuD1161/ac97c0e2f09cd98d96dc720c6fe2fcb2 to your computer and use it in GitHub Desktop.
Save LuD1161/ac97c0e2f09cd98d96dc720c6fe2fcb2 to your computer and use it in GitHub Desktop.
// Set name
let color = ['brown', 'black', 'yellow', 'white', 'grey', 'red'][Math.floor(Math.random()*6)];
let breed = ['ragamuffin', 'persian', 'siamese', 'siberian', 'birman', 'bombay', 'ragdoll'][Math.floor(Math.random()*7)];
if (!localStorage.name) localStorage.name = color + '_' + breed;
// Utility functions
let cookie = (name) => (document.cookie.match(new RegExp(`(?:^|; )${name}=(.*?)(?:$|;)`)) || [])[1];
let esc = (str) => str.replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&apos;');
// Sending messages
let send = (msg) => fetch(`send?name=${encodeURIComponent(localStorage.name)}&msg=${encodeURIComponent(msg)}`,
{credentials: 'include'}).then((res) => res.json()).then(handle);
let display = (line) => conversation.insertAdjacentHTML('beforeend', `<p>${line}</p>`);
let recaptcha_id = '6LeB410UAAAAAGkmQanWeqOdR6TACZTVypEEXHcu';
window.addEventListener('load', function() {
messagebox.addEventListener('keydown', function(event) {
if (event.keyCode == 13 && messagebox.value != '') {
if (messagebox.value == '/report') {
grecaptcha.execute(recaptcha_id, {action: 'report'}).then((token) => send('/report ' + token));
} else {
send(messagebox.value);
}
messagebox.value = '';
}
});
send('Hi all');
});
// Receiving messages
function handle(data) {
({
undefined(data) {},
error(data) { display(`Something went wrong :/ Check the console for error message.`); console.error(data); },
name(data) { display(`${esc(data.old)} is now known as ${esc(data.name)}`); },
rename(data) { localStorage.name = data.name; },
secret(data) { display(`Successfully changed secret to <span data-secret="${esc(cookie('flag'))}">*****</span>`); },
msg(data) {
let you = (data.name == localStorage.name) ? ' (you)' : '';
if (!you && data.msg == 'Hi all') send('Hi');
display(`<span data-name="${esc(data.name)}">${esc(data.name)}${you}</span>: <span>${esc(data.msg)}</span>`);
},
ban(data) {
if (data.name == localStorage.name) {
document.cookie = 'banned=1; Path=/';
sse.close();
display(`You have been banned and from now on won't be able to receive and send messages.`);
} else {
display(`${esc(data.name)} was banned.<style>span[data-name^=${esc(data.name)}] { color: red; }</style>`);
}
},
})[data.type](data);
}
let sse = new EventSource("receive");
sse.onmessage = (msg) => handle(JSON.parse(msg.data));
// Say goodbye
window.addEventListener('unload', () => navigator.sendBeacon(`send?name=${encodeURIComponent(localStorage.name)}&msg=Bye`));
// Admin helper function. Invoke this to automate banning people in a misbehaving room.
// Note: the admin will already have their secret set in the cookie (it's a cookie with long expiration),
// so no need to deal with /secret and such when joining a room.
function cleanupRoomFullOfBadPeople() {
send(`I've been notified that someone has brought up a forbidden topic. I will ruthlessly ban anyone who mentions d*gs going forward. Please just stop and start talking about cats for d*g's sake.`);
last = conversation.lastElementChild;
setInterval(function() {
var p;
while (p = last.nextElementSibling) {
last = p;
if (p.tagName != 'P' || p.children.length < 2) continue;
var name = p.children[0].innerText;
var msg = p.children[1].innerText;
if (msg.match(/dog/i)) {
send(`/ban ${name}`);
send(`As I said, d*g talk will not be tolerated.`);
}
}
}, 1000);
}
const http = require('http');
const express = require('express');
const cookieParser = require('cookie-parser')
const uuidv4 = require('uuid/v4');
const SSEClient = require('sse').Client;
const admin = require('./admin');
const pubsub = require('@google-cloud/pubsub')();
const app = express();
app.set('etag', false);
app.use(cookieParser());
// Check if user is admin based on the 'flag' cookie, and set the 'admin' flag on the request object
app.use(admin.middleware);
// Check if banned
app.use(function(req, res, next) {
if (req.cookies.banned) {
res.sendStatus(403);
res.end();
} else {
next();
}
});
// Opening redirect and room index
app.get('/', (req, res) => res.redirect(`/room/${uuidv4()}/`));
let roomPath = '/room/:room([0-9a-f-]{36})';
app.get(roomPath + '/', function(req, res) {
res.sendFile(__dirname + '/static/index.html', {
headers: {
'Content-Security-Policy': [
'default-src \'self\'',
'style-src \'unsafe-inline\' \'self\'',
'script-src \'self\' https://www.google.com/recaptcha/ https://www.gstatic.com/recaptcha/',
'frame-src \'self\' https://www.google.com/recaptcha/',
].join('; ')
},
});
});
// Process incoming messages
app.all(roomPath + '/send', async function(req, res) {
let room = req.params.room, {msg, name} = req.query, response = {}, arg;
console.log(`${room} <-- (${name}):`, msg)
if (!(req.headers.referer || '').replace(/^https?:\/\//, '').startsWith(req.headers.host)) {
response = {type: "error", error: 'CSRF protection error'};
} else if (msg[0] != '/') {
broadcast(room, {type: 'msg', name, msg});
} else {
switch (msg.match(/^\/[^ ]*/)[0]) {
case '/name':
if (!(arg = msg.match(/\/name (.+)/))) break;
response = {type: 'rename', name: arg[1]};
broadcast(room, {type: 'name', name: arg[1], old: name});
case '/ban':
if (!(arg = msg.match(/\/ban (.+)/))) break;
if (!req.admin) break;
broadcast(room, {type: 'ban', name: arg[1]});
case '/secret':
if (!(arg = msg.match(/\/secret (.+)/))) break;
res.setHeader('Set-Cookie', 'flag=' + arg[1] + '; Path=/; Max-Age=31536000');
response = {type: 'secret'};
case '/report':
if (!(arg = msg.match(/\/report (.+)/))) break;
var ip = req.headers['x-forwarded-for'];
ip = ip ? ip.split(',')[0] : req.connection.remoteAddress;
response = await admin.report(arg[1], ip, `https://${req.headers.host}/room/${room}/`);
}
}
console.log(`${room} --> (${name}):`, response)
res.json(response);
res.status(200);
res.end();
});
// Process room broadcast messages
const rooms = new Map();
app.get(roomPath + '/receive', function(req, res) {
res.setHeader('X-Accel-Buffering', 'no');
let channel = new SSEClient(req, res);
channel.initialize();
let roomName = req.params.room;
let room = rooms.get(roomName) || new Set();
rooms.set(roomName, room.add(channel))
req.once('close', () => { room.size > 1 ? room.delete(channel) : rooms.delete(roomName) });
});
// Broadcast to all instances using Cloud Pub/Sub. For local testing, it's easy
// to skip by commenting it out and patching the broadcast fn below.
var publisher;
pubsub.createTopic('catchat', function() {
var topic = pubsub.topic('catchat');
publisher = topic.publisher();
topic.createSubscription('catchat-' + uuidv4(), {ackDeadlineSeconds: 10}).then(function(data) {
data[0].on('message', function(msg) {
msg.ack();
var room = msg.attributes.room;
if (!rooms.has(room)) return;
var msg = msg.data.toString('utf-8');
console.log(`${room} ^^^`, msg)
for (let channel of rooms.get(room)) channel.send(msg);
});
});
});
function broadcast(room, msg) {
// for (let channel of (rooms.get(room) || [])) channel.send(JSON.stringify(msg)); // Local broadcast only
publisher.publish(Buffer.from(JSON.stringify(msg)), {room: room}); // Pub/Sub broadcast
}
// Static files
app.get('/server.js', (req, res) => res.sendFile(__filename));
app.use(express.static(__dirname + '/static/', {fallthrough: false}));
app.listen(8080);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment