Compare commits

..

5 Commits

Author SHA1 Message Date
lamp 3c6a549a5f sanitize socket event types 2025-05-16 01:27:27 -07:00
lamp 85d2189595 fix 2025-03-26 23:14:07 -07:00
lamp bb85b00b9c more limits 2025-03-26 23:09:53 -07:00
lamp 5df9a65ecf fix theme select 2025-03-12 16:25:07 -07:00
lamp cefad7dcdd enforce user data types 2025-03-12 16:13:38 -07:00
8 changed files with 97 additions and 28 deletions
+1 -1
View File
@@ -19,7 +19,7 @@ export function App() {
setUser({...user, ...partial_user});
};
var [theme, _setTheme] = useState(localStorage.theme);
var [theme, _setTheme] = useState(localStorage.theme || "");
var setTheme = theme => { _setTheme(theme); localStorage.theme = theme; }
+1 -4
View File
@@ -1,4 +1,4 @@
work in progress:
unfinished/work in progress:
- message delete & edit
@@ -7,6 +7,3 @@ todo:
- better replies
- mousing not in react?
- fix scrollback issues
- more limits
no users = no motivation
+16
View File
@@ -9,6 +9,7 @@
"busboy": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"hash-through": "^0.1.16",
"mime": "^3.0.0",
"mongodb": "^6.3.0",
@@ -396,6 +397,15 @@
"node": ">= 0.10.0"
}
},
"node_modules/express-async-errors": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz",
"integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==",
"license": "ISC",
"peerDependencies": {
"express": "^4.16.2"
}
},
"node_modules/express/node_modules/cookie": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz",
@@ -1684,6 +1694,12 @@
}
}
},
"express-async-errors": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz",
"integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==",
"requires": {}
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
+1
View File
@@ -4,6 +4,7 @@
"busboy": "^1.6.0",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"express-async-errors": "^3.1.1",
"hash-through": "^0.1.16",
"mime": "^3.0.0",
"mongodb": "^6.3.0",
+1
View File
@@ -1 +1,2 @@
export const DATA_DIR = process.env.DATA_DIR || "data";
export const MIN_FREE_GB = process.env.MIN_FREE_GB ? Number(process.env.MIN_FREE_GB) : 10;
+22 -13
View File
@@ -1,4 +1,5 @@
import express from "express";
import "express-async-errors";
import busboy from "busboy";
import HashThrough from "hash-through";
import { to36 } from "1636";
@@ -9,8 +10,8 @@ import mime from "mime";
import Message from "./models/Message.js";
import Emoji from "./models/Emoji.js";
import { DATA_DIR } from "./constants.js";
import { shuffleArray } from "./util.js";
import { DATA_DIR, MIN_FREE_GB } from "./constants.js";
import { df } from "./util.js";
export var router = express.Router();
@@ -32,7 +33,12 @@ router.get("/messages", (req, res, next) => {
Message.getMessages({query, secret: req.query.secret}).then(messages => res.send(messages)).catch(next);
});
router.post("/upload", (req, res, next) => {
router.post("/upload", async (req, res, next) => {
var free = await df(DATA_DIR);
if (free < MIN_FREE_GB) {
res.sendStatus(507);
return;
}
var boi = busboy({headers: req.headers});
var codes = [];
boi.on("file", (name, file, info) => {
@@ -98,16 +104,19 @@ router.get("/emojis", (req, res, next) => {
});
router.put("/emoji/:name", express.raw({limit: "1mb", type: ()=>true}), async (req, res, next) => {
try {
var emoji = new Emoji({
name: req.params.name,
type: req.query.type,
data: req.body
});
await emoji.save();
res.sendStatus(201);
} catch (error) {next(error)}
router.put("/emoji/:name", express.raw({limit: "1mb", type: ()=>true}), async (req, res) => {
var free = await df(DATA_DIR);
if (free < MIN_FREE_GB) {
res.sendStatus(507);
return;
}
var emoji = new Emoji({
name: req.params.name,
type: req.query.type,
data: req.body
});
await emoji.save();
res.sendStatus(201);
});
+25 -8
View File
@@ -2,6 +2,7 @@ import { Server as SocketIOServer } from "socket.io";
import { randomUUID } from "crypto";
import server from "./server.js";
import Message from "./models/Message.js";
import { Quota } from "./util.js";
export var io = new SocketIOServer(server, {
cors: {origin: "*"},
@@ -11,31 +12,46 @@ export var io = new SocketIOServer(server, {
io.on("connection", socket => {
socket.ip = socket.handshake.headers["x-forwarded-for"]?.split(',')[0] || socket.handshake.address;
console.debug("connection from", socket.ip, "socket id", socket.id);
console.log("connect", socket.ip, socket.id);
socket.quotas = {
user: new Quota(1000, 1000*60*60),
message: new Quota(30, 60000)
};
socket.on("disconnect", reason => {
console.log("disconnect", socket.id, reason);
for (var quota in socket.quotas) {
socket.quotas[quota].destroy();
}
})
socket.on("user", user => {
if (!socket.quotas.user.spend()) return;
if (typeof user != "object") return;
user = {
name: user.name?.trim()?.substring(0,32),
color: user.color?.trim()?.substring(0,32),
website: user.website?.trim()?.substring(0,1000),
uuid: user.uuid?.substring(0,128) || "A"+randomUUID(),
secret: user.secret?.substring(0,128),
name: user.name?.toString().trim().substring(0,32),
color: user.color?.toString().trim().substring(0,32),
website: user.website?.toString().trim().substring(0,1000),
uuid: user.uuid?.toString().substring(0,128) || "A"+randomUUID(),
secret: user.secret?.toString().substring(0,128),
socketid: socket.id,
ip: socket.ip,
agent: socket.handshake.headers["user-agent"]
};
console.debug("user", user);
console.debug("user", JSON.stringify(user));
socket.data.user = user;
broadcastUsers();
});
socket.once("user", async user => {
if (typeof user != "object") return;
//await newMessage({color: "#00FF00", content:`${user.name} connected`});
socket.on("disconnect", () => {
//newMessage({color: "#FF0000", content: `${socket.data.user.name} disconnected`});
broadcastUsers();
});
socket.on("message", message => {
if (!socket.quotas.message.spend()) return;
if (typeof message != "object") return;
newMessage({
content: message.content,
content: message.content?.toString().substring(0,10000),
user: {...socket.data.user}
}).catch(error => {
console.error(socket.id, error.message);
@@ -45,6 +61,7 @@ io.on("connection", socket => {
io.emit("type", socket.id);
});
socket.on("mouse", (x, y) => {
if (typeof x != "number" || typeof y != "number") return;
//socket.broadcast.emit("mouse", x, y, socket.id);
// see own cursor (test)
io.emit("mouse", x, y, socket.id);
+28
View File
@@ -6,3 +6,31 @@ export function shuffleArray(array) {
array[j] = temp;
}
}
export class Quota {
constructor(limit, resetInterval) {
this.limit = limit;
this.count = limit;
this.interval = setInterval(() => {
this.count = this.limit;
}, resetInterval);
}
destroy () {
clearInterval(this.interval);
}
spend() {
return --this.count > 0;
}
}
import { execFile } from "child_process";
import { promisify } from "util";
var exec = promisify(execFile);
var free, ldft;
export async function df(path) {
if (Date.now() - ldft < 1000*60) return free;
ldft = Date.now();
var {stdout} = await exec("df", ["--output=avail", "--block-size=1G", path]);
return free = Number(stdout.toString().match(/\d+/)[0]);
}