eirtube/eirtubeMods/ratelimiting.js
2024-12-19 18:49:09 -06:00

137 lines
3.7 KiB
JavaScript
Executable File

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
}
}