Various updates and general cleanup

- Updated socket.io to v3
- Added namespaces for chat and TanksJS-Server
- Replaced game-server-query with game-dig because it was not updated
- Code cleanup
This commit is contained in:
Arne van Iterson 2020-11-10 19:10:36 +01:00
parent 293a97d256
commit b4e9c67be5
17 changed files with 2628 additions and 684 deletions

34
.eslintrc.js Normal file
View File

@ -0,0 +1,34 @@
module.exports = {
"env": {
"browser": true,
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

94
html/index.html Normal file
View File

@ -0,0 +1,94 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>API Documentation</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="css/styles.css">
<link rel="apple-touch-icon" sizes="180x180" href="/res/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/res/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/res/favicon-16x16.png">
<link rel="manifest" href="/res/site.webmanifest">
</head>
<body>
<div>
<h1>ARNweb API</h1>
</div>
<span class="apiReference get">
<span class="requestType get">GET</span>
<a href="/system"><code>/system</code></a><br>
<div>
<p>Returns json object containing general information about the server.</p>
</div>
</span>
<span class="apiReference get">
<span class="requestType get">GET</span>
<a href="/chat"><code>/chat</code></a><br>
<div>
<p>Returns json object containing general information about the chat server.</p>
</div>
</span>
<span class="apiReference get">
<span class="requestType get">GET</span>
<a href="/tanks"><code>/tanks</code></a><br>
<div>
<p>Returns json object containing status information about the TanksJS-Server instance.</p>
</div>
</span>
<span class="apiReference other">
<span class="requestType other">Socket</span>
<a href="/socket.io/"><code>/socket.io/</code></a><br>
<div>
<p>Socket.io instance that serves both ARNweb chat and a TanksJS server.</p>
<p>Use the following namespaces to access them accordingly:</p><br>
<b><code>/socket.io/tanks</code></b>
<p>TanksJS-server instance, see <a href="//gitea.arnweb.nl/arne/TanksJS-Server">TanksJS-Server</a> for more
information.</p><br>
<b><code>/socket.io/chat</code></b>
<p>Simple chat backend server for <a href="//arnweb.nl/account">ARNweb Chat</a>.<br>Use the following
communication steps to make your own client:</p>
<p>
<code><b>Server -> Client</b><br>user_identify { }</code>
</p>
<p>
<code><b>Server <- Client</b> <br>user_login { "username", "mailHash" }</code>
</p>
<p>
<code><b>Server <- Client</b> <br>message_send { "username", "mailHash", "message" }</code>
</p>
<p>
<code><b>Server -> Client</b><br>message_receive { "username", "mailHash", "message" }</code>
</p>
</div>
</span>
<span class="apiReference get">
<span class="requestType get">GET</span>
<code>/:game/:host</code><br>
<div>
<p>Returns json object containing information about the requested game server.</p>
<p>Powered by node-GameDig, more info on <a href="//www.npmjs.com/package/gamedig">npm</a>.</p>
</div>
</span>
<span class="apiReference other">
<span class="requestType other">cURL</span>
<code>/curl</code><br>
<div>
<p>Returns hello world to cURL requests.</p>
</div>
</span>
</body>
</html>

View File

@ -1,65 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>API Documentation</title>
<script src="//ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<div>
<h1>ARNweb API</h1>
</div>
<span class="apiReference get">
<span class="requestType get">GET</span>
<a href="/system"><code>/system</code></a><br>
<div>
<span>Returns json object containing general information about the server.</span>
</div>
</span>
<span class="apiReference get">
<span class="requestType get">GET</span>
<a href="/chat"><code>/chat</code></a><br>
<div>
<span>Returns json object containing general information about the chat server.</span>
</div>
</span>
<span class="apiReference other">
<span class="requestType other">Socket</span>
<a href="/chat/socket.io/"><code>/chat/socket.io/</code></a><br>
<div>
<span>Socket.io instance used by the chat on ARNweb.</span><br><br>
<span>
<bold>Server -> Client</bold><br><code>user_identify { }</code>
</span><br><br>
<span>
<bold>Server <- Client</bold> <br><code>user_login { "username", "mailHash" }</code>
</span><br><br>
<span>
<bold>Client -> Server</bold><br><code>message_send { "username", "mailHash", "message" }</code>
</span>
</div>
</span>
<span class="apiReference get">
<span class="requestType get">GET</span>
<code>/:game/:host</code><br>
<div>
<span>Returns json object containing information about the requested game server.</span><br>
<span><a href="//www.npmjs.com/package/game-server-query#games-list">Supported games</a></span>
</div>
</span>
<span class="apiReference other">
<span class="requestType other">cURL</span>
<code>/curl</code><br>
<div>
<span>Returns hello world to cURL requests.</span>
</div>
</span>
</body>
</html>

147
index.js
View File

@ -1,147 +0,0 @@
const express = require('express');
const app = express();
const port = 2428;
const server = app.listen(port, () => console.log(`ARNweb API running on port ${port}`))
var io = require('socket.io').listen(server, { path: '/chat/socket.io' })
var striptags = require('striptags');
var MarkdownIt = require('markdown-it'),
md = new MarkdownIt();
var users = [];
const style = require('ansi-styles');
const os = require("os");
const si = require('systeminformation');
var sysInfo;
const gameQuery = require('game-server-query');
const fs = require('fs');
let rawdata = fs.readFileSync(__dirname + "/node_modules/game-server-query/games.json");
const games = JSON.parse(rawdata);
// Request documentation
app.get('/', (req, res) =>
res.sendFile(__dirname + "/index.html")
);
// Function for easy server broadcasts
function serverMsg(content) {
return {
'username': 'Server',
'mailHash': '470965550d3f019830c421f0535d9268',
'message': content
}
}
// Socket.io server side
io.on('connection', function (socket) {
// Send identification request to client
socket.emit('user_identify');
// Bind username and mailhash to client
socket.on('user_login', function (user) {
socket.username = user.username;
socket.mailHash = user.mailHash;
if (users.length > 0) {
socket.emit('message_receive', serverMsg(`Hello ${socket.username}, welcome to ARNweb Chat. The following people are currently online: ${users.join(', ')}`));
} else {
socket.emit('message_receive', serverMsg(`Hello ${socket.username}, welcome to ARNweb Chat. You are the only one here at the moment`));
}
io.emit('message_receive', serverMsg(`${socket.username} has connected`));
users.push(socket.username);
});
// Send disconnect broadcast to all clients
socket.on('disconnect', function () {
io.emit('message_receive', serverMsg(`${socket.username} has disconnected`));
users.splice(users.indexOf(socket.username), 1);
});
// Handle messages
socket.on('message_send', function (message) {
// Build message data for clients
var message = striptags(message, '<br />');
if (message !== '') {
message = md.render(message);
message = message.replace(/\r?\n/g, '<br />');
var msg = {
'username': socket.username,
'mailHash': socket.mailHash,
'message': message
}
// Send message to clients
io.emit('message_receive', msg);
} else {
socket.emit('message_receive', serverMsg(`Empty messages will not be sent.`));
}
});
});
// Chat socket and info
app.get('/chat/', function (req, res) {
// Server browser
res.json({
"status": "online",
"users": users
});
});
// System info
app.get('/system/', function (req, res) {
var time = new Date();
sysInfo = {
"hostname": os.hostname(),
"type": os.type(),
"load": os.loadavg(),
"time": si.time()
};
si.cpuTemperature(function (data) {
sysInfo["temp"] = data;
});
si.mem(function (data) {
sysInfo["mem"] = data;
});
si.fsSize(function (data) {
sysInfo["fs"] = data;
res.json(sysInfo);
})
});
// cURL
app.get('/curl/', function (req, res) {
const userAgent = req.headers['user-agent'];
if (userAgent.search(/curl|wget/i) !== -1) {
res.send(`
${style.green.open}Hello Green!${style.green.close}
${style.red.open}Hello Red!${style.red.close}
`);
} else {
res.send('Only cURL requests!');
}
});
app.get('/css/:stylesheet', function (req, res) {
res.sendFile(__dirname + '/css/' + req.params.stylesheet);
});
// Game servers
app.get('/:type/:host', function (req, res) {
if (games[req.params.type] != undefined) {
gameQuery(
{
type: req.params.type,
host: req.params.host
},
function (state) {
res.json(state);
}
);
} else {
res.json({ "error": "Error: Game type unknown." });
}
});

2602
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,10 +1,11 @@
{
"name": "arnweb-api",
"version": "1.0.0",
"description": "API for api.arnweb.nl\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[D\u001b[Don\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[C\u001b[D\u001b[D\u001b[Dapi.arnweb.nl for getting information from various servers and other information about the server.",
"main": "index.js",
"description": "API for getting information about services run on ARNweb.nl.",
"main": "src/index.js",
"scripts": {
"start": "node index.js"
"start": "node src/index.js",
"start:dev": "nodemon src/index.js"
},
"repository": {
"type": "git",
@ -15,16 +16,21 @@
"game",
"arnweb"
],
"author": "McArn",
"author": "Arne van Iterson",
"license": "ISC",
"dependencies": {
"ansi-styles": "^4.2.1",
"colors": "^1.4.0",
"express": "^4.17.1",
"game-server-query": "^1.0.10",
"gamedig": "^2.0.23",
"markdown-it": "^10.0.0",
"os": "^0.1.1",
"socket.io": "^2.3.0",
"socket.io": "^3.0.1",
"striptags": "^3.1.1",
"systeminformation": "^4.16.0"
},
"devDependencies": {
"eslint": "^7.13.0",
"nodemon": "^2.0.6"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
res/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

BIN
res/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 B

BIN
res/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 650 B

BIN
res/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

16
res/site.webmanifest Normal file
View File

@ -0,0 +1,16 @@
{
"name": "",
"short_name": "",
"icons": [{
"src": "/res/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}, {
"src": "/res/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

116
src/index.js Normal file
View File

@ -0,0 +1,116 @@
const express = require("express");
const app = express();
const port = 2428;
const server = app.listen(port, () => console.log(`ARNweb API running on port ${port}`));
const path = require("path");
const style = require("ansi-styles");
const Gamedig = require("gamedig");
const io = require("socket.io")(server, {
path: "/socket.io"
});
var ns = {
chat: io.of("/chat"),
tanks: io.of("/tanks")
};
// Request documentation
app.get("/", (req, res) =>
res.status(200).sendFile(path.resolve(__dirname + "/../html/index.html"))
);
app.get("/css/:stylesheet", function (req, res) {
res.status(200).sendFile(path.resolve(__dirname + "/../css/" + req.params.stylesheet));
});
app.get("/res/:icon", function (req, res) {
res.status(200).sendFile(path.resolve(__dirname + "/../res/" + req.params.icon));
});
// Socket.io chat server side
const chat = require("./services/chat.js");
ns.chat.on("connection", function (socket) {
chat.handle(ns.chat, socket);
});
// Chat socket and info
app.get("/chat/", function (req, res) {
// Server browser
res.status(200).json({
"status": "online",
"query": {
"path": req.path,
"time": Date.now()
},
"users": chat.users
});
});
// Socket.io tanks server side
const tanks = require("./services/tanks.js");
ns.tanks.on("connection", function (socket) {
tanks.handle(ns.tanks, socket);
});
app.get("/tanks", function (req, res) {
res.status(200).json({
"status": "online",
"query": {
"path": req.path,
"time": Date.now()
},
"players": tanks.players
});
});
// System info
const system = require("./services/system.js");
app.get("/system/", function (req, res) {
system.handle(req, res);
});
// cURL
app.get("/curl/", function (req, res) {
const userAgent = req.headers["user-agent"];
if (userAgent.search(/curl|wget/i) !== -1) {
res.status(200).send(`
${style.green.open}Hello Green!${style.green.close}
${style.red.open}Hello Red!${style.red.close}
`);
} else {
res.status(400).json({
"error": "Error: Only cURL requests."
});
}
});
// Game servers
app.get("/:type/:host", function (req, res) {
var query = {
type: req.params.type
};
var host = String(req.params.host).split(":");
if (host.length > 1) {
query.host = host[0],
query.port = host[1];
} else {
query.host = req.params.host;
}
Gamedig.query(
query
).then((state) => {
res.status(200).json(state);
}).catch((error) => {
query.path = req.path;
query.time = Date.now();
res.status(503).json({
"error": error.message,
"query": query
});
});
});

62
src/services/chat.js Normal file
View File

@ -0,0 +1,62 @@
var striptags = require("striptags");
var MarkdownIt = require("markdown-it"),
md = new MarkdownIt();
var users = [];
// Function for easy server broadcasts
function serverMsg(content) {
return {
"username": "Server",
"mailHash": "470965550d3f019830c421f0535d9268",
"message": content
};
}
function handle(io, socket) {
// Send identification request to client
socket.emit("user_identify");
// Bind username and mailhash to client
socket.on("user_login", function (user) {
socket.username = user.username;
socket.mailHash = user.mailHash;
if (users.length > 0) {
socket.emit("message_receive", serverMsg(`Hello ${socket.username}, welcome to ARNweb Chat. The following people are currently online: ${users.join(", ")}`));
} else {
socket.emit("message_receive", serverMsg(`Hello ${socket.username}, welcome to ARNweb Chat. You are the only one here at the moment`));
}
io.emit("message_receive", serverMsg(`${socket.username} has connected`));
users.push(socket.username);
});
// Send disconnect broadcast to all clients
socket.on("disconnect", function () {
io.emit("message_receive", serverMsg(`${socket.username} has disconnected`));
users.splice(users.indexOf(socket.username), 1);
});
// Handle messages
socket.on("message_send", function (message) {
// Build message data for clients
message = striptags(message, "<br />");
if (message !== "") {
message = md.render(message);
message = message.replace(/\r?\n/g, "<br />");
var msg = {
"username": socket.username,
"mailHash": socket.mailHash,
"message": message
};
// Send message to clients
io.emit("message_receive", msg);
} else {
socket.emit("message_receive", serverMsg("Empty messages will not be sent."));
}
});
}
module.exports = {
handle,
users
};

26
src/services/system.js Normal file
View File

@ -0,0 +1,26 @@
const os = require("os");
const si = require("systeminformation");
var sysInfo;
function handle(req, res) {
sysInfo = {
"hostname": os.hostname(),
"type": os.type(),
"load": os.loadavg(),
"time": si.time()
};
si.cpuTemperature(function (data) {
sysInfo["temp"] = data;
});
si.mem(function (data) {
sysInfo["mem"] = data;
});
si.fsSize(function (data) {
sysInfo["fs"] = data;
res.status(200).json(sysInfo);
});
}
module.exports = {
handle
};

130
src/services/tanks.js Normal file
View File

@ -0,0 +1,130 @@
var colours = require("colors");
colours.enable();
colours.setTheme({
request: "green",
connect: "cyan",
disconnect: ["red", "dim"],
room: ["brightWhite"],
data: "grey"
});
const traffic = {
in: colours.data("-> "),
out: colours.data("<- "),
int: colours.data("-- ")
};
var players = {};
function handle(io, socket) {
// Handle disconnection
socket.on("disconnect", () => {
if (players[socket.id]) {
const room = players[socket.id].room;
var playerUpdate = {};
if (io.adapter.rooms[room]) {
for (const id in io.adapter.rooms[room].sockets) {
playerUpdate[id] = players[id];
}
}
io.in(room).emit("roomUpdate", playerUpdate);
console.log(traffic.out + colours.disconnect(`User ${String(socket.id)} disconnected from room ${room}`));
if (io.adapter.rooms.get(room) == undefined) {
console.log(traffic.int + colours.room(`Room ${room} has been deleted due to a lack of players`));
}
}
delete players[socket.id];
});
console.log(traffic.in + colours.request(`Socket ${String(socket.id)} is trying to connect`));
console.log(traffic.out + colours.data(`Requesting identification to client ${String(socket.id)}`));
// Request identification
socket.emit("identify");
// Wait for identification
socket.on("identification", (data) => {
console.log(traffic.in + colours.request(`User ${String(socket.id)} playing ${data.gameID} is trying to join a room with ${data.playersMax} players max`));
// Define room to be joined
var join = "";
// Check if there are any available rooms for the requested game and check if they aren't full
io.adapter.rooms.forEach((set, room) => {
if (/[0-9]_[0-9].*/i.test(room)) {
var roomId = room.split("_");
if (roomId[0] == data.gameID) {
if (roomId[1] == data.playersMax) {
if (io.adapter.rooms.get(room).size < data.playersMax) {
join += room;
} else {
console.log(traffic.int + colours.room(`Room ${room} is full, skipping.`));
}
}
}
}
});
// If no available room is found, make one
if (join == "") {
var count = 0;
var regex = new RegExp(data.gameID + "_" + data.playersMax + "_[0-9].*");
io.adapter.rooms.forEach((set, room) => {
if (regex.test(room)) {
count++;
}
});
join = data.gameID + "_" + data.playersMax + "_" + count;
console.log(traffic.int + colours.room(`There is no room available for the requested game, making room ${join}`));
}
socket.join(join);
// Start game if the max number of players is reached
if (io.adapter.rooms.get(join).size == data.playersMax) {
console.log(traffic.int + colours.room(`Room ${join} has reached the maximum amount of players, starting game`));
io.in(join).emit("gameStart");
}
players[socket.id] = {
username: data.name,
room: join
};
var playerUpdate = {};
io.adapter.rooms.get(join).forEach((id) => {
playerUpdate[id] = players[id];
});
io.in(join).emit("roomUpdate", playerUpdate);
console.log(traffic.in + colours.connect(`User ${String(socket.id)} connected to room ${join}`));
});
socket.on("update", (data) => {
if (players[socket.id]) {
socket.to(players[socket.id].room).emit("update", {
uuid: socket.id,
player: data
});
}
});
socket.on("spectate", (data) => {
if (io.adapter.rooms.has(data)) {
console.log(traffic.in + colours.connect(`User ${String(socket.id)} is spectating room ${data}`));
socket.join(data);
}
});
}
module.exports = {
handle,
players
};