IPost/server.js
2025-04-29 00:29:00 +02:00

416 lines
12 KiB
JavaScript

import http from 'http'
import express, { Router } from 'express'
import useragent from 'express-useragent'
import fileUpload from 'express-fileupload'
import * as bodyParser from 'body-parser'
import cookieParser from 'cookie-parser'
import * as mysql from 'mysql2'
import * as ws from 'ws'
import getIP from './extra_modules/getip.js'
import { unsign } from './extra_modules/unsign.js'
import { readFileSync, appendFile } from 'fs'
import { format } from 'util'
import { setup as SETUP_ROUTES } from './routes/setup_all_routes.js'
import { verify as verifyHCaptcha_int } from 'hcaptcha'
import { ensureExists } from './extra_modules/ensureExists.js'
import * as compress from 'compression'
const compression = compress.default
import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
const config = JSON.parse(readFileSync('server_config.json'))
const time = Date.now()
const original_log = console.log
/**
* custom logging function
* @param {number} level importance level if information
* @param {any} info information to format + log
* @return {undefined} returns nothing
*/
function log_info(level, ...info) {
let text = info
if (text === undefined || text.length === 0) {
text = level
level = 5
}
if (
config['logs'] &&
config['logs']['level'] &&
config['logs']['level'] >= level
) {
let tolog = `[INFO] [${Date.now()}] : ${format(text)} \n`
original_log(tolog) //still has some nicer colors
ensureExists(__dirname + '/logs/', function (err) {
if (err) {
process.stderr.write(tolog) //just write it to stderr
} else {
appendFile(__dirname + '/logs/' + time, tolog, function (err) {
if (err) {
process.stderr.write(err)
}
})
}
})
}
}
//console.log = log_info;
const hcaptcha_secret = config.hcaptcha_secret
// wrapper for the HCaptcha verify function
function verifyHCaptcha(token) {
return verifyHCaptcha_int(
hcaptcha_secret,
token,
undefined,
config.hcaptcha_sitekey
)
}
const WebSocket = ws.WebSocketServer
const router = Router()
const app = express()
const con = mysql.createPool({
connectionLimit: config.mysql.connections,
host: config.mysql.host,
user: config.mysql.user,
password: readFileSync(config.mysql.password_file).toString(),
multipleStatements: true,
supportBigNumbers: true,
})
const cookiesecret = readFileSync('cookiesecret.txt').toString()
/**
* custom, bad random number generator
* @param {number} seed seed for the number generator, defaults to current timestamp
* @constructor
*/
class RNG {
constructor(seed) {
if (!seed) seed = Date.now()
this.seed = seed
this.random = function (min, max) {
if (!min) min = 0
if (!max) {
max = min
min = 0
}
this.seed += Math.log(Math.abs(Math.sin(this.seed)) * 100)
return Math.abs(Math.sin(this.seed)) * max + min
}
this.rand = function (min, max) {
return Math.floor(this.random(min, max))
}
}
}
const rand = new RNG()
const genstring_characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
const genstring_charactersLength = genstring_characters.length
/**
* generates a semi-random string
* @param {number} length length of string to generate
* @return {string} semi-random string generated
*/
function genstring(length) {
let result = ''
for (let i = 0; i < length; i++) {
result += genstring_characters.charAt(
rand.rand(genstring_charactersLength)
)
}
return result
}
var API_CALLS = {}
var API_CALLS_ACCOUNT = {}
var USER_CALLS = {}
var SESSIONS = {}
var REVERSE_SESSIONS = {}
var INDIVIDUAL_CALLS = {}
/**
* clears current api call list (per IP)
* @return {undefined} returns nothing
*/
function clear_api_calls() {
API_CALLS = {}
}
/**
* clears current api account call list (per account)
* @return {undefined} returns nothing
*/
function clear_account_api_calls() {
API_CALLS_ACCOUNT = {}
}
/**
* clears current user file call list (per IP)
* @return {undefined} returns nothing
*/
function clear_user_calls() {
USER_CALLS = {}
}
setInterval(clear_api_calls, config.rate_limits.api.reset_time)
setInterval(clear_account_api_calls, config.rate_limits.api.reset_time)
setInterval(clear_user_calls, config.rate_limits.user.reset_time)
function increaseIndividualCall(url, req) {
let conf = config['rate_limits']['individual'][url]
if (!conf) {
//if(!url.startsWith("/avatars/")) //ignore avatars /* DEBUG: inidividual ratelimiters */
//console.log(5, "url not in individual ratelimiter", url); /* DEBUG: inidividual ratelimiters */
return true
}
if (!conf['enabled']) return true
let ip = getIP(req)
if (INDIVIDUAL_CALLS[ip] === undefined) INDIVIDUAL_CALLS[ip] = {}
if (INDIVIDUAL_CALLS[ip][url] === undefined) INDIVIDUAL_CALLS[ip][url] = 0
if (INDIVIDUAL_CALLS[ip][url] === 0) {
setTimeout(function () {
INDIVIDUAL_CALLS[ip][url] = 0
}, conf['reset_time'])
}
INDIVIDUAL_CALLS[ip][url]++
if (INDIVIDUAL_CALLS[ip][url] >= conf['max']) {
console.log(
3,
'ratelimiting someone on',
url,
INDIVIDUAL_CALLS[ip][url],
conf['max'],
ip
)
return false
}
return true
}
function increaseAccountAPICall(req, res) {
let cookie = req.cookies.AUTH_COOKIE
if (!cookie) {
return true
}
let unsigned = unsign(cookie, req, res)
if (!unsigned) {
return true //if there's no account, why not just ignore it
}
unsigned = decodeURIComponent(unsigned)
if (!unsigned) return false
let values = unsigned.split(' ')
let username = values[0]
if (API_CALLS_ACCOUNT[username] === undefined)
API_CALLS_ACCOUNT[username] = 0
if (API_CALLS_ACCOUNT[username] >= config.rate_limits.api.max_per_account) {
res.status(429)
res.send('You are sending way too many api calls!')
return false
}
return true
}
function increaseAPICall(req, res, next) {
let ip = getIP(req)
if (API_CALLS[ip] === undefined) API_CALLS[ip] = 0
if (API_CALLS[ip] >= config.rate_limits.api.max_without_session) {
if (
REVERSE_SESSIONS[ip] &&
req.cookies.session !== REVERSE_SESSIONS[ip]
) {
//expected a session, but didn't get one
res.status(429)
res.send('You are sending way too many api calls!')
return
}
if (!req.cookies.session) {
let session
do {
session = genstring(300)
} while (SESSIONS[session] !== undefined)
SESSIONS[session] = ip
REVERSE_SESSIONS[ip] = session
setTimeout(function () {
SESSIONS[session] = undefined
REVERSE_SESSIONS[ip] = undefined
}, 50000)
res.cookie('session', session, {
maxAge: 100000,
httpOnly: true,
secure: true,
})
console.log(3, 'sending session to ' + ip)
}
}
if (API_CALLS[ip] >= config.rate_limits.api.max_with_session) {
res.status(429)
res.send('You are sending too many api calls!')
console.log(3, 'rate limiting ' + ip)
return false
}
API_CALLS[ip]++
if (!increaseAccountAPICall(req, res)) return false //can't forget account-based ratelimits
if (next) next()
return true
}
function increaseUSERCall(req, res, next) {
let ip = getIP(req)
if (USER_CALLS[ip] === undefined) USER_CALLS[ip] = 0
if (USER_CALLS[ip] >= config.rate_limits.user.max) {
res.status(429)
res.send('You are sending too many requests!')
console.log(2, 'rate limiting ' + ip)
return false
}
USER_CALLS[ip]++
if (next) next()
return true
}
console.log(5, 'loading routes')
app.use(useragent.express())
app.use(
fileUpload({
limits: {
files: 5,
fileSize: 1_000_000,
},
})
)
app.use((_req, res, next) => {
res.set('x-powered-by', 'ipost')
res.set('X-Frame-Options', 'DENY')
res.set('X-XSS-Protection', '1; mode=block')
res.set('X-Content-Type-Options', 'nosniff')
res.set('Referrer-Policy', 'no-referrer')
next()
})
app.use(bodyParser.default.json({ limit: '100mb' }))
app.use(bodyParser.default.urlencoded({ limit: '100mb', extended: true }))
app.use(cookieParser(cookiesecret))
app.use(compression())
let blocked_headers = [
'HTTP_VIA',
'HTTP_X_FORWARDED_FOR',
'HTTP_FORWARDED_FOR',
'HTTP_X_FORWARDED',
'HTTP_FORWARDED',
'HTTP_CLIENT_IP',
'HTTP_FORWARDED_FOR_IP',
'VIA',
'X_FORWARDED_FOR',
'FORWARDED_FOR',
'X_FORWARDED',
'FORWARDED',
'CLIENT_IP',
'FORWARDED_FOR_IP',
'HTTP_PROXY_CONNECTION',
]
if (!config.disallow_proxies_by_headers) {
blocked_headers = []
}
app.use(function (_req, res, next) {
res.set('X-XSS-Protection', '1; mode=block')
next()
})
app.use('/*path', function (req, res, next) {
for (let i = 0; i < blocked_headers.length; i++) {
if (req.header(blocked_headers[i]) !== undefined) {
res.json({ error: "we don't allow proxies on our website." })
return
}
}
let fullurl = req.baseUrl + req.path
if (fullurl !== '/') {
fullurl = fullurl.substring(0, fullurl.length - 1)
}
if (!increaseIndividualCall(fullurl, req)) {
res.status(429)
res.json({ error: 'you are sending too many requests!' })
return
}
next()
})
console.log(5, 'finished loading user routes, starting with api routes')
/*
START /API/*
*/
var wss
var commonfunctions = {
increaseAPICall,
increaseUSERCall,
increaseAccountAPICall,
increaseIndividualCall,
wss,
genstring,
ensureExists,
dirname: __dirname,
config,
hcaptcha: {
verify: verifyHCaptcha,
sitekey: config.hcaptcha_sitekey,
},
}
SETUP_ROUTES(router, con, commonfunctions)
router.get('/api/getChannels', function (_req, res) {
res.set('Access-Control-Allow-Origin', '*')
let sql = `select post_receiver_name from ipost.posts where post_is_private = '0' group by post_receiver_name;`
con.query(sql, [], function (err, result) {
if (err) throw err
res.json(result)
})
/* #swagger.security = [{
"appTokenAuthHeader": []
}] */
})
/*
END /API/*
*/
console.log(5, 'finished loading routes')
app.use(router)
const httpServer = http.createServer(app)
httpServer.listen(config['ports']['http'], function () {
console.log(5, 'HTTP Server is listening')
})
wss = new WebSocket({
server: httpServer,
perMessageDeflate: {
zlibDeflateOptions: {
chunkSize: 1024,
memLevel: 7,
level: 3,
},
zlibInflateOptions: {
chunkSize: 10 * 1024,
},
clientNoContextTakeover: true,
serverNoContextTakeover: true,
serverMaxWindowBits: 10,
concurrencyLimit: 10,
threshold: 1024 * 16,
},
})
wss.on('connection', function connection(ws) {
ws.channel = 'everyone'
console.log(5, 'new connection')
ws.on('message', function incoming(message) {
message = JSON.parse(message)
if (message.id === 'switchChannel') {
ws.channel = decodeURIComponent(message.data)
}
})
})
commonfunctions.wss = wss
console.log(5, 'starting up all services')