Add linter and documentation for future gateway version

This commit is contained in:
hippoz 2021-03-04 21:28:02 +02:00
parent 9ed67992a1
commit 1c1b710cde
No known key found for this signature in database
GPG key ID: 7C52899193467641
18 changed files with 1860 additions and 842 deletions

29
.eslintrc.js Normal file
View file

@ -0,0 +1,29 @@
module.exports = {
"env": {
"commonjs": true,
"es2021": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 12
},
"rules": {
"indent": [
"error",
4
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"double"
],
"semi": [
"error",
"always"
]
}
};

200
DOCS.md Normal file
View file

@ -0,0 +1,200 @@
# Waffle API docs (currently not implemented)
# Gateway
Waffle uses a WebSocket gateway to transmit various updates from the server to the client and vice-versa (new message, user disconnecting, connect to voice, etc.)
# Gateway Payload format
All payloads sent through the gateway are *string payloads*. They all start with a number represeting the "instruction", followed by a "@" character (to delimit the instruction number from the rest of the payload), after which comes the data for the specific instruction.
Example packet:
```json
23@this is my payload data
```
Note for the example above: The "this is my payload data" text should not be user controlled. If user controlled input is required, it must be sanitized and/or passed as a JSON field.
Packets can also have JSON as a payload:
```json
32@{"name":"Alice"}
```
## Instructions
The terms "channel" and "category" are used interchangeably.
## 0:HELLO
*Part of handshake, Server to client*
Sent by the server to the client as soon as possible after they connect to the gateway.
This payload can contain a JSON object with various information about the server. It should be relied on only when absolutely necessary.
Example:
```json
0@{}
```
## 1:YOO
*Part of handshake, Client to server*
Sent by the client as soon as possible after receiving the HELLO instruction.
JSON data format:
| Field | Description |
| - | - |
| token | The authentication token |
| client | The name of the application the client is connecting from (e.g. browser_react_app) |
Example:
```json
1@{"token":"my totally real token","client":"amazing client"}
```
If the token is invalid, or the connection is otherwise rejected, the client should be disconnected as soon as possible, and no YOO\_ACK should be sent.
## 2:YOO\_ACK
*Part of handshake, Server to client*
Sent by the server as soon as possible after processing the YOO payload from the client.
This payload contains an empty JSON object, that may have data added to it in later versions of this software.
Example:
```json
2@{}
```
## 3:ACTION\_CREATE\_MESSAGE
*Auth required, Client to server*
Sent by the client when sending a message.
JSON data format:
| Field | Description |
| - | - |
| content | The text content of the message |
| channel | An object that contains "_id", the id of the channel to send the message to |
Example:
```json
3@{"content":"i hate you","channel_id":"totally real channel id"}
```
## 4:EVENT\_NEW\_MESSAGE
*Auth required, Server to client*
Sent by the server when a new message is created in a channel the client is subscribed to.
JSON data format:
[Message object](#message-object)
## 5:ACTION\_UPDATE\_STATUS
*Auth required, Client to server*
Sent by the client when updating status. Usually, this packet is sent as soon as possible after getting YOO\_ACK to indicate the client is online.
JSON data format:
| Field | Description |
| - | - |
| status | Can be 0(STATUS\_OFFLINE) or 1(STATUS\_ONLINE) |
| status_text | Custom status text, max 50 characters, min 1 non-whitespace character, trimmed |
Example:
```json
5@{"status":1,"status_text":"hello"}
```
## 6:EVENT\_CHANNEL\_MEMBERS
*Auth required, Server to client*
Sent by the server after YOO\_ACK, and otherwise when the entire status needs to be updated. If only one user's status changes, use EVENT\_CHANNEL\_MEMBER\_UPDATE.
JSON data format (this ones tough):
Contains a JSON object, where each key is a channel id and each property is an array of [user presence objects](#user-presence-object).
Example:
```json
6@{"totallyrealid":[{_id:"totallyrealuserid",username:"totallyrealusername",status:1,status_text:"hello"}]}
```
## 7:EVENT\_CHANNEL\_MEMBER\_UPDATE
*Auth required, Server to client*
Sent by the server when the status of a member changes. Of course, the client has to be in a channel with that member in order to receive this update.
JSON data format:
It's just a [user presence object](#user-presence-object).
Example:
```json
7@{_id:"totallyrealuserid",username:"totallyrealusername",status:1,status_text:"hello"}
```
## 21:ACTION\_VOICE\_BEGIN\_SESSION
*Auth required, Client to server*
Sent by the client whenever it wants to initiate a voice session in a specific channel.
JSON data format:
| Field | Description |
| - | - |
| channel | An object that contains "_id", the id of the channel to connect to |
(unfinished)
## 22:ACTION\_VOICE\_BEGIN\_SESSION
*Auth required, Client to server*
Sent by the server when the voice session request has been accepted. If it is rejected, the client MUST be disconnected from the gateway as soon as possible.
JSON data format:
| Field | Description |
| - | - |
| reportTo | The IP and port of the voice server to connect to |
(unfinished)
# Objects and data types
## Message object
| Field | Description |
| - | - |
| content | The text content of the message (max 2000 characters, min 1 character, trimmed) |
| channel | A [message channel object](#message-channel-object) |
| author | A [message author object](#message-author-object) |
## Message channel object
Used mostly for messages.
| Field | Description |
| - | - |
| _id | The id of the channel |
| name | The name of the channel |
## Message author object
Used mostly for messages.
| Field | Description |
| - | - |
| _id | The id of the user |
| name | The name of the user |
## User presence object
| Field | Description |
| - | - |
| _id | The id of the user |
| name | The name of the user |
| status | The status of the user (see ACTION_UPDATE_STATUS) |
| status_text | The text status of the user (see ACTION_UPDATE_STATUS) |

View file

@ -1,19 +1,19 @@
const User = require('../../models/User');
const secret = require('../../secret');
const config = require('../../config');
const User = require("../../models/User");
const secret = require("../../secret");
const config = require("../../config");
const jwt = require('jsonwebtoken');
const jwt = require("jsonwebtoken");
const redirect = (res, status=401, url=undefined) => {
if (!url) {
res.status(status).json({
error: true,
message: 'ERROR_ACCESS_DENIED'
message: "ERROR_ACCESS_DENIED"
});
return;
}
res.redirect(url);
}
};
function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config.roleMap.RESTRICTED) {
return (req, res) => {
@ -31,7 +31,7 @@ function authenticateEndpoint(callback, url=undefined, minPermissionLevel=config
if (!data) {
redirect(res, 401, url);
return
return;
}
if (!data.username) {

View file

@ -1,13 +1,13 @@
const User = require('../../models/User');
const Category = require('../../models/Category');
const Post = require('../../models/Post');
const config = require('../../config');
const User = require("../../models/User");
const Category = require("../../models/Category");
const Post = require("../../models/Post");
const config = require("../../config");
const { authenticateEndpoint } = require('./authfunctions');
const { authenticateEndpoint } = require("./authfunctions");
const mongoose = require('mongoose');
const { body, param, validationResult } = require('express-validator');
const express = require('express');
const mongoose = require("mongoose");
const { body, param, validationResult } = require("express-validator");
const express = require("express");
const app = express.Router();
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
@ -19,13 +19,13 @@ const createLimiter = rateLimit({
max: 10,
});
app.post('/category/create', [
app.post("/category/create", [
createLimiter,
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape()
], authenticateEndpoint(async (req, res, user) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
return;
}
@ -38,20 +38,20 @@ app.post('/category/create', [
res.status(200).json({
error: false,
message: 'SUCCESS_CATEGORY_CREATED',
message: "SUCCESS_CATEGORY_CREATED",
category: category.getPublicObject()
});
}, undefined, config.roleMap.USER));
app.post('/post/create', [
app.post("/post/create", [
createLimiter,
body('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
body('title').not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
body('body').not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
body("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 }),
body("title").not().isEmpty().trim().isLength({ min: 3, max: 32 }).escape(),
body("body").not().isEmpty().trim().isLength({ min: 3, max: 1000 }).escape(),
], authenticateEndpoint(async (req, res, user) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
return;
}
@ -74,46 +74,46 @@ app.post('/post/create', [
if (r.n < 1) {
res.status(404).json({
error: true,
message: 'ERROR_CATEGORY_NOT_FOUND'
message: "ERROR_CATEGORY_NOT_FOUND"
});
return;
}
res.status(200).json({
error: false,
message: 'SUCCESS_POST_CREATED',
message: "SUCCESS_POST_CREATED",
post: {
_id: post._id
}
});
}, undefined, config.roleMap.USER));
app.get('/category/:category/info', [
param('category').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
], authenticateEndpoint(async (req, res, user) => {
app.get("/category/:category/info", [
param("category").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
], authenticateEndpoint(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
return;
}
const categoryId = req.params.category;
const category = await Category.findById(categoryId).populate('posts.creator', User.getPulicFields());
const category = await Category.findById(categoryId).populate("posts.creator", User.getPulicFields());
// TODO: Implement subscribing to a channel and stuff
const users = await User.find().sort({ _id: -1 }).limit(50).select(User.getPulicFields())
const users = await User.find().sort({ _id: -1 }).limit(50).select(User.getPulicFields());
if (!category) {
res.status(404).json({
error: true,
message: 'ERROR_CATEGORY_NOT_FOUND'
message: "ERROR_CATEGORY_NOT_FOUND"
});
return;
}
res.status(200).json({
error: false,
message: 'SUCCESS_CATEGORY_DATA_FETCHED',
message: "SUCCESS_CATEGORY_DATA_FETCHED",
category: category.getPublicObject(),
userInfo: {
userListLimit: 50,
@ -122,18 +122,18 @@ app.get('/category/:category/info', [
});
}, undefined, config.roleMap.USER));
app.get('/category/list', authenticateEndpoint(async (req, res, user) => {
app.get("/category/list", authenticateEndpoint(async (req, res) => {
let count = parseInt(req.query.count);
if (!Number.isInteger(count)) {
count = 10;
}
// TODO: This is probably not efficient
const categories = await Category.find().sort({ _id: -1 }).limit(count).select('-posts -__v').populate('creator', User.getPulicFields());
const categories = await Category.find().sort({ _id: -1 }).limit(count).select("-posts -__v").populate("creator", User.getPulicFields());
res.status(200).json({
error: false,
message: 'SUCCESS_CATEGORY_LIST_FETCHED',
message: "SUCCESS_CATEGORY_LIST_FETCHED",
categories
});
}, undefined, config.roleMap.USER));

View file

@ -1,17 +1,17 @@
const User = require('../../../models/User');
const secret = require('../../../secret');
const config = require('../../../config');
const Category = require('../../../models/Category');
const RateLimiter = require('./ratelimiter');
const User = require("../../../models/User");
const secret = require("../../../secret");
const config = require("../../../config");
const Category = require("../../../models/Category");
const RateLimiter = require("./ratelimiter");
const jwt = require('jsonwebtoken');
const siolib = require('socket.io');
const uuid = require('uuid');
const jwt = require("jsonwebtoken");
const siolib = require("socket.io");
const uuid = require("uuid");
class GatewayServer {
constructor(httpServer) {
this._io = siolib(httpServer);
this._gateway = this._io.of('/gateway');
this._gateway = this._io.of("/gateway");
this.rateLimiter = new RateLimiter({
points: 5,
time: 1000,
@ -19,15 +19,15 @@ class GatewayServer {
});
this.eventSetup();
this._commandPrefix = '/';
this._commandPrefix = "/";
}
}
GatewayServer.prototype._sendSystemMessage = function(socket, message, category) {
const messageObject = {
author: {
username: '__SYSTEM',
_id: '5fc69864f15a7c5e504c9a1f'
username: "__SYSTEM",
_id: "5fc69864f15a7c5e504c9a1f"
},
category: {
title: category.title,
@ -37,78 +37,78 @@ GatewayServer.prototype._sendSystemMessage = function(socket, message, category)
_id: uuid.v4()
};
socket.emit('message', messageObject);
socket.emit("message", messageObject);
};
GatewayServer.prototype.notifyClientsOfUpdate = function(reason) {
this._gateway.emit('refreshClient', { reason: reason || 'REFRESH' });
this._gateway.emit("refreshClient", { reason: reason || "REFRESH" });
};
GatewayServer.prototype._processCommand = async function(socket, message) {
const content = message.content;
const fullCommandString = content.slice(this._commandPrefix.length, content.length);
const fullCommand = fullCommandString.split(' ');
const command = fullCommand[0] || 'INVALID_COMMAND';
const fullCommand = fullCommandString.split(" ");
const command = fullCommand[0] || "INVALID_COMMAND";
const args = fullCommand.length - 1;
switch (command) {
case 'INVALID_COMMAND': {
this._sendSystemMessage(socket, 'Invalid command.', message.category);
case "INVALID_COMMAND": {
this._sendSystemMessage(socket, "Invalid command.", message.category);
break;
}
case 'admin/fr': {
case "admin/fr": {
if (args === 1) {
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
this._gateway.emit('refreshClient', { reason: fullCommand[1] || 'REFRESH' });
this._gateway.emit("refreshClient", { reason: fullCommand[1] || "REFRESH" });
} else {
this._sendSystemMessage(socket, 'how about no', message.category);
this._sendSystemMessage(socket, "how about no", message.category);
}
} else {
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
}
break;
}
case 'admin/fru': {
case "admin/fru": {
if (args === 1) {
if (socket.user.permissionLevel >= config.roleMap.ADMIN) {
const user = await this._findSocketInRoom(message.category._id, fullCommand[1]);
if (!user) {
this._sendSystemMessage(socket, 'User not found.', message.category);
this._sendSystemMessage(socket, "User not found.", message.category);
break;
}
this._gateway.in(user.user.sid).emit('refreshClient', { reason: 'REFRESH' });
this._gateway.in(user.user.sid).emit("refreshClient", { reason: "REFRESH" });
} else {
this._sendSystemMessage(socket, 'how about no', message.category);
this._sendSystemMessage(socket, "how about no", message.category);
}
} else {
this._sendSystemMessage(socket, 'Invalid number of arguments.', message.category);
this._sendSystemMessage(socket, "Invalid number of arguments.", message.category);
}
break;
}
default: {
this._sendSystemMessage(socket, 'That command does not exist.', message.category);
this._sendSystemMessage(socket, "That command does not exist.", message.category);
break;
}
}
};
GatewayServer.prototype.authDisconnect = function(socket, callback) {
console.log('[E] [gateway] [handshake] User disconnected due to failed authentication');
console.log("[E] [gateway] [handshake] User disconnected due to failed authentication");
socket.isConnected = false;
socket.disconnect();
socket.disconnect(true);
callback(new Error('ERR_GATEWAY_AUTH_FAIL'));
callback(new Error("ERR_GATEWAY_AUTH_FAIL"));
};
GatewayServer.prototype.eventSetup = function() {
this._gateway.use((socket, callback) => {
console.log('[*] [gateway] [handshake] User authentication attempt');
console.log("[*] [gateway] [handshake] User authentication attempt");
socket.isConnected = false;
setTimeout(() => {
if (socket.isConnected) return;
console.log('[E] [gateway] [handshake] User still not connected after timeout, removing...');
console.log("[E] [gateway] [handshake] User still not connected after timeout, removing...");
socket.disconnect();
socket.disconnect(true);
}, config.gatewayStillNotConnectedTimeoutMS);
@ -117,7 +117,7 @@ GatewayServer.prototype.eventSetup = function() {
const token = socket.handshake.query.token;
if (!token) return this.authDisconnect(socket, callback);
if (!(typeof token === 'string')) return this.authDisconnect(socket, callback);
if (!(typeof token === "string")) return this.authDisconnect(socket, callback);
const allSockets = this._gateway.sockets;
for (let [_, e] of allSockets) {
@ -155,10 +155,10 @@ GatewayServer.prototype.eventSetup = function() {
});
});
this._gateway.on('connection', (socket) => {
this._gateway.on("connection", (socket) => {
console.log(`[*] [gateway] [handshake] User ${socket.user.username} connected, sending hello and waiting for yoo...`);
socket.emit('hello', {
socket.emit("hello", {
gatewayStillNotConnectedTimeoutMS: config.gatewayStillNotConnectedTimeoutMS,
resolvedUser: {
username: socket.user.username,
@ -166,14 +166,14 @@ GatewayServer.prototype.eventSetup = function() {
}
});
socket.once('yoo', () => {
socket.once("yoo", () => {
console.log(`[*] [gateway] [handshake] Got yoo from ${socket.user.username}, connection is finally completed!`);
socket.isConnected = true;
socket.on('message', async ({ category, content, nickAuthor, destUser }) => {
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === 'string') || !(typeof category._id === 'string')) return;
socket.on("message", async ({ category, content, nickAuthor, destUser }) => {
if (!category || !content || !socket.joinedCategories || !socket.isConnected || !socket.user || !(typeof content === "string") || !(typeof category._id === "string")) return;
content = content.trim();
if (!content || content === '' || content === ' ' || content.length >= 2000) return;
if (!content || content === "" || content === " " || content.length >= 2000) return;
if (!this.rateLimiter.consoom(socket.user.token)) { // TODO: maybe user ip instead of token?
console.log(`[E] [gateway] Rate limiting ${socket.user.username}`);
return;
@ -181,7 +181,7 @@ GatewayServer.prototype.eventSetup = function() {
// TODO: When/if category permissions are added, check if the user has permissions for that category
const categoryTitle = socket.joinedCategories[category._id];
if (!categoryTitle || !(typeof categoryTitle === 'string')) return;
if (!categoryTitle || !(typeof categoryTitle === "string")) return;
let messageObject = {
author: {
@ -197,14 +197,14 @@ GatewayServer.prototype.eventSetup = function() {
_id: uuid.v4()
};
if (nickAuthor && nickAuthor.username && (typeof nickAuthor.username) === 'string' && nickAuthor.username.length <= 32 && nickAuthor.username.length >= 3) {
if (nickAuthor && nickAuthor.username && (typeof nickAuthor.username) === "string" && nickAuthor.username.length <= 32 && nickAuthor.username.length >= 3) {
if (socket.user.permissionLevel === config.roleMap.BOT) {
messageObject = {
nickAuthor: {
username: nickAuthor.username
},
...messageObject
}
};
}
}
@ -213,22 +213,22 @@ GatewayServer.prototype.eventSetup = function() {
return;
}
if (destUser && destUser._id && (typeof destUser._id) === 'string') {
if (destUser && destUser._id && (typeof destUser._id) === "string") {
const user = await this._findSocketInRoom(messageObject.category._id, destUser._id);
if (!user) return;
this._gateway.in(user.user.sid).emit('message', messageObject);
this._gateway.in(user.user.sid).emit("message", messageObject);
return;
}
this._gateway.in(category._id).emit('message', messageObject);
this._gateway.in(category._id).emit("message", messageObject);
});
socket.on('subscribe', async (categories) => {
socket.on("subscribe", async (categories) => {
if ( !socket.isConnected || !socket.user || !categories || !Array.isArray(categories) || categories === []) return;
try {
for (const v of categories) {
if (!v && !(typeof v === 'string')) continue;
if (!v && !(typeof v === "string")) continue;
// TODO: When/if category permissions are added, check if the user has permissions for that category
const category = await Category.findById(v);
if (category && category.title && category._id) {
@ -240,7 +240,7 @@ GatewayServer.prototype.eventSetup = function() {
console.log(`[*] [gateway] User ${socket.user.username} subscribed to room ${v} (${category.title}), sending updated user list to all members of that room...`);
const upd = await this._generateClientListUpdateObject(v, category.title);
this._gateway.in(v).emit('clientListUpdate', upd);
this._gateway.in(v).emit("clientListUpdate", upd);
}
}
} catch (e) {
@ -248,18 +248,18 @@ GatewayServer.prototype.eventSetup = function() {
}
});
socket.on('disconnecting', async () => {
socket.on("disconnecting", async () => {
console.log(`[*] [gateway] User ${socket.user.username} is disconnecting, broadcasting updated user list to all of the rooms they have been in...`);
const rooms = socket.rooms;
rooms.forEach(async (room) => {
// Socket io automatically adds a user to a room with their own id
if (room === socket.id) return;
const categoryTitle = socket.joinedCategories[room] || 'UNKNOWN';
const categoryTitle = socket.joinedCategories[room] || "UNKNOWN";
await socket.leave(room);
const upd = await this._generateClientListUpdateObject(room, categoryTitle);
socket.in(room).emit('clientListUpdate', upd);
socket.in(room).emit("clientListUpdate", upd);
});
});
});
@ -309,7 +309,7 @@ GatewayServer.prototype._findSocketInRoom = async function(room, userid) {
return updatedClientList[0] || undefined;
};
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle='UNKNOWN') {
GatewayServer.prototype._generateClientListUpdateObject = async function(room, categoryTitle="UNKNOWN") {
const clientList = await this._getSocketsInRoom(room);
return {
category: {

View file

@ -1,16 +1,16 @@
const usersAPI = require('./users');
const contentAPI = require('./content');
const usersAPI = require("./users");
const contentAPI = require("./content");
const express = require('express');
const express = require("express");
const app = express.Router();
app.use('/users', usersAPI);
app.use('/content', contentAPI);
app.use("/users", usersAPI);
app.use("/content", contentAPI);
app.get('/', (req, res) => {
app.get("/", (req, res) => {
// TODO: Add more checks for this, or maybe remove
res.json({ apiStatus: 'OK', apis: [ 'users', 'content' ] });
res.json({ apiStatus: "OK", apis: [ "users", "content" ] });
});
module.exports = app;

View file

@ -1,15 +1,15 @@
const User = require('../../models/User');
const config = require('../../config');
const secret = require('../../secret');
const User = require("../../models/User");
const config = require("../../config");
const secret = require("../../secret");
const { authenticateEndpoint } = require('./authfunctions');
const { authenticateEndpoint } = require("./authfunctions");
// TODO: Might want to use something else (https://blog.benpri.me/blog/2019/01/13/why-you-shouldnt-be-using-bcrypt-and-scrypt/)
const bcrypt = require('bcrypt');
const mongoose = require('mongoose');
const { body, query, param, validationResult } = require('express-validator');
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require("bcrypt");
const mongoose = require("mongoose");
const { body, param, validationResult } = require("express-validator");
const express = require("express");
const jwt = require("jsonwebtoken");
const app = express.Router();
mongoose.connect(config.mongoUrl, {useNewUrlParser: true, useUnifiedTopology: true});
@ -22,7 +22,7 @@ const createAccountLimiter = rateLimit({
message: "You are being rate limited"
});
app.get('/account/create/info', async (req, res) => {
app.get("/account/create/info", async (req, res) => {
let requiresCode = false;
if (config.restrictions) {
const restrictions = config.restrictions.signup;
@ -33,22 +33,22 @@ app.get('/account/create/info', async (req, res) => {
res.json({
error: false,
message: 'SUCCESS_ACCOUNT_CREATE_INFO_FETCH',
message: "SUCCESS_ACCOUNT_CREATE_INFO_FETCH",
requiresSpecialCode: requiresCode
});
});
app.post('/account/create', [
app.post("/account/create", [
createAccountLimiter,
body('username').not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
body('email').not().isEmpty().isEmail().normalizeEmail(),
body('password').not().isEmpty().isLength({ min: 8, max: 128 }),
body('specialCode').optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
body("username").not().isEmpty().trim().isLength({ min: 3, max: 32 }).isAlphanumeric(),
body("email").not().isEmpty().isEmail().normalizeEmail(),
body("password").not().isEmpty().isLength({ min: 8, max: 128 }),
body("specialCode").optional().isLength({ min: 12, max: 12 }).isAlphanumeric()
], async (req, res) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
return;
}
@ -60,11 +60,11 @@ app.post('/account/create', [
if (passedSpecialCode && specialCode) {
if (specialCode !== passedSpecialCode) {
res.status(401).json({ error: true, message: 'ERROR_REQUEST_SPECIAL_CODE_MISSING', errors: [{ msg: 'No specialCode passed', param: 'specialCode', location: 'body' }] });
res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] });
return false;
}
} else {
res.status(401).json({ error: true, message: 'ERROR_REQUEST_SPECIAL_CODE_MISSING', errors: [{ msg: 'No specialCode passed', param: 'specialCode', location: 'body' }] });
res.status(401).json({ error: true, message: "ERROR_REQUEST_SPECIAL_CODE_MISSING", errors: [{ msg: "No specialCode passed", param: "specialCode", location: "body" }] });
return false;
}
}
@ -74,13 +74,13 @@ app.post('/account/create', [
const existingUser = await User.findByUsername(username);
if (existingUser) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_USERNAME_EXISTS', errors: [{ value: username, msg: 'Username exists', param: 'username', location: 'body' }] });
res.status(400).json({ error: true, message: "ERROR_REQUEST_USERNAME_EXISTS", errors: [{ value: username, msg: "Username exists", param: "username", location: "body" }] });
return;
}
const unhashedPassword = req.body.password;
const email = req.body.email;
const startingRole = 'USER';
const startingRole = "USER";
const hashedPassword = await bcrypt.hash(unhashedPassword, config.bcryptRounds);
@ -94,28 +94,28 @@ app.post('/account/create', [
const userObject = await user.getPublicObject();
console.log('[*] [logger] [users] [create] User created', userObject);
console.log("[*] [logger] [users] [create] User created", userObject);
res.status(200).json({
error: false,
message: 'SUCCESS_USER_CREATED',
message: "SUCCESS_USER_CREATED",
user: userObject
});
} catch (e) {
console.error('Internal server error', e);
res.status(500).json({ error: true, message: 'INTERNAL_SERVER_ERROR' });
console.error("Internal server error", e);
res.status(500).json({ error: true, message: "INTERNAL_SERVER_ERROR" });
return;
}
});
app.post('/token/create', [
app.post("/token/create", [
createAccountLimiter,
body('username').not().isEmpty().trim().isAlphanumeric(),
body('password').not().isEmpty()
body("username").not().isEmpty().trim().isAlphanumeric(),
body("password").not().isEmpty()
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
res.status(400).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
return;
}
@ -123,7 +123,7 @@ app.post('/token/create', [
const existingUser = await User.findByUsername(username);
if (!existingUser) {
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
return;
}
@ -136,45 +136,45 @@ app.post('/token/create', [
passwordCheck = false;
}
if (!passwordCheck) {
res.status(403).json({ error: true, message: 'ERROR_REQUEST_LOGIN_INVALID' });
res.status(403).json({ error: true, message: "ERROR_REQUEST_LOGIN_INVALID" });
return;
}
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: '3h' }, async (err, token) => {
jwt.sign({ username }, secret.jwtPrivateKey, { expiresIn: "3h" }, async (err, token) => {
if (err) {
res.status(500).json({
error: true,
message: 'INTERNAL_SERVER_ERROR'
message: "INTERNAL_SERVER_ERROR"
});
return;
}
// TODO: Ugly fix for setting httponly cookies
if (req.body.alsoSetCookie) {
res.cookie('token', token, {
res.cookie("token", token, {
maxAge: 3 * 60 * 60 * 1000, httpOnly: true, domain: config.address,
});
}
const userObject = await existingUser.getPublicObject();
console.log('[*] [logger] [users] [token create] Token created', userObject);
console.log("[*] [logger] [users] [token create] Token created", userObject);
res.status(200).json({
error: false,
message: 'SUCCESS_TOKEN_CREATED',
message: "SUCCESS_TOKEN_CREATED",
user: userObject,
token
});
});
});
app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
app.get("/current/info", authenticateEndpoint(async (req, res, user) => {
const userObject = await user.getPublicObject();
res.status(200).json({
error: false,
message: 'SUCCESS_USER_DATA_FETCHED',
message: "SUCCESS_USER_DATA_FETCHED",
user: {
token: req.cookies.token, // TODO: Passing the token like this is *terribly* insecure
...userObject
@ -182,12 +182,12 @@ app.get('/current/info', authenticateEndpoint(async (req, res, user) => {
});
}, undefined, 0));
app.get('/user/:userid/info', [
param('userid').not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
], authenticateEndpoint(async (req, res, user) => {
app.get("/user/:userid/info", [
param("userid").not().isEmpty().trim().escape().isLength({ min: 24, max: 24 })
], authenticateEndpoint(async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.status(400).json({ error: true, message: 'ERROR_REQUEST_INVALID_DATA', errors: errors.array() });
res.status(400).json({ error: true, message: "ERROR_REQUEST_INVALID_DATA", errors: errors.array() });
return;
}
@ -195,7 +195,7 @@ app.get('/user/:userid/info', [
if (!userid) {
res.sendStatus(400).json({
error: true,
message: 'ERROR_REQUEST_INVALID_DATA'
message: "ERROR_REQUEST_INVALID_DATA"
});
return;
}
@ -203,13 +203,13 @@ app.get('/user/:userid/info', [
res.status(200).json({
error: false,
message: 'SUCCESS_USER_DATA_FETCHED',
message: "SUCCESS_USER_DATA_FETCHED",
user: await otherUser.getPublicObject(),
});
}, undefined, config.roleMap.USER));
app.post('/browser/token/clear', authenticateEndpoint((req, res, user) => {
res.clearCookie('token');
app.post("/browser/token/clear", authenticateEndpoint((req, res) => {
res.clearCookie("token");
res.sendStatus(200);
}));

15
api/v2/gateway/index.js Normal file
View file

@ -0,0 +1,15 @@
const websockets = require("ws");
class GatewayServer {
constructor({ server }) {
this.wss = new websockets.Server({ server });
this.wss.on("connection", (ws) => {
ws.on("message", (data) => {
});
});
}
}
module.exports = GatewayServer;

View file

@ -0,0 +1,18 @@
const config = require("../../../config");
const opcodes = {
0: "HELLO"
};
const parseMessage = (message) => {
if (typeof message !== "string") throw new Error("GatewayMessageParser: Message is not a string");
if (message.length < 1) throw new Error("GatewayMessageParser: Message has less than 1 character");
if (message.length > config.gatewayMaxStringPayloadLength) throw new Error(`GatewayMessageParser: Message has more than ${config.gatewayMaxStringPayloadLength} characters`);
const op = parseInt(message[0]);
if (!op || isNaN(op)) throw new Error("GatewayMessageParser: Message has invalid opcode");
const opcodeName = opcodes[op];
if (!opcodeName) throw new Error("GatewayMessageParser: Message has unknown opcode");
};
module.exports = { parseMessage };

View file

@ -2,21 +2,24 @@ module.exports = {
ports: {
mainServerPort: 3005,
},
address: 'localhost',
address: "localhost",
//restrictions: {
// signup: {
// specialCode: ''
// }
//},
corsAllowList: [ 'localhost' ],
mongoUrl: 'mongodb://localhost:27017/app',
corsAllowList: [ "http://localhost:3000", "http://localhost:3005" ],
mongoUrl: "mongodb://192.168.0.105:27017/app",
// Internal stuff - only touch if you know what you're doing
bcryptRounds: 10,
roleMap: {
'BANNED': 0,
'RESTRICTED': 1,
'USER': 2,
'BOT': 3,
'ADMIN': 4
"BANNED": 0,
"RESTRICTED": 1,
"USER": 2,
"BOT": 3,
"ADMIN": 4
},
gatewayStillNotConnectedTimeoutMS: 15*1000
gatewayStillNotConnectedTimeoutMS: 15*1000,
gatewayMaxStringPayloadLength:
};

View file

@ -1,13 +1,13 @@
const config = require('./config');
const apiRoute = require('./api/v1');
const config = require("./config");
const apiRoute = require("./api/v1");
const express = require('express');
const cookieParser = require('cookie-parser');
const cors = require('cors')
const http = require('http');
const express = require("express");
const cookieParser = require("cookie-parser");
const cors = require("cors");
const http = require("http");
const { authenticateEndpoint } = require('./api/v1/authfunctions');
const GatewayServer = require('./api/v1/gateway/index');
const { authenticateEndpoint } = require("./api/v1/authfunctions");
const GatewayServer = require("./api/v1/gateway/index");
const app = express();
const httpServer = http.createServer(app);
@ -22,36 +22,36 @@ app.use(cors({
if (config.corsAllowList.indexOf(origin) !== -1 || !origin) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
callback(new Error("Not allowed by CORS"));
}
},
credentials: true,
optionsSuccessStatus: 200
}));
app.use('/api/v1', apiRoute);
app.use(express.static('app'));
app.use("/api/v1", apiRoute);
app.use(express.static("app"));
app.get('/', authenticateEndpoint((req, res, user) => {
res.redirect('/app.html');
}, '/auth.html'));
app.get("/", authenticateEndpoint((req, res) => {
res.redirect("/app.html");
}, "/auth.html"));
app.get('/admin', (req, res) => {
res.send('Keanu chungus wholesome 100 reddit moment 😀i beat up a kid that said minecraft bad 😂and my doggo bit him so i gave him snaccos😉 and we watched pewdiepie together while in elon musks cyber truck 😳talking about how superior reddit memers are : “haha emojis bad” 😲i said and keanu reeves came outta nowhere and said “this is wholesome 100, updoot this wholesome boy” 😗so i got alot of updoots and edit: thanks for the gold kind stranger😣. but the kind stranger revealed himself to be baby yoda eating chiccy nuggies😨 and drinking choccy milk😎 so we went to the cinema to see our (communism funny) favorite movies avengers endgame😆 but then thor played fortnite and fortnite bad😡, so then i said “reality is often dissappointing” and then baby yoda replied r/unexpectedthanos and i replied by r/expectedthanos😖 for balance and then danny devito came to pick us up from the cinema😩 and all the insta normies and gay mods stood watching😵 ,as we,superior redditors went home with danny devito to suck on his magnum dong😫 but i said no homo and started sucking,not like those gay mods😮,then the next morning we woke up to MrBeast telling us to plant 69420 million trees😌, me, baby yoda and danny said nice, and then on our way to plant 69420 million trees😊 (nice) we saw a kid doing a tiktok so keanu reeves appeared and said “we have a kid to burn” and i replied “youre breathtaking”😄 so i said “i need a weapon” and baby yoda gave me an RPG so i blew the kid (DESTRUCTION 100)😎 and posted it on r/memes and r/dankmemes and r/pewdiepiesubmissions and got 1000000000 updoots😘,im sure pewds will give me a big pp, then we shat on emoji users😂😂 and started dreaming about girls that will never like me😢 and posted a lie on r/teenagers about how i got a GF after my doggo died by the hands of fortnite players😳 so i exploited his death for updoots😜, but i watched the sunset with the wholesome gang😁 (keanu,danny,Mrbeast, pewds, spongebob,stefan karl , bob ross, steve irwin, baby yoda and other artists that reddit exploits them) [Everyone liked that] WHOLESOME 100 REDDIT 100🤡');
app.get("/admin", (req, res) => {
res.send("Keanu chungus wholesome 100 reddit moment 😀i beat up a kid that said minecraft bad 😂and my doggo bit him so i gave him snaccos😉 and we watched pewdiepie together while in elon musks cyber truck 😳talking about how superior reddit memers are : “haha emojis bad” 😲i said and keanu reeves came outta nowhere and said “this is wholesome 100, updoot this wholesome boy” 😗so i got alot of updoots and edit: thanks for the gold kind stranger😣. but the kind stranger revealed himself to be baby yoda eating chiccy nuggies😨 and drinking choccy milk😎 so we went to the cinema to see our (communism funny) favorite movies avengers endgame😆 but then thor played fortnite and fortnite bad😡, so then i said “reality is often dissappointing” and then baby yoda replied r/unexpectedthanos and i replied by r/expectedthanos😖 for balance and then danny devito came to pick us up from the cinema😩 and all the insta normies and gay mods stood watching😵 ,as we,superior redditors went home with danny devito to suck on his magnum dong😫 but i said no homo and started sucking,not like those gay mods😮,then the next morning we woke up to MrBeast telling us to plant 69420 million trees😌, me, baby yoda and danny said nice, and then on our way to plant 69420 million trees😊 (nice) we saw a kid doing a tiktok so keanu reeves appeared and said “we have a kid to burn” and i replied “youre breathtaking”😄 so i said “i need a weapon” and baby yoda gave me an RPG so i blew the kid (DESTRUCTION 100)😎 and posted it on r/memes and r/dankmemes and r/pewdiepiesubmissions and got 1000000000 updoots😘,im sure pewds will give me a big pp, then we shat on emoji users😂😂 and started dreaming about girls that will never like me😢 and posted a lie on r/teenagers about how i got a GF after my doggo died by the hands of fortnite players😳 so i exploited his death for updoots😜, but i watched the sunset with the wholesome gang😁 (keanu,danny,Mrbeast, pewds, spongebob,stefan karl , bob ross, steve irwin, baby yoda and other artists that reddit exploits them) [Everyone liked that] WHOLESOME 100 REDDIT 100🤡");
});
app.use((err, req, res, next) => {
console.error('[E] Internal server error', err);
res.status(500).json({ error: true, status: 500, message: 'ERR_INTERNAL_SERVER_ERROR' });
console.error("[E] Internal server error", err);
res.status(500).json({ error: true, status: 500, message: "ERR_INTERNAL_SERVER_ERROR" });
});
const onServerClosing = (evt) => {
gateway.notifyClientsOfUpdate('exit');
const onServerClosing = () => {
gateway.notifyClientsOfUpdate("exit");
process.exit();
};
['exit', 'SIGINT', 'SIGUSR1', 'SIGUSR2', 'uncaughtException', 'SIGTERM'].forEach((eventType) => {
["exit", "SIGINT", "SIGUSR1", "SIGUSR2", "uncaughtException", "SIGTERM"].forEach((eventType) => {
process.on(eventType, onServerClosing.bind(null, eventType));
})
});
httpServer.listen(config.ports.mainServerPort, () => {
console.log(`[*] [server] Main server is listening on port ${config.ports.mainServerPort}`);

View file

@ -1,142 +0,0 @@
const Client = require('./index');
const fetch = require('node-fetch');
const LISTEN_ON = '5fcbf598b39160080e797ad6';
const PREFIX = '::';
const ADMIN_ID = '5fc828ea4e96e00725c17fd7';
const joined = [];
const selected = [];
// https://stackoverflow.com/questions/17619741/randomly-generating-unique-number-in-pairs-multiple-times
const randomPairs = (players) => {
const pairs = [];
while(players.length) {
pairs.push([
pluckRandomElement(players),
pluckRandomElement(players)
]);
}
return pairs;
};
const pluckRandomElement = (array) => {
const i = randomInt(array.length);
return array.splice(i, 1)[0];
};
const randomInt = (limit) => {
return Math.floor(Math.random() * limit);
};
const getRandomUser = (self, count=0) => {
if (count > 3) return joined[0];
count++;
let final;
let chosen = joined[Math.floor(Math.random() * joined.length)];
final = chosen;
if (chosen._id === self._id) return final = getRandomUser(self, count);
if ((selected.indexOf(chosen)) !== 1) return final = getRandomUser(self, count);
if (!final) return final = getRandomUser(self, count);
return final || null;
};
const main = async () => {
const client = new Client('https://b.hippoz.xyz', {
throwErrors: true
});
const res = await fetch(`${client.url}/api/v1/users/token/create`, {
method: 'POST',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'D',
password: 'D'
})
});
const json = await res.json();
if (!res.ok || json.error) throw new Error('Failed to generate token from API endpoint');
await client.setToken(json.token);
await client.gatewayConnect();
client.gateway.on('connect', () => {
const category = client.gateway.subscribeToCategoryChat(LISTEN_ON);
client.gateway.on('message', (e) => {
if (e.author._id === client.user._id) return;
if (!e.content.startsWith(PREFIX)) return;
if (e.category._id !== category) return;
const cmdString = e.content.substring(PREFIX.length);
const cmdFull = cmdString.split(' ');
const cmd = cmdFull[0] || 'NO_CMD';
console.log(cmdFull);
switch (cmd) {
case 'join': {
const existing = joined.findIndex((o) => {
return o._id === e.author._id;
});
if (existing !== -1) {
client.gateway.sendMessage(category, 'Already joined', {
nickAuthor: { username: 'Error' },
destUser: { _id: e.author._id }
});
break;
}
joined.push(e.author);
console.log(`[*] User joined`, e.author);
client.gateway.sendMessage(category, `${e.author.username} joined!`, {
nickAuthor: { username: 'New join!' }
});
break;
}
case 'roll': {
if (e.author._id !== ADMIN_ID) {
client.gateway.sendMessage(category, 'Access denied', {
nickAuthor: { username: 'Error' },
destUser: { _id: e.author._id }
});
break;
}
client.gateway.sendMessage(category, 'Rolling...', {
nickAuthor: { username: 'Woo' }
});
const pairs = randomPairs(joined);
for (const pair of pairs) {
const p1 = pair[0];
const p2 = pair[1];
if (!p1 || !p2) continue;
for (const player of pair) {
client.gateway.sendMessage(category, `${p1.username} with ${p2.username}`, {
nickAuthor: { username: 'Your result' },
destUser: { _id: player._id }
});
}
}
break;
}
}
});
});
};
main();

View file

@ -1,22 +1,22 @@
const mongoose = require('mongoose');
const Post = require('./Post');
const User = require('./User');
const mongoose = require("mongoose");
const Post = require("./Post");
const User = require("./User");
const categorySchema = new mongoose.Schema({
title: String,
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
posts: [Post.schema]
});
categorySchema.method('getPublicObject', function() {
categorySchema.method("getPublicObject", function() {
return {
title: this.title,
creator: this.populate('creator', User.getPulicFields()).creator,
creator: this.populate("creator", User.getPulicFields()).creator,
posts: this.posts,
_id: this._id
}
};
});
const Category = mongoose.model('Category', categorySchema);
const Category = mongoose.model("Category", categorySchema);
module.exports = Category;

View file

@ -1,10 +1,10 @@
const mongoose = require('mongoose');
const mongoose = require("mongoose");
const Post = mongoose.model('Post', {
const Post = mongoose.model("Post", {
title: String,
body: String,
creator: {type: mongoose.Schema.Types.ObjectId, ref: 'User'},
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: 'Category'}
creator: {type: mongoose.Schema.Types.ObjectId, ref: "User"},
categoryId: {type: mongoose.Schema.Types.ObjectId, ref: "Category"}
});
module.exports = Post;

View file

@ -1,6 +1,6 @@
const config = require('../config');
const config = require("../config");
const mongoose = require('mongoose');
const mongoose = require("mongoose");
const userSchema = new mongoose.Schema({
username: String,
@ -10,17 +10,17 @@ const userSchema = new mongoose.Schema({
color: String
});
userSchema.method('getPublicObject', function() {
userSchema.method("getPublicObject", function() {
return {
username: this.username,
permissionLevel: config.roleMap[this.role],
role: this.role,
color: this.color,
_id: this._id
}
};
});
userSchema.method('getFullObject', function() {
userSchema.method("getFullObject", function() {
return {
username: this.username,
password: this.password,
@ -29,29 +29,29 @@ userSchema.method('getFullObject', function() {
role: this.role,
color: this.color,
_id: this._id
}
};
});
const User = mongoose.model('User', userSchema);
const User = mongoose.model("User", userSchema);
// NOTE(hippoz): These are all actually material design colors, taken from https://material-ui.com/customization/color/#playground
const colors = [
'#f44336',
'#e91e63',
'#9c27b0',
'#673ab7',
'#3f51b5',
'#2196f3',
'#03a9f4',
'#00bcd4',
'#009688',
'#4caf50',
'#8bc34a',
'#cddc39',
'#ffeb3b',
'#ffc107',
'#ff9800',
'#ff5722'
"#f44336",
"#e91e63",
"#9c27b0",
"#673ab7",
"#3f51b5",
"#2196f3",
"#03a9f4",
"#00bcd4",
"#009688",
"#4caf50",
"#8bc34a",
"#cddc39",
"#ffeb3b",
"#ffc107",
"#ff9800",
"#ff5722"
];
User.generateColorFromUsername = function(username) {
@ -68,7 +68,7 @@ User.findByUsername = async function(username) {
};
User.getPulicFields = function() {
return 'username role _id color';
return "username role _id color";
};
module.exports = User;

897
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -19,6 +19,10 @@
"jsonwebtoken": "^8.5.1",
"mongoose": "^5.10.0",
"socket.io": "^3.0.1",
"uuid": "^8.3.1"
"uuid": "^8.3.1",
"ws": "^7.4.3"
},
"devDependencies": {
"eslint": "^7.21.0"
}
}

View file

@ -1,10 +1,10 @@
module.exports = {
jwtPrivateKey: 'KEY'
jwtPrivateKey: "KEY"
};
// Set default values
// You shouldn't need to touch this for configuring this
if (module.exports.jwtPrivateKey === 'KEY') {
console.error('[*] [config] jwtPrivateKey was not specified in secret.js. A randomly generated private key will be used instead');
module.exports.jwtPrivateKey = require('crypto').randomBytes(129).toString('base64');
if (module.exports.jwtPrivateKey === "KEY") {
console.error("[*] [config] jwtPrivateKey was not specified in secret.js. A randomly generated private key will be used instead");
module.exports.jwtPrivateKey = require("crypto").randomBytes(129).toString("base64");
}