Created
June 25, 2018 10:29
-
-
Save LuD1161/ac97c0e2f09cd98d96dc720c6fe2fcb2 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, '''); | |
// 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); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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