Spaces:
Sleeping
Sleeping
| ; | |
| const { Worker: ThreadWorker, isMainThread, parentPort, workerData } = require('worker_threads'); | |
| // ============ DATABASE WORKER THREAD ============ | |
| if (!isMainThread) { | |
| const Database = require('better-sqlite3'); | |
| const path = require('path'); | |
| const fs = require('fs'); | |
| const DATA_DIR = workerData.dataDir || "/data"; | |
| if (!fs.existsSync(DATA_DIR)) { | |
| fs.mkdirSync(DATA_DIR, { recursive: true }); | |
| } | |
| const dbPath = path.join(DATA_DIR, 'users.db'); | |
| const db = new Database(dbPath); | |
| db.pragma('journal_mode = WAL'); | |
| db.pragma('synchronous = OFF'); | |
| db.pragma('cache_size = -2000000'); | |
| db.pragma('temp_store = MEMORY'); | |
| db.pragma('mmap_size = 268435456'); | |
| db.exec(` | |
| CREATE TABLE IF NOT EXISTS users ( | |
| id TEXT PRIMARY KEY, | |
| username TEXT UNIQUE NOT NULL, | |
| password_hash TEXT NOT NULL, | |
| name TEXT NOT NULL, | |
| created_at TEXT NOT NULL, | |
| is_login INTEGER DEFAULT 0, | |
| bio TEXT DEFAULT '', | |
| pic_url TEXT DEFAULT '', | |
| date_of_birth TEXT DEFAULT '', | |
| is_profile_public TEXT DEFAULT 'true' | |
| ) | |
| `); | |
| db.exec(`CREATE INDEX IF NOT EXISTS idx_username ON users(username);`); | |
| const stmts = { | |
| getAllUsers: db.prepare('SELECT * FROM users'), | |
| getUserById: db.prepare('SELECT * FROM users WHERE id = ?'), | |
| getUserByUsername: db.prepare('SELECT * FROM users WHERE username = ?'), | |
| insertUser: db.prepare(` | |
| INSERT INTO users ( | |
| id, username, password_hash, name, created_at, is_login, bio, pic_url, date_of_birth, is_profile_public | |
| ) VALUES ( | |
| @id, @username, @password_hash, @name, @created_at, @is_login, @bio, @pic_url, @date_of_birth, @is_profile_public | |
| ) | |
| `), | |
| updateLogin: db.prepare('UPDATE users SET is_login = ? WHERE id = ?'), | |
| updateProfile: db.prepare(` | |
| UPDATE users SET | |
| username = COALESCE(NULLIF(@username, ''), username), | |
| name = COALESCE(NULLIF(@name, ''), name), | |
| bio = COALESCE(NULLIF(@bio, ''), bio), | |
| date_of_birth = COALESCE(NULLIF(@date_of_birth, ''), date_of_birth), | |
| is_profile_public = COALESCE(NULLIF(@is_profile_public, ''), is_profile_public) | |
| WHERE id = @id | |
| `), | |
| updatePhoto: db.prepare('UPDATE users SET pic_url = ? WHERE id = ?'), | |
| getCount: db.prepare('SELECT COUNT(*) as count FROM users') | |
| }; | |
| parentPort.on('message', async (message) => { | |
| const { id, action, data } = message; | |
| try { | |
| let result; | |
| switch (action) { | |
| case 'getAllUsers': | |
| result = stmts.getAllUsers.all(); | |
| break; | |
| case 'getUserById': | |
| result = stmts.getUserById.get(data.userId); | |
| break; | |
| case 'getUserByUsername': | |
| result = stmts.getUserByUsername.get(data.username); | |
| break; | |
| case 'insertUser': | |
| stmts.insertUser.run(data.user); | |
| result = { success: true }; | |
| break; | |
| case 'updateLogin': | |
| stmts.updateLogin.run(data.isLogin ? 1 : 0, data.userId); | |
| result = { success: true }; | |
| break; | |
| case 'updateProfile': | |
| stmts.updateProfile.run({ | |
| id: data.userId, | |
| username: data.username || '', | |
| name: data.name || '', | |
| bio: data.bio || '', | |
| date_of_birth: data.date_of_birth || '', | |
| is_profile_public: data.is_profile_public || 'true' | |
| }); | |
| result = { success: true }; | |
| break; | |
| case 'updatePhoto': | |
| stmts.updatePhoto.run(data.picUrl, data.userId); | |
| result = { success: true }; | |
| break; | |
| case 'batchOperation': | |
| result = processBatch(data.operations); | |
| break; | |
| case 'getCount': | |
| result = stmts.getCount.get().count; | |
| break; | |
| default: | |
| throw new Error(`Unknown action: ${action}`); | |
| } | |
| parentPort.postMessage({ id, success: true, result }); | |
| } catch (error) { | |
| parentPort.postMessage({ id, success: false, error: error.message }); | |
| } | |
| }); | |
| function processBatch(operations) { | |
| const transaction = db.transaction(() => { | |
| for (const op of operations) { | |
| switch (op.action) { | |
| case 'signup': | |
| try { stmts.insertUser.run(op.user); } catch (e) {} | |
| break; | |
| case 'login': | |
| stmts.updateLogin.run(1, op.userId); | |
| break; | |
| case 'logout': | |
| stmts.updateLogin.run(0, op.userId); | |
| break; | |
| case 'updateProfile': | |
| stmts.updateProfile.run({ | |
| id: op.data.id || op.data.userId, | |
| username: op.data.username || '', | |
| name: op.data.name || '', | |
| bio: op.data.bio || '', | |
| date_of_birth: op.data.date_of_birth || '', | |
| is_profile_public: op.data.is_profile_public || 'true' | |
| }); | |
| break; | |
| case 'updatePhoto': | |
| stmts.updatePhoto.run(op.picUrl, op.userId); | |
| break; | |
| } | |
| } | |
| }); | |
| transaction(); | |
| return { processed: operations.length }; | |
| } | |
| return; | |
| } | |
| // ============ MAIN THREAD ============ | |
| const fastify = require('fastify')({ | |
| logger: false, | |
| bodyLimit: 1048576, | |
| keepAliveTimeout: 61000, | |
| connectionTimeout: 61000, | |
| maxRequestsPerSocket: 0, | |
| requestTimeout: 30000 | |
| }); | |
| const cors = require('@fastify/cors'); | |
| const multipart = require('@fastify/multipart'); | |
| const fastifyStatic = require('@fastify/static'); | |
| const crypto = require('crypto'); | |
| const fs = require('fs'); | |
| const fsp = require('fs').promises; | |
| const path = require('path'); | |
| let sharp; | |
| try { sharp = require('sharp'); } catch (e) {} | |
| const MAX_BUCKET_SIZE = 60 * 1024 * 1024 * 1024; | |
| const MAX_USERS = 30000; | |
| const MAX_PHOTO_SIZE_KB = 20; | |
| const MAX_RPS = 15000; | |
| const BATCH_SIZE = 15000; | |
| const HASH = process.env.HASH || 'v3l0st-d3f4ult-h4sh-k3y-2024!@#$%^&*()'; | |
| const SPACE_NAME = process.env.SPACE_NAME || '2'; | |
| const MAX_USERNAME_LENGTH = 200; | |
| const MAX_NAME_LENGTH = 200; | |
| const MAX_PASSWORD_LENGTH = 200; | |
| const MAX_BIO_LENGTH = 1000; | |
| let totalUsers = 0; | |
| let currentRPS = 0; | |
| let rpsWindowStart = Date.now(); | |
| let isSpaceFull_RPS = false; | |
| let isSpaceFull_Users = false; | |
| const DATA_DIR = process.env.DATA_DIR || "/data"; | |
| const PHOTOS_DIR = path.join(DATA_DIR, "photos"); | |
| const STATUS_DIR = process.env.STATUS_DIR || "/status"; | |
| const FULL_FILE = path.join(STATUS_DIR, "full.txt"); | |
| const SIZE_FILE = path.join(STATUS_DIR, "size.txt"); | |
| [DATA_DIR, PHOTOS_DIR, STATUS_DIR].forEach(dir => { | |
| if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); | |
| }); | |
| if (!fs.existsSync(FULL_FILE)) fs.writeFileSync(FULL_FILE, '', 'utf8'); | |
| if (!fs.existsSync(SIZE_FILE)) fs.writeFileSync(SIZE_FILE, '', 'utf8'); | |
| async function getFullSpaces() { | |
| try { | |
| const content = await fsp.readFile(FULL_FILE, 'utf8'); | |
| return content.trim() ? content.trim().split('|') : []; | |
| } catch (e) { return []; } | |
| } | |
| async function addSpaceToFull() { | |
| try { | |
| let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => ''); | |
| content = content.trim(); | |
| let spaces = content ? content.split('|') : []; | |
| if (!spaces.includes(SPACE_NAME)) { | |
| spaces.push(SPACE_NAME); | |
| await fsp.writeFile(FULL_FILE, spaces.join('|'), 'utf8'); | |
| } | |
| } catch (e) {} | |
| } | |
| async function removeSpaceFromFull() { | |
| try { | |
| let content = await fsp.readFile(FULL_FILE, 'utf8').catch(() => ''); | |
| content = content.trim(); | |
| let spaces = content ? content.split('|') : []; | |
| const newSpaces = spaces.filter(s => s !== SPACE_NAME); | |
| if (newSpaces.length !== spaces.length) { | |
| await fsp.writeFile(FULL_FILE, newSpaces.join('|'), 'utf8'); | |
| } | |
| } catch (e) {} | |
| } | |
| async function addSpaceToSize() { | |
| try { | |
| let content = await fsp.readFile(SIZE_FILE, 'utf8').catch(() => ''); | |
| content = content.trim(); | |
| let spaces = content ? content.split('|') : []; | |
| if (!spaces.includes(SPACE_NAME)) { | |
| spaces.push(SPACE_NAME); | |
| await fsp.writeFile(SIZE_FILE, spaces.join('|'), 'utf8'); | |
| } | |
| } catch (e) {} | |
| } | |
| function checkRPSStatus() { | |
| const now = Date.now(); | |
| if (now - rpsWindowStart >= 1000) { | |
| if (currentRPS >= MAX_RPS * 0.9 && !isSpaceFull_RPS) { | |
| isSpaceFull_RPS = true; | |
| addSpaceToFull(); | |
| } else if (currentRPS < MAX_RPS * 0.7 && isSpaceFull_RPS) { | |
| isSpaceFull_RPS = false; | |
| removeSpaceFromFull(); | |
| } | |
| currentRPS = 0; | |
| rpsWindowStart = now; | |
| } | |
| } | |
| async function checkUserCountStatus() { | |
| try { | |
| const count = await dbRequest('getCount'); | |
| if (count >= MAX_USERS && !isSpaceFull_Users) { | |
| isSpaceFull_Users = true; | |
| await addSpaceToSize(); | |
| await addSpaceToFull(); | |
| } | |
| } catch (e) {} | |
| } | |
| let dbWorker; | |
| let dbRequestId = 0; | |
| const dbPromises = new Map(); | |
| function initDBWorker() { | |
| return new Promise((resolve, reject) => { | |
| dbWorker = new ThreadWorker(__filename, { workerData: { dataDir: DATA_DIR } }); | |
| dbWorker.on('message', (message) => { | |
| const { id, success, result, error } = message; | |
| const promise = dbPromises.get(id); | |
| if (promise) { | |
| dbPromises.delete(id); | |
| if (success) promise.resolve(result); | |
| else promise.reject(new Error(error)); | |
| } | |
| }); | |
| dbWorker.on('error', reject); | |
| dbWorker.on('online', resolve); | |
| }); | |
| } | |
| function dbRequest(action, data = {}) { | |
| return new Promise((resolve, reject) => { | |
| const id = ++dbRequestId; | |
| dbPromises.set(id, { resolve, reject }); | |
| dbWorker.postMessage({ id, action, data }); | |
| }); | |
| } | |
| class FastRateLimiter { | |
| constructor(maxRequests = 15000, windowMs = 1000) { | |
| this.maxRequests = maxRequests; | |
| this.windowMs = windowMs; | |
| this.requests = 0; | |
| this.windowStart = Date.now(); | |
| } | |
| isAllowed() { | |
| const now = Date.now(); | |
| if (now - this.windowStart >= this.windowMs) { | |
| this.requests = 0; | |
| this.windowStart = now; | |
| } | |
| if (this.requests >= this.maxRequests) return false; | |
| this.requests++; | |
| currentRPS++; | |
| return true; | |
| } | |
| } | |
| const rateLimiter = new FastRateLimiter(15000, 1000); | |
| function hashPassword(password) { | |
| return crypto.createHash('sha256').update(HASH + password + HASH).digest('hex'); | |
| } | |
| function verifyPassword(password, hash) { | |
| return hashPassword(password) === hash; | |
| } | |
| function generateId() { | |
| return crypto.randomBytes(16).toString('base64url').substring(0, 21); | |
| } | |
| function getBucketSize() { | |
| let totalSize = 0; | |
| try { | |
| const files = fs.readdirSync(PHOTOS_DIR); | |
| for (const file of files) { | |
| try { totalSize += fs.statSync(path.join(PHOTOS_DIR, file)).size; } catch (e) {} | |
| } | |
| try { totalSize += fs.statSync(path.join(DATA_DIR, 'users.db')).size; } catch (e) {} | |
| } catch (e) {} | |
| return totalSize; | |
| } | |
| async function compressToWebp(fileData, maxSizeKb = 20) { | |
| if (!sharp) return fileData; | |
| try { | |
| let image = sharp(fileData); | |
| const metadata = await image.metadata(); | |
| const currentSizeKb = fileData.length / 1024; | |
| if (metadata.format === 'webp' && currentSizeKb <= maxSizeKb) return fileData; | |
| let quality = 80; | |
| let outputBuffer; | |
| while (quality >= 10) { | |
| outputBuffer = await image | |
| .resize(800, 800, { fit: 'inside', withoutEnlargement: true }) | |
| .webp({ quality }) | |
| .toBuffer(); | |
| if (outputBuffer.length / 1024 <= maxSizeKb) break; | |
| quality -= 10; | |
| } | |
| if (outputBuffer.length / 1024 > maxSizeKb) { | |
| outputBuffer = await image | |
| .resize(400, 400, { fit: 'inside', withoutEnlargement: true }) | |
| .webp({ quality: 30 }) | |
| .toBuffer(); | |
| } | |
| return outputBuffer; | |
| } catch (e) { return fileData; } | |
| } | |
| async function setupFastify() { | |
| await fastify.register(cors, { origin: '*' }); | |
| await fastify.register(multipart, { limits: { fileSize: 5 * 1024 * 1024 } }); | |
| await fastify.register(fastifyStatic, { root: PHOTOS_DIR, prefix: '/photos/', maxAge: 86400000 }); | |
| fastify.addHook('onRequest', async (request, reply) => { | |
| if (!rateLimiter.isAllowed()) { | |
| reply.status(503).send({ error: 'rate_limit' }); | |
| return; | |
| } | |
| }); | |
| fastify.post('/signup', async (request, reply) => { | |
| const { username, password, name } = request.body || {}; | |
| if (!username || !password || !name) { | |
| reply.status(400).send({ detail: 'Missing fields' }); | |
| return; | |
| } | |
| const cleanUsername = username.trim().toLowerCase(); | |
| if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) { | |
| reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` }); | |
| return; | |
| } | |
| if (password.length < 6 || password.length > MAX_PASSWORD_LENGTH) { | |
| reply.status(400).send({ detail: `Password must be 6-${MAX_PASSWORD_LENGTH} chars` }); | |
| return; | |
| } | |
| if (name.trim().length > MAX_NAME_LENGTH) { | |
| reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` }); | |
| return; | |
| } | |
| const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername }); | |
| if (existingUser) { | |
| reply.status(409).send({ detail: 'Username exists' }); | |
| return; | |
| } | |
| const count = await dbRequest('getCount'); | |
| if (count >= MAX_USERS) { | |
| reply.status(507).send({ detail: 'Server full' }); | |
| return; | |
| } | |
| const userId = generateId(); | |
| const newUser = { | |
| id: userId, | |
| username: cleanUsername, | |
| password_hash: hashPassword(password), | |
| name: name.trim(), | |
| created_at: new Date().toISOString(), | |
| is_login: 0, | |
| bio: '', | |
| pic_url: '', | |
| date_of_birth: '', | |
| is_profile_public: 'true' | |
| }; | |
| try { | |
| await dbRequest('insertUser', { user: newUser }); | |
| totalUsers = count + 1; | |
| await checkUserCountStatus(); | |
| reply.send({ success: true, user_id: userId, username: cleanUsername }); | |
| } catch (e) { | |
| reply.status(500).send({ detail: 'Signup failed' }); | |
| } | |
| }); | |
| fastify.post('/login', async (request, reply) => { | |
| const { username, password } = request.body || {}; | |
| if (!username || !password) { | |
| reply.status(400).send({ detail: 'Missing fields' }); | |
| return; | |
| } | |
| const cleanUsername = username.trim().toLowerCase(); | |
| const user = await dbRequest('getUserByUsername', { username: cleanUsername }); | |
| if (!user) { | |
| reply.status(401).send({ detail: 'Invalid credentials' }); | |
| return; | |
| } | |
| if (!verifyPassword(password, user.password_hash)) { | |
| reply.status(401).send({ detail: 'Invalid credentials' }); | |
| return; | |
| } | |
| await dbRequest('updateLogin', { userId: user.id, isLogin: true }); | |
| reply.send({ | |
| success: true, | |
| id: user.id, | |
| username: user.username, | |
| name: user.name, | |
| bio: user.bio || '', | |
| pic_url: user.pic_url || '', | |
| date_of_birth: user.date_of_birth || '', | |
| is_profile_public: user.is_profile_public || 'true', | |
| created_at: user.created_at | |
| }); | |
| }); | |
| fastify.post('/logout', async (request, reply) => { | |
| const { id } = request.body || {}; | |
| if (!id) { | |
| reply.status(400).send({ detail: 'Missing ID' }); | |
| return; | |
| } | |
| const user = await dbRequest('getUserById', { userId: id }); | |
| if (!user) { | |
| reply.status(404).send({ detail: 'Not found' }); | |
| return; | |
| } | |
| await dbRequest('updateLogin', { userId: id, isLogin: false }); | |
| reply.send({ success: true }); | |
| }); | |
| fastify.put('/updateprofiledetails', async (request, reply) => { | |
| const { id, username, name, bio, date_of_birth, is_profile_public } = request.body || {}; | |
| if (!id) { | |
| reply.status(400).send({ detail: 'Missing ID' }); | |
| return; | |
| } | |
| const user = await dbRequest('getUserById', { userId: id }); | |
| if (!user) { | |
| reply.status(404).send({ detail: 'Not found' }); | |
| return; | |
| } | |
| if (name && name.length > MAX_NAME_LENGTH) { | |
| reply.status(400).send({ detail: `Name max ${MAX_NAME_LENGTH} chars` }); | |
| return; | |
| } | |
| if (bio && bio.length > MAX_BIO_LENGTH) { | |
| reply.status(400).send({ detail: `Bio max ${MAX_BIO_LENGTH} chars` }); | |
| return; | |
| } | |
| let cleanUsername = ''; | |
| if (username && username.trim() !== '') { | |
| cleanUsername = username.trim().toLowerCase(); | |
| if (cleanUsername.length < 3 || cleanUsername.length > MAX_USERNAME_LENGTH) { | |
| reply.status(400).send({ detail: `Username must be 3-${MAX_USERNAME_LENGTH} chars` }); | |
| return; | |
| } | |
| if (cleanUsername !== user.username) { | |
| const existingUser = await dbRequest('getUserByUsername', { username: cleanUsername }); | |
| if (existingUser) { | |
| reply.status(409).send({ detail: 'Username exists' }); | |
| return; | |
| } | |
| } | |
| } | |
| await dbRequest('updateProfile', { | |
| userId: id, | |
| username: cleanUsername, | |
| name: name || '', | |
| bio: bio !== undefined ? bio : '', | |
| date_of_birth: date_of_birth || '', | |
| is_profile_public: is_profile_public || 'true' | |
| }); | |
| reply.send({ success: true }); | |
| }); | |
| fastify.post('/uploadphoto', async (request, reply) => { | |
| if (getBucketSize() >= MAX_BUCKET_SIZE) { | |
| reply.status(507).send({ detail: 'Storage full' }); | |
| return; | |
| } | |
| try { | |
| const data = await request.file(); | |
| if (!data || !data.mimetype.startsWith('image/')) { | |
| reply.status(400).send({ detail: 'Invalid image' }); | |
| return; | |
| } | |
| const fileBuffer = await data.toBuffer(); | |
| const id = (data.fields && data.fields.id) ? data.fields.id.value : null; | |
| const user = await dbRequest('getUserById', { userId: id }); | |
| if (!id || !user) { | |
| reply.status(404).send({ detail: 'Not found' }); | |
| return; | |
| } | |
| const compressed = await compressToWebp(fileBuffer, MAX_PHOTO_SIZE_KB); | |
| const filename = `${id}_${Math.floor(Date.now() / 1000)}.webp`; | |
| await fsp.writeFile(path.join(PHOTOS_DIR, filename), compressed); | |
| await dbRequest('updatePhoto', { userId: id, picUrl: `/photos/${filename}` }); | |
| reply.send({ success: true }); | |
| } catch (e) { | |
| reply.status(500).send({ detail: 'Failed' }); | |
| } | |
| }); | |
| fastify.get('/getuser', async (request, reply) => { | |
| const { id, username } = request.query || {}; | |
| let user; | |
| if (username) { | |
| user = await dbRequest('getUserByUsername', { username: username.toLowerCase() }); | |
| } else if (id) { | |
| user = await dbRequest('getUserById', { userId: id }); | |
| } else { | |
| reply.status(400).send({ detail: 'Missing params' }); | |
| return; | |
| } | |
| if (!user) { | |
| reply.status(404).send({ detail: 'Not found' }); | |
| return; | |
| } | |
| reply.send({ | |
| id: user.id, | |
| username: user.username, | |
| name: user.name, | |
| bio: user.bio || '', | |
| pic_url: user.pic_url || '', | |
| date_of_birth: user.date_of_birth || '', | |
| is_profile_public: user.is_profile_public || 'true', | |
| created_at: user.created_at, | |
| is_login: Boolean(user.is_login) | |
| }); | |
| }); | |
| fastify.get('/check', async (request, reply) => { | |
| const count = await dbRequest('getCount'); | |
| reply.send({ | |
| status: 'ok', | |
| count: count, | |
| maxUsers: MAX_USERS, | |
| isFull: count >= MAX_USERS, | |
| spaceName: SPACE_NAME | |
| }); | |
| }); | |
| return fastify; | |
| } | |
| async function start() { | |
| try { | |
| await initDBWorker(); | |
| totalUsers = await dbRequest('getCount'); | |
| await setupFastify(); | |
| setInterval(() => { checkRPSStatus(); }, 100); | |
| setInterval(async () => { await checkUserCountStatus(); }, 5000); | |
| const port = process.env.PORT || 7860; | |
| await fastify.listen({ port, host: '0.0.0.0' }); | |
| console.log(`Server running on port ${port}`); | |
| } catch (err) { | |
| console.error('Startup error:', err.message); | |
| process.exit(1); | |
| } | |
| } | |
| process.on('SIGTERM', async () => { | |
| await removeSpaceFromFull(); | |
| await fastify.close(); | |
| if (dbWorker) await dbWorker.terminate(); | |
| process.exit(0); | |
| }); | |
| process.on('SIGINT', async () => { | |
| await removeSpaceFromFull(); | |
| await fastify.close(); | |
| if (dbWorker) await dbWorker.terminate(); | |
| process.exit(0); | |
| }); | |
| start().catch(err => process.exit(1)); |