130 lines
3.8 KiB
JavaScript
130 lines
3.8 KiB
JavaScript
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);
|
|
}
|
|
});
|
|
}
|