const fs = require('fs'); const path = require('path'); const { WebSocketServer } = require('ws'); const PORT = parseInt(process.env.PORT, 10) || 8080; const MAX_FILE_SIZE = parseInt(process.env.MAX_FILE_SIZE, 10) || 10 * 1024 * 1024; const usersPath = path.join(__dirname, process.env.USERS_FILE || 'users.json'); let validUsers; try { validUsers = JSON.parse(fs.readFileSync(usersPath, 'utf-8')); console.log(`Loaded ${validUsers.length} users from users.json`); } catch (err) { console.error('Failed to load users.json:', err.message); process.exit(1); } function authenticate(login, password) { return validUsers.find(u => u.login === login && u.password === password) || null; } const wss = new WebSocketServer({ port: PORT }); console.log(`WebSocket server running on ws://localhost:${PORT}`); wss.on('connection', (ws, req) => { const clientIp = req.socket.remoteAddress; console.log(`New connection from ${clientIp}`); let authenticated = false; let userLogin = null; ws.on('message', (data) => { let parsed; try { parsed = JSON.parse(data.toString()); } catch (e) { ws.send(JSON.stringify({ type: 'error', message: 'Invalid JSON' })); return; } if (!authenticated) { if (parsed.type === 'auth') { const user = authenticate(parsed.login, parsed.password); if (user) { authenticated = true; userLogin = parsed.login; console.log(`User authenticated: ${userLogin}`); ws.send(JSON.stringify({ type: 'auth_result', success: true })); broadcast({ type: 'system', text: `${userLogin} joined the chat` }, null); } else { console.log(`Auth failed for login: ${parsed.login}`); ws.send(JSON.stringify({ type: 'auth_result', success: false, reason: 'Invalid login or password' })); } } else { ws.send(JSON.stringify({ type: 'error', message: 'Authenticate first' })); } return; } if (parsed.type === 'text') { if (!parsed.text || typeof parsed.text !== 'string') { ws.send(JSON.stringify({ type: 'error', message: 'Invalid text message' })); return; } console.log(`Text from ${userLogin}: ${parsed.text.substring(0, 50)}`); broadcast({ type: 'text', from: userLogin, timestamp: Date.now(), text: parsed.text }, null); return; } if (parsed.type === 'file') { if (!parsed.filename || !parsed.mime || !parsed.data) { ws.send(JSON.stringify({ type: 'error', message: 'Invalid file message' })); return; } const size = Math.ceil((parsed.data.length * 3) / 4); if (size > MAX_FILE_SIZE) { ws.send(JSON.stringify({ type: 'error', message: 'File too large (max 10 MB)' })); return; } console.log(`File from ${userLogin}: ${parsed.filename} (${parsed.mime}, ~${size} bytes)`); broadcast({ type: 'file', from: userLogin, timestamp: Date.now(), filename: parsed.filename, mime: parsed.mime, data: parsed.data }, null); return; } ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' })); }); ws.on('close', (code, reason) => { console.log(`Connection closed${userLogin ? ` for ${userLogin}` : ''} (code: ${code})`); if (userLogin) { broadcast({ type: 'system', text: `${userLogin} left the chat` }, null); } }); ws.on('error', (err) => { console.error(`WebSocket error${userLogin ? ` for ${userLogin}` : ''}:`, err.message); }); }); function broadcast(message, excludeWs) { const json = JSON.stringify(message); wss.clients.forEach(client => { if (client !== excludeWs && client.readyState === 1) { client.send(json); } }); }