208 lines
7.1 KiB
TypeScript
208 lines
7.1 KiB
TypeScript
/* MIT License
|
||
*
|
||
* Copyright (c) 2020 Adler Oliveira Silva Neves
|
||
*
|
||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||
* of this software and associated documentation files (the "Software"), to deal
|
||
* in the Software without restriction, including without limitation the rights
|
||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
* copies of the Software, and to permit persons to whom the Software is
|
||
* furnished to do so, subject to the following conditions:
|
||
*
|
||
* The above copyright notice and this permission notice shall be included in all
|
||
* copies or substantial portions of the Software.
|
||
*
|
||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||
* SOFTWARE.
|
||
*/
|
||
|
||
/* Contribuitors:
|
||
* - Adler O. S. Neves <adlerosn@gmail.com>
|
||
*/
|
||
|
||
import * as express from 'express';
|
||
import * as WebSocket from 'ws';
|
||
import * as http from 'http';
|
||
import * as uuid from 'uuid';
|
||
import * as cors from 'cors';
|
||
import * as path from 'path';
|
||
import * as fs from 'fs';
|
||
|
||
const app = express();
|
||
|
||
app.use(cors());
|
||
|
||
const server = http.createServer(app);
|
||
|
||
const wss = new WebSocket.Server({ server });
|
||
|
||
let genWebSocketUUID = () => uuid.v4();
|
||
|
||
interface IdentifiableWebSocket extends WebSocket {
|
||
uid: string;
|
||
nick: string;
|
||
room: string;
|
||
roles: Array<string>;
|
||
}
|
||
|
||
function get_outgoing_info(iws: IdentifiableWebSocket) {
|
||
return {
|
||
room: iws.room,
|
||
client_id: iws.uid,
|
||
client_nick: iws.nick,
|
||
client_roles: iws.roles
|
||
}
|
||
}
|
||
|
||
|
||
function broadcastToRoom(ws: WebSocket, message: String) {
|
||
wss.clients.forEach((client: WebSocket) => {
|
||
if ((client as IdentifiableWebSocket).room == (ws as IdentifiableWebSocket).room)
|
||
if (client != ws)
|
||
client.send(message);
|
||
});
|
||
}
|
||
|
||
|
||
wss.on('connection', (ws: WebSocket) => {
|
||
let iws: IdentifiableWebSocket = ws as IdentifiableWebSocket;
|
||
iws.uid = genWebSocketUUID();
|
||
iws.nick = iws.uid;
|
||
iws.room = '';
|
||
iws.roles = [];
|
||
let sendConnectionGreeting = () => {
|
||
broadcastToRoom(ws, JSON.stringify({
|
||
type: "ClientConnected",
|
||
...get_outgoing_info(ws as IdentifiableWebSocket)
|
||
}));
|
||
};
|
||
let sendDisconnectionGreeting = () => {
|
||
broadcastToRoom(ws, JSON.stringify({
|
||
type: "ClientDisconnected",
|
||
...get_outgoing_info(ws as IdentifiableWebSocket)
|
||
}));
|
||
};
|
||
let sendHello = () => {
|
||
ws.send(JSON.stringify({
|
||
type: "YouAre",
|
||
client_id: (ws as IdentifiableWebSocket).uid,
|
||
client_nick: (ws as IdentifiableWebSocket).nick,
|
||
room: (ws as IdentifiableWebSocket).room,
|
||
neighbors: Array.from(wss.clients).filter(client =>
|
||
(client as IdentifiableWebSocket).room == (ws as IdentifiableWebSocket).room
|
||
).map(client => (client as IdentifiableWebSocket).uid).filter(
|
||
client_uid => client_uid != (ws as IdentifiableWebSocket).uid
|
||
)
|
||
}));
|
||
};
|
||
ws.on('message', (message: string) => {
|
||
if (message.startsWith('@joinRoom:')) {
|
||
sendDisconnectionGreeting();
|
||
(ws as IdentifiableWebSocket).room = message.substr(10);
|
||
sendHello();
|
||
sendConnectionGreeting();
|
||
} else if (message == '@iAm') {
|
||
sendHello();
|
||
} else if (message.startsWith('@iAm:')) {
|
||
let newNick = message.substr(5);
|
||
if (newNick.length > 0)
|
||
(ws as IdentifiableWebSocket).nick = newNick;
|
||
else
|
||
(ws as IdentifiableWebSocket).nick = (ws as IdentifiableWebSocket).uid;
|
||
sendHello();
|
||
sendConnectionGreeting();
|
||
} else if (message.startsWith('@iAm.')) {
|
||
let newRoles = message.substr(5).split('.').map(x => x.trim()).filter(x => x.length);
|
||
(ws as IdentifiableWebSocket).roles = newRoles;
|
||
sendHello();
|
||
sendConnectionGreeting();
|
||
} else {
|
||
broadcastToRoom(ws, JSON.stringify({
|
||
type: "MessageBroadcast",
|
||
...get_outgoing_info(ws as IdentifiableWebSocket),
|
||
message: message
|
||
}));
|
||
}
|
||
});
|
||
ws.on('disconnect', (_: WebSocket) => sendDisconnectionGreeting());
|
||
sendHello();
|
||
sendConnectionGreeting();
|
||
});
|
||
|
||
app.get('/', (req, resp) => {
|
||
resp.write(`
|
||
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Websocket-only</title>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
</head>
|
||
<body>
|
||
<article>
|
||
<h1>Websocket-only</h1>
|
||
<h4>Mostly JSON-driven</h4>
|
||
<div>Commands (plain text):</div>
|
||
<ul>
|
||
<li><strong>@iAm</strong></li>
|
||
<li><strong>@joinRoom:</strong><em>room</em></li>
|
||
<li><em>anything else is a message</em></li>
|
||
</ul>
|
||
<br>
|
||
<div>Responses (JSON):</div>
|
||
<ul>
|
||
<li>
|
||
<div>{type: "YouAre",</div>
|
||
<div>client_id: String,</div>
|
||
<div>room: String,</div>
|
||
<div>neighbors: List<String> }</div>
|
||
</li>
|
||
<li>
|
||
<div>{type: "ClientConnected",</div>
|
||
<div>room: String,</div>
|
||
<div>client_id: String }</div>
|
||
</li>
|
||
<li>
|
||
<div>{type: "ClientDisconnected",</div>
|
||
<div>room: String,</div>
|
||
<div>client_id: String }</div>
|
||
</li>
|
||
<li>
|
||
<div>{type: "MessageBroadcast",</div>
|
||
<div>client_id: String,</div>
|
||
<div>message: String }</div>
|
||
</li>
|
||
</ul>
|
||
<p>You can <a href="server.ts">download the source code</a>. It's MIT-licensed.</p>
|
||
<p>If you’re seeing this, maybe you have found my backend.</p>
|
||
<p>Messages aren’t logged and, thus, no history can be retrieved.</p>
|
||
<p>Rooms could be listable, but that brings privacy concerns.</p>
|
||
<p>CORS policy is “allow-all”.</p>
|
||
<p>No rate limits were set.</p>
|
||
<p>Please, don’t abuse me.</p>
|
||
</article>
|
||
</body>
|
||
</html>
|
||
`.trim())
|
||
resp.end();
|
||
});
|
||
app.get('/server.ts', (req, resp) => {
|
||
fs.readFile(`${__dirname}${path.sep}server.ts`, 'utf-8', (err, data) => {
|
||
resp.setHeader('Content-Type', 'text/x-typescript; charset=utf-8');
|
||
resp.setHeader('Content-Disposition', 'attachment; filename="server.ts"');
|
||
resp.send(data);
|
||
resp.end();
|
||
});
|
||
});
|
||
|
||
server.listen((parseInt(process.env.PORT || "0") || 8999), '0.0.0.0', () => {
|
||
let addr = (server.address() || { address: "", family: "", port: 0 });
|
||
let addrString = typeof (addr) == 'string' ? addr : `${addr.address}:${addr.port}`;
|
||
console.log(`Server started on http://${addrString}`);
|
||
});
|