const {render} = require("pinski/plugins") const {getToken} = require("../utils/getuser") const constants = require("../utils/constants") const db = require("../utils/db") const crypto = require("crypto") let users = {} let buckets = {} class User { constructor(req) { this.uuid = crypto.randomUUID() this.token = User.getToken(req) this.ip = User.getIP(req) users[this.token] = this users[this.ip] = this } static getToken(req) { return getToken(req) } static getIP(req) { return req.headers["x-forwarded-for"] || req.socket.remoteAddress || null } } class Bucket { constructor() { this.left = constants.server_setup.ratelimiting.max_bucket_size, this.lastModified = Date.now() } drain() { // Refill bucket this.left += Math.floor(((Date.now() - this.lastModified) / 1000) / constants.server_setup.ratelimiting.bucket_refill_rate_seconds) this.left = Math.min(this.left, constants.server_setup.ratelimiting.max_bucket_size) if (this.left > 0) { this.left-- this.lastModified = Date.now() return true } return false } } function authorized(req, doRateLimit, responseHeaders) { if (constants.server_setup.ratelimiting.enabled && doRateLimit) { let bucket = module.exports.getBucket(req) if (!bucket.drain()) return { success: false, message: "ratelimited", timeLeftSeconds: ((bucket.lastModified + (1000 * constants.server_setup.ratelimiting.bucket_refill_rate_seconds)) - Date.now()) / 1000 } } const token = getToken(req, responseHeaders) let out = db.prepare("SELECT * FROM SeenTokens WHERE token = ?").pluck().all(token) return { success: Object.keys(req.headers).some((header) => req.headers[header] == "same-origin" || (header == "referer" && req.headers[header].indexOf("/watch?v=") > -1)) || out.length > 0, message: "auth" } } // Clear users and buckets that have been inactive setInterval(() => { for (const k of Object.keys(buckets)) { const b = buckets[k] if ((Date.now() - b.lastModified) < 1000 * 60 * 30) continue for (const k2 of Object.keys(users)) { const u = users[k2] if (u.uuid == k) delete users[k2] } delete buckets[k] } }, 1000 * 60 * 30) module.exports = { getUser: function(req) { const token = User.getToken(req) if (token && users[token]) { delete users[User.getIP(req)] // If a user has tokens enabled, we don't want their IP address to block other users on the same network return users[token] } const ip = User.getIP(req) if (users[ip]) return users[ip] return new User(req) }, getBucket: function(req) { const user = module.exports.getUser(req) const bucket = buckets[user.uuid] if (bucket) return bucket buckets[user.uuid] = new Bucket() return buckets[user.uuid] }, remove: function(req) { const token = User.getToken(req) if (token && users[token]) { const uuid = users[token].uuid delete users[token] delete users[User.getIP(req)] delete buckets[uuid] return } const ip = User.getIP(req) if (users[ip]) { const uuid = users[ip].uuid delete users[ip] delete buckets[uuid] } }, authorize: function(req, doRateLimit, responseHeaders) { const output = authorized(req, doRateLimit, responseHeaders) if (output.success == false) { if (output.message == "ratelimited") return render(429, "pug/errors/local-rate-limited.pug", { timeLeftSeconds: output.timeLeftSeconds }, { ...responseHeaders, "Retry-After": Math.ceil(output.timeLeftSeconds) }) else if (output.message == "auth") // return render(403, "pug/errors/message-error.pug", { error: new Error("Access denied") }, responseHeaders) return { statusCode: 403, headers: responseHeaders, contentType: "application/json", content: { error: "Access denied" } } } return true } }