This commit is contained in:
zefie
2025-06-16 13:27:20 -04:00
parent 9c39580a37
commit 1c3cec5f3a

View File

@@ -1,10 +1,11 @@
const net = require('net'); const net = require('net');
const dns = require('dns'); const dns = require('dns');
const { crc16 } = require('easy-crc');
const tls = require('tls'); const tls = require('tls');
const fs = require('fs'); const fs = require('fs');
const { get } = require('http'); const { get } = require('http');
const WTVShared = require('./WTVShared.js').WTVShared; const WTVShared = require('./WTVShared.js').WTVShared;
const crypto = require('crypto');
const { mode } = require('crypto-js');
class WTVIRC { class WTVIRC {
/* /*
@@ -30,7 +31,7 @@ class WTVIRC {
constructor(minisrv_config, host = 'localhost', port = 6667, debug = false) { constructor(minisrv_config, host = 'localhost', port = 6667, debug = false) {
this.minisrv_config = minisrv_config; this.minisrv_config = minisrv_config;
this.wtvshared = new WTVShared(minisrv_config); this.wtvshared = new WTVShared(minisrv_config);
this.version = this.version = '';
this.host = host; this.host = host;
this.port = port; this.port = port;
this.debug = debug; this.debug = debug;
@@ -56,6 +57,8 @@ class WTVIRC {
this.servers = new Map(); // socket -> server information this.servers = new Map(); // socket -> server information
this.serverusers = new Map(); // server -> Set of users connected to this server this.serverusers = new Map(); // server -> Set of users connected to this server
this.reservednicks = []; this.reservednicks = [];
this.hostnames = new Map(); // nickname -> hostname
this.realhosts = new Map(); // nickname -> real IP address
this.uniqueids = new Map(); // nickname -> unique ID mapping this.uniqueids = new Map(); // nickname -> unique ID mapping
this.channelprefixes = ['#','&']; this.channelprefixes = ['#','&'];
this.default_channel_modes = ['n','t']; this.default_channel_modes = ['n','t'];
@@ -179,7 +182,7 @@ class WTVIRC {
secureSocket.timestamp = Date.now(); secureSocket.timestamp = Date.now();
secureSocket.secure = true; secureSocket.secure = true;
secureSocket.uniqueId = `${this.serverId}${parseInt(crc16('CCITT-FALSE', Buffer.from(String(secureSocket.remoteAddress) + String(secureSocket.remotePort), "utf8")).toString(16), 16)}`; secureSocket.uniqueId = `${this.serverId}${this.generateUniqueId(secureSocket)}`;
// Push the secure socket to clients // Push the secure socket to clients
this.clients.push(secureSocket); this.clients.push(secureSocket);
this.clientpeak = Math.max(this.clientpeak, this.clients.length); this.clientpeak = Math.max(this.clientpeak, this.clients.length);
@@ -221,7 +224,7 @@ class WTVIRC {
}); });
socket.timestamp = Date.now(); socket.timestamp = Date.now();
socket.secure = false; socket.secure = false;
socket.uniqueId = `${this.serverId}${parseInt(crc16('CCITT-FALSE', Buffer.from(String(socket.remoteAddress) + String(socket.remotePort), "utf8")).toString(16), 16)}`; socket.uniqueId = `${this.serverId}${this.generateUniqueId(socket)}`;
//socket.write(`:${this.servername} NOTICE AUTH :Welcome to minisrv IRC Server\r\n`); //socket.write(`:${this.servername} NOTICE AUTH :Welcome to minisrv IRC Server\r\n`);
socket.on('data', data => { socket.on('data', data => {
@@ -311,9 +314,38 @@ class WTVIRC {
this.clients = this.clients.filter(c => c !== socket); this.clients = this.clients.filter(c => c !== socket);
this.clientpeak = this.clientpeak - 1; this.clientpeak = this.clientpeak - 1;
socket.servername = serverName; socket.servername = serverName;
socket.serverId = serverId; socket.uniqueId = serverId;
this.servers.set(socket, serverName) this.servers.set(socket, serverName)
socket.write(`SERVER ${this.servername} 1 ${this.serverId} + :${this.irc_motd}\r\n`); socket.write(`SERVER ${this.servername} 1 ${this.serverId} + :${this.irc_motd}\r\n`);
for (const [sock, nickname] of this.nicknames.entries()) {
if (!sock || !nickname) continue;
const uniqueId = sock.uniqueId;
const signonTime = Math.floor(this.usersignontimestamps.get(nickname) || Date.now() / 1000);
const userModes = (this.usermodes.get(nickname) || []).join('');
const username = this.usernames.get(nickname) || '';
socket.write(`:${this.serverId} UID ${nickname} 1 ${signonTime} +${userModes} ${username} ${sock.host} ${sock.realhost} ${sock.remoteAddress} ${uniqueId} * :${sock.userinfo}\r\n`);
}
for (const [channel, users] of this.channels.entries()) {
const modes = this.channelmodes.get(channel) || [];
for (const user of users) {
console.log(`Sending SJOIN for user ${user} in channel ${channel}`);
let userPrefix = '';
if ((this.channelops.get(channel) || new Set()).has(user)) {
userPrefix = '@';
} else if ((this.channelhalfops.get(channel) || new Set()).has(user)) {
userPrefix = '%';
} else if ((this.channelvoices.get(channel) || new Set()).has(user)) {
userPrefix = '+';
}
const userUniqueId = this.uniqueids.get(user);
if (userUniqueId) {
socket.write(`:${this.serverId} SJOIN ${Math.floor(Date.now() / 1000)} ${channel} +${modes.join('')} :${userPrefix}${userUniqueId}\r\n`);
}
}
}
// Send EOB to the server
socket.write(`:${this.serverId} EOB \r\n`);
break;
case 'SVINFO': case 'SVINFO':
// Handle SVINFO command from server // Handle SVINFO command from server
if (parts.length < 4) { if (parts.length < 4) {
@@ -341,6 +373,17 @@ class WTVIRC {
const reservedNick = parts[3]; const reservedNick = parts[3];
const reason = parts.slice(4).join(' ') || ''; const reason = parts.slice(4).join(' ') || '';
this.reservednicks.push(reservedNick); this.reservednicks.push(reservedNick);
if (expiry > 0) {
setTimeout(() => {
const index = this.reservednicks.indexOf(reservedNick);
if (index !== -1) {
this.reservednicks.splice(index, 1);
if (this.debug) {
console.log(`Reservation for ${reservedNick} expired`);
}
}
}, expiry * 1000);
}
break; break;
case 'UID': case 'UID':
// Handle UID command from server // Handle UID command from server
@@ -348,14 +391,14 @@ class WTVIRC {
console.warn('Invalid UID command from server'); console.warn('Invalid UID command from server');
break; break;
} }
const nickname = parts[1]; var nickname = parts[1];
const server_Id = parts[2]; const server_Id = parts[2];
const timestamp = parseInt(parts[3]) || 0; const timestamp = parseInt(parts[3]) || 0;
const userModes = parts[4].replace("/+/","").split(''); const userModes = parts[4].replace("/+/","").split('');
const username = parts[5]; var username = parts[5];
const servername = parts[6]; var hostname = parts[6];
const servername2 = parts[7]; const ipaddress = parts[7];
const address = parts[8]; const ipaddress2 = parts[8];
const userUniqueId = parts[9]; const userUniqueId = parts[9];
var serverUsers = this.serverusers.get(socket) || new Set(); var serverUsers = this.serverusers.get(socket) || new Set();
if (serverUsers === true) { if (serverUsers === true) {
@@ -367,6 +410,8 @@ class WTVIRC {
this.globalpeak = Math.max(this.globalpeak, this.countGlobalUsers()); this.globalpeak = Math.max(this.globalpeak, this.countGlobalUsers());
this.usersignontimestamps.set(nickname, timestamp); this.usersignontimestamps.set(nickname, timestamp);
this.usernames.set(nickname, username); this.usernames.set(nickname, username);
this.hostnames.set(nickname, hostname);
this.realhosts.set(nickname, ipaddress2);
for (const mode of userModes) { for (const mode of userModes) {
var usermodes = this.usermodes.get(nickname) || []; var usermodes = this.usermodes.get(nickname) || [];
if (usermodes === true) { if (usermodes === true) {
@@ -375,18 +420,6 @@ class WTVIRC {
this.usermodes.set(nickname, [...(usermodes), mode]); this.usermodes.set(nickname, [...(usermodes), mode]);
} }
break; break;
case 'EOB':
for (const [sock, nickname] of this.nicknames.entries()) {
if (!sock || !nickname) continue;
const uniqueId = sock.uniqueId;
const signonTime = this.usersignontimestamps.get(nickname) || Math.floor(Date.now() / 1000);
const userModes = (this.usermodes.get(nickname) || []).join('');
const username = this.usernames.get(nickname) || '';
socket.write(`:${socket.uniqueId} UID ${nickname} 1 ${signonTime} +${userModes} ${username} ${sock.host} ${sock.realhost} ${sock.remoteAddress} ${uniqueId} * ${nickname} :${sock.userinfo}\r\n`);
}
// Send EOB to the server
socket.write(`:${this.serverId} EOB \r\n`);
break;
case 'SVSNICK': case 'SVSNICK':
// Handle SVSNICK command from server // Handle SVSNICK command from server
if (parts.length < 5) { if (parts.length < 5) {
@@ -401,6 +434,25 @@ class WTVIRC {
this.processNickChange(targetSocket, newNick); this.processNickChange(targetSocket, newNick);
this.broadcastToAllServers(line, socket); this.broadcastToAllServers(line, socket);
break; break;
case 'SJOIN':
var channel = parts[2];
var modes = parts[3];
var uniqueId = parts[4].slice(1);
if (['@', '%', '+'].includes(uniqueId[0])) {
uniqueId = uniqueId.slice(1);
}
var userSocket = this.findSocketByUniqueId(uniqueId);
var nickname = this.findUserByUniqueId(uniqueId);
var username = this.usernames.get(nickname) || nickname;
var hostname = this.hostnames.get(nickname)
if (!this.channels.has(channel)) {
this.createChannel(channel);
}
if (!this.channels.get(channel).has(nickname)) {
this.channels.get(channel).add(nickname);
}
this.broadcastChannel(channel, `:${nickname}!${username}@${hostname} JOIN ${channel}\r\n`, userSocket);
break;
case 'SQUIT': case 'SQUIT':
this.servers.delete(socket); this.servers.delete(socket);
break; break;
@@ -436,6 +488,9 @@ class WTVIRC {
var whoisHost = parts[4]; var whoisHost = parts[4];
var whoisServer = parts[5]; var whoisServer = parts[5];
var whoisRealname = parts.slice(6).join(' '); var whoisRealname = parts.slice(6).join(' ');
if (whoisRealname.startsWith(':')) {
whoisRealname = whoisRealname.slice(1);
}
targetSocket.write(`:${socket.servername} 311 ${whoisNick} ${whoisNick} ${whoisUser} ${whoisHost} ${whoisServer} :${whoisRealname}\r\n`); targetSocket.write(`:${socket.servername} 311 ${whoisNick} ${whoisNick} ${whoisUser} ${whoisHost} ${whoisServer} :${whoisRealname}\r\n`);
responded = true; responded = true;
break; break;
@@ -445,6 +500,9 @@ class WTVIRC {
var whoisNick = parts[2]; var whoisNick = parts[2];
var serverName = parts[2]; var serverName = parts[2];
var serverInfo = parts.slice(3).join(' '); var serverInfo = parts.slice(3).join(' ');
if (serverInfo.startsWith(':')) {
serverInfo = serverInfo.slice(1);
}
targetSocket.write(`:${socket.servername} 312 ${whoisNick} ${serverName} :${serverInfo}\r\n`); targetSocket.write(`:${socket.servername} 312 ${whoisNick} ${serverName} :${serverInfo}\r\n`);
responded = true; responded = true;
break; break;
@@ -495,7 +553,11 @@ class WTVIRC {
const numericCode = parts[0]; const numericCode = parts[0];
const targetID = parts[1]; const targetID = parts[1];
const senderName = parts[2]; // Remove server ID prefix const senderName = parts[2]; // Remove server ID prefix
const numericMessage = parts.slice(3).join(' '); var numericMessage = parts.slice(3).join(' ');
if (numericMessage.startsWith(':')) {
numericMessage = numericMessage.slice(1); // Remove leading ':'
}
if (!targetSocket) { if (!targetSocket) {
console.warn(`No socket found for uniqueID ${targetID}`); console.warn(`No socket found for uniqueID ${targetID}`);
break; break;
@@ -507,13 +569,17 @@ class WTVIRC {
// part out the line to "sourceUniqueId command targetUniqueId :message" // part out the line to "sourceUniqueId command targetUniqueId :message"
var sourceUniqueId = parts[0].slice(1); // Remove the leading ':' var sourceUniqueId = parts[0].slice(1); // Remove the leading ':'
var srvCommand = parts[1]; var srvCommand = parts[1];
if (srvCommand === 'QUIT') { switch (srvCommand) {
case 'QUIT':
var nick_name = this.findUserByUniqueId(sourceUniqueId); var nick_name = this.findUserByUniqueId(sourceUniqueId);
var user_name = this.usernames.get(nick_name) || nick_name; var user_name = this.usernames.get(nick_name) || nick_name;
for (const [channel, users] of this.channels.entries()) { for (const [channel, users] of this.channels.entries()) {
if (users.has(nick_name)) { if (users.has(nick_name)) {
this.broadcastChannel(channel, `:${nick_name}!${user_name}@${this.servername} QUIT :Remote server disconnected\r\n`); this.broadcastChannel(channel, `:${nick_name}!${user_name}@${this.servername} QUIT :Remote server disconnected\r\n`);
} }
if (this.channels.has(channel) && this.channels.get(channel).size === 0) {
this.deleteChannel(channel);
}
} }
// Remove user from the server's user list // Remove user from the server's user list
const serverUsers = this.serverusers.get(socket); const serverUsers = this.serverusers.get(socket);
@@ -526,39 +592,299 @@ class WTVIRC {
this.usernames.delete(nick_name); this.usernames.delete(nick_name);
this.uniqueids.delete(nick_name); this.uniqueids.delete(nick_name);
break; break;
} else if (srvCommand === "GLOBOPS") { case 'JOIN':
var message = parts.slice(3).join(' '); var channel = parts[3];
this.broadcastToAllServers(`:${sourceUniqueId} GLOBOPS :${message}`, socket); if (!this.channels.has(channel)) {
break; this.createChannel(channel);
} else if (srvCommand === 'PRIVMSG' || srvCommand === 'NOTICE') { }
var targetUniqueId = parts[2]; var userSocket = this.findSocketByUniqueId(sourceUniqueId);
var message = parts.slice(3).join(' '); if (!userSocket) {
var sourceSocket = this.findSocketByUniqueId(sourceUniqueId);
var targetSocket = this.findSocketByUniqueId(targetUniqueId);
if (!sourceSocket) {
console.warn(`No socket found for source unique ID ${sourceUniqueId}`); console.warn(`No socket found for source unique ID ${sourceUniqueId}`);
break; break;
} }
var nickname = this.findUserByUniqueId(sourceUniqueId);
var username = this.usernames.get(nickname) || nickname;
if (!this.channels.get(channel).has(nickname)) {
this.channels.get(channel).add(nickname);
}
this.broadcastChannel(channel, `:${nickname}!${username}@${userSocket.host} JOIN ${channel}\r\n`, userSocket);
break;
case 'PART':
var channel = parts[2];
var nickname = this.findUserByUniqueId(sourceUniqueId);
var username = this.usernames.get(nickname) || nickname;
var hostname = this.hostnames.get(nickname);
this.broadcastChannel(channel, `:${nickname}!${username}@${hostname} PART ${channel} :${parts.slice(4).join(' ')}\r\n`, userSocket);
if (this.channels.has(channel) && this.channels.get(channel).size === 0) {
this.deleteChannel(channel);
}
break;
case 'GLOBOPS':
var message = parts.slice(3).join(' ');
this.broadcastToAllServers(`:${sourceUniqueId} GLOBOPS :${message}`, socket);
break;
case 'TBURST':
// Handle TBURST command from server
if (parts.length < 6) {
console.warn(`Invalid TBURST command from server: ${line}`);
break;
}
var channel = parts[3];
var topic = parts[6];
if (!this.channels.has(channel)) {
this.createChannel(channel);
}
this.channeltopics.set(channel, topic);
var nickname = this.findUserByUniqueId(sourceUniqueId);
this.broadcastChannel(channel, `:${nickname} TOPIC ${channel} :${topic}\r\n`);
break;
case 'MODE':
var targetUniqueId = parts[2];
if (this.channelprefixes.some(prefix => targetUniqueId.startsWith(prefix))) {
// It's a channel, broadcast to all users in the channel
if (this.channels.has(targetUniqueId)) {
var nickname = this.findUserByUniqueId(sourceUniqueId);
var username = this.usernames.get(nickname) || nickname;
var hostname = socket.serverinfo.name;
var modes = parts[3];
// Split modes into array and process each character
let modeChars = modes.split('');
let modeMsg = `:${nickname}!${username}@${hostname} MODE ${targetUniqueId} `;
let addingFlag = false;
let prevAdding = false;
let paramIndex = 4; // Start after the modes
let params = 0;
let flags = [];
for (let j = 0; j < modeChars.length; j++) {
let param = null;
let modeStr = '';
let mc = modeChars[j];
if (mc === '+') {
addingFlag = true;
modeMsg += '+';
prevAdding = true;
continue;
} else if (mc === '-') {
addingFlag = false;
modeMsg += '-';
prevAdding = false;
continue;
}
modeStr += mc;
// Modes that require a parameter
if (['o', 'I', 'b', 'e', 'v', 'h', 'l', 'k'].includes(mc)) {
var plusminus = (addingFlag) ? "+" : "-";
flags.push(plusminus + mc);
params++;
} else {
var channelmodes = this.channelmodes.get(targetUniqueId) || [];
if (channelmodes === true) {
channelmodes = [];
}
if (addingFlag) {
channelmodes.push(mc);
} else {
channelmodes = channelmodes.filter(mode => mode !== mc);
}
this.channelmodes.set(targetUniqueId, channelmodes);
}
if (modeStr.length > 0) {
modeMsg += modeStr;
}
if (param) {
modeMsg += ` ${param}`;
}
}
if (params > 0 && parts.length > paramIndex) {
for (let i = 0; i < params; i++) {
var target = this.findUserByUniqueId(parts[paramIndex]);
if (!target) {
target = parts[paramIndex];
}
if (!target) {
console.warn(`No target found for unique ID ${parts[paramIndex]}`);
break;
}
modeMsg += ` ${target}`;
if (flags[i] === '+o' || flags[i] === '-o') {
var channelOps = this.channelops.get(targetUniqueId) || new Set();
if (channelOps === true) {
channelOps = new Set();
}
if (flags[i] === '+o') {
channelOps.add(target);
} else if (flags[i] === '-o') {
channelOps.delete(target);
}
this.channelops.set(targetUniqueId, channelOps);
} else if (flags[i] === '+h' || flags[i] === '-h') {
var channelHalfOps = this.channelhalfops.get(targetUniqueId) || new Set();
if (channelHalfOps === true) {
channelHalfOps = new Set();
}
if (flags[i] === '+h') {
channelHalfOps.add(target);
} else if (flags[i] === '-h') {
channelHalfOps.delete(target);
}
this.channelhalfops.set(targetUniqueId, channelHalfOps);
} else if (flags[i] === '+v' || flags[i] === '-v') {
var channelVoices = this.channelvoices.get(targetUniqueId) || new Set();
if (channelVoices === true) {
channelVoices = new Set();
}
if (flags[i] === '+v') {
channelVoices.add(target);
} else if (flags[i] === '-v') {
channelVoices.delete(target);
}
this.channelvoices.set(targetUniqueId, channelVoices);
} else if (flags[i] === '+b' || flags[i] === '-b') {
var channelBans = this.channelbans.get(targetUniqueId) || [];
if (channelBans === true) {
channelBans = [];
}
if (flags[i] === '+b') {
channelBans.push(target);
} else if (flags[i] === '-b') {
channelBans = channelBans.filter(ban => ban !== target);
}
this.channelbans.set(targetUniqueId, channelBans);
} else if (flags[i] === '+e' || flags[i] === '-e') {
var channelExemptions = this.channelexemptions.get(targetUniqueId) || [];
if (channelExemptions === true) {
channelExemptions = [];
}
if (flags[i] === '+e') {
channelExemptions.push(target);
} else if (flags[i] === '-e') {
channelExemptions = channelExemptions.filter(exception => exception !== target);
}
this.channelexemptions.set(targetUniqueId, channelExemptions);
} else if (flags[i] === '+I' || flags[i] === '-I') {
var channelInvites = this.channelinvites.get(targetUniqueId) || [];
if (channelInvites === true) {
channelInvites = [];
}
if (flags[i] === '+I') {
channelInvites.push(target);
} else if (flags[i] === '-I') {
channelInvites = channelInvites.filter(invite => invite !== target);
}
this.channelinvites.set(targetUniqueId, channelInvites);
} else if (flags[i] === '+l' || flags[i] === '-l') {
// Check if 'l' mode is already present, if not, add it with the limit
let chan_modes = this.channelmodes.get(targetUniqueId) || [];
if (!chan_modes.some(m => /^l\d+$/.test(m))) {
// Remove any old l modes, then update if it exists, else add new
const limitValue = target;
const existingIndex = chan_modes.findIndex(m => /^l\d+$/.test(m));
if (existingIndex !== -1) {
chan_modes[existingIndex] = `l${limitValue}`;
} else {
chan_modes.push(`l${limitValue}`);
}
this.channelmodes.set(targetUniqueId, chan_modes);
}
} else if (flags[i] === '+k' || flags[i] === '-k') {
let chan_modes = this.channelmodes.get(targetUniqueId) || [];
if (!chan_modes || chan_modes === true) {
chan_modes = [];
}
const keyModeIndex = chan_modes.findIndex(m => typeof m === 'string' && m.startsWith('k '));
if (keyModeIndex !== -1) {
// Update existing key
chan_modes[keyModeIndex] = `k ${target}`;
} else {
// Add new key mode
chan_modes.push(`k ${target}`);
}
this.channelmodes.set(targetUniqueId, chan_modes);
}
paramIndex++;
}
}
modeMsg += '\r\n';
this.broadcastChannel(targetUniqueId, modeMsg);
this.broadcastToAllServers(modeMsg, socket);
}
break;
}
var targetSocket = this.findSocketByUniqueId(targetUniqueId);
if (!targetSocket) { if (!targetSocket) {
console.warn(`No socket found for target unique ID ${targetUniqueId}`); console.warn(`No socket found for target unique ID ${targetUniqueId}`);
break; break;
} }
targetSocket.write(`:${targetSocket.nickname} MODE ${targetSocket.nickname} ${parts.slice(2).join(' ')}\r\n`);
this.broadcastToAllServers(`:${sourceUniqueId} MODE ${targetUniqueId} ${parts.slice(3).join(' ')}\r\n`, socket);
break;
case 'PRIVMSG':
case 'NOTICE':
var targetUniqueId = parts[2];
var message = parts.slice(3).join(' ');
var sourceSocket = this.findSocketByUniqueId(sourceUniqueId);
if (!sourceSocket) {
console.warn(`No socket found for source unique ID ${sourceUniqueId}`);
break;
}
var sourceNickname = this.getUsernameFromUniqueId(sourceUniqueId); var sourceNickname = this.getUsernameFromUniqueId(sourceUniqueId);
var sourceUsername = this.usernames.get(sourceNickname) || sourceNickname; var sourceUsername = this.usernames.get(sourceNickname) || sourceNickname;
if (this.channelprefixes.some(prefix => targetUniqueId.startsWith(prefix))) {
// It's a channel, broadcast to all users in the channel except the source
if (this.channels.has(targetUniqueId)) {
const users = this.channels.get(targetUniqueId);
for (const user of users) {
const userSocket = Array.from(this.nicknames.keys()).find(s => this.nicknames.get(s) === user);
if (userSocket && userSocket.uniqueId !== sourceUniqueId) {
userSocket.write(`:${sourceNickname}!${sourceUsername}@${sourceSocket.host} ${srvCommand} ${targetUniqueId} :${message}\r\n`);
}
}
}
break;
}
var targetSocket = this.findSocketByUniqueId(targetUniqueId);
if (!targetSocket) {
console.warn(`No socket found for target unique ID ${targetUniqueId}`);
break;
}
var targetNickname = this.getUsernameFromUniqueId(targetUniqueId); var targetNickname = this.getUsernameFromUniqueId(targetUniqueId);
if (message.startsWith(':')) { if (message.startsWith(':')) {
message = message.slice(1); // Remove leading ':' message = message.slice(1); // Remove leading ':'
} }
targetSocket.write(`:${sourceNickname}!${sourceUsername}@${sourceSocket.serverinfo.name} ${srvCommand} ${targetNickname} :${message}\r\n`); targetSocket.write(`:${sourceNickname}!${sourceUsername}@${sourceSocket.serverinfo.name} ${srvCommand} ${targetNickname} :${message}\r\n`);
break; break;
} else if (srvCommand === "SVSMODE") { case "SVSJOIN":
if (parts.length < 3) {
console.warn('Invalid SVSJOIN command from server');
break;
}
var targetUniqueId = parts[2];
var channelName = parts[3];
var nickname = this.findUserByUniqueId(targetUniqueId);
var username = this.usernames.get(nickname) || nickname;
var hostname = this.hostnames.get(nickname) || '';
var targetSocket = this.findSocketByUniqueId(targetUniqueId);
if (!this.channels.has(channelName)) {
this.createChannel(channelName);
}
this.channels.get(channelName).add(nickname);
targetSocket.write(`:${nickname}!${username}@${hostname} JOIN ${channelName}\r\n`);
this.broadcastChannel(channelName, `:${nickname}!${username}@${hostname} JOIN ${channelName}\r\n`, targetSocket);
this.broadcastToAllServers(`:${sourceUniqueId} SVSJOIN ${channelName} ${targetUniqueId}\r\n`, socket);
break;
case "SVSMODE":
if (parts.length < 4) { if (parts.length < 4) {
console.warn('Invalid SVSMODE command from server'); console.warn('Invalid SVSMODE command from server');
break; break;
} }
var targetUniqueId = parts[2]; var targetUniqueId = parts[2];
var targetSocket = this.findSocketByUniqueId(targetUniqueId); var targetSocket = this.findSocketByUniqueId(targetUniqueId);
var targetNickname = targetSocket.nickname;
var modes = parts[4].split(''); var modes = parts[4].split('');
let adding = true; let adding = true;
for (const char of modes.join('')) { for (const char of modes.join('')) {
@@ -582,13 +908,12 @@ class WTVIRC {
targetSocket.write(`:${socket.servername} MODE ${targetSocket.nickname} ${modes.join('')}\r\n`); targetSocket.write(`:${socket.servername} MODE ${targetSocket.nickname} ${modes.join('')}\r\n`);
this.broadcastToAllServers(`:${sourceUniqueId} SVSMODE ${targetUniqueId} ${modes.join('')}\r\n`, socket); this.broadcastToAllServers(`:${sourceUniqueId} SVSMODE ${targetUniqueId} ${modes.join('')}\r\n`, socket);
break; break;
} default:
if (this.debug) { if (this.debug) {
console.log(`Unhandled server command from ${sourceUniqueId} to ${targetUniqueId}: ${srvCommand} ${message}`); console.log(`Unhandled server command from ${sourceUniqueId} to ${targetUniqueId}: ${srvCommand} ${message}`);
} }
} }
console.warn(`Unknown server command: ${command}`); }
} }
} }
@@ -638,7 +963,7 @@ class WTVIRC {
// initial commands before we assign socket.isserver = true // initial commands before we assign socket.isserver = true
const serverCommands = ['PASS', 'CAPAB', 'SERVER', 'SVINFO']; const serverCommands = ['PASS', 'CAPAB', 'SERVER', 'SVINFO'];
const firstWord = line.trim().split(' ')[0].toUpperCase(); const firstWord = line.trim().split(' ')[0].toUpperCase();
if (prefix && serverCommands.includes(firstWord)) { if (!prefix && serverCommands.includes(firstWord)) {
this.processServerData(socket, line); this.processServerData(socket, line);
continue; continue;
} }
@@ -895,6 +1220,8 @@ class WTVIRC {
this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} -s\r\n`); this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} -s\r\n`);
} else if (mode.startsWith('+z') || mode.startsWith('-z')) { } else if (mode.startsWith('+z') || mode.startsWith('-z')) {
socket.write(`:${this.servername} 472 ${socket.nickname} ${mode.slice(1)} :is set by the server and cannot be changed\r\n`); socket.write(`:${this.servername} 472 ${socket.nickname} ${mode.slice(1)} :is set by the server and cannot be changed\r\n`);
} else if (mode.startsWith('+r') || mode.startsWith('-r')) {
socket.write(`:${this.servername} 472 ${socket.nickname} ${mode.slice(1)} :is set by the server and cannot be changed\r\n`);
} else if (mode.startsWith('+Z')) { } else if (mode.startsWith('+Z')) {
if (!socket.secure) { if (!socket.secure) {
socket.write(`:${this.servername} 472 ${socket.nickname} ${mode.slice(1)} :You must be secure to set this mode\r\n`); socket.write(`:${this.servername} 472 ${socket.nickname} ${mode.slice(1)} :You must be secure to set this mode\r\n`);
@@ -1201,7 +1528,16 @@ class WTVIRC {
this.channelops.get(ch).add(socket.nickname); this.channelops.get(ch).add(socket.nickname);
} }
this.broadcastUser(socket.nickname, `:${socket.nickname}!${socket.username}@${socket.host} JOIN ${ch}\r\n`, socket); this.broadcastUser(socket.nickname, `:${socket.nickname}!${socket.username}@${socket.host} JOIN ${ch}\r\n`, socket);
this.broadcastToAllServers(`:${socket.uniqueId} JOIN ${ch}\r\n`); let modes = this.channelmodes.get(ch) || [];
let prefix = '';
if ((this.channelops.get(ch) || new Set()).has(socket.nickname)) {
prefix = '@';
} else if ((this.channelhalfops.get(ch) || new Set()).has(socket.nickname)) {
prefix = '%';
} else if ((this.channelvoices.get(ch) || new Set()).has(socket.nickname)) {
prefix = '+';
}
this.broadcastToAllServers(`:${this.serverId} SJOIN ${Math.floor(Date.now() / 1000)} ${ch} +${modes.join('')} :${prefix}${socket.uniqueId}\r\n`);
if (this.channeltopics.has(ch)) { if (this.channeltopics.has(ch)) {
const topic = this.channeltopics.get(ch); const topic = this.channeltopics.get(ch);
socket.write(`:${this.servername} 332 ${socket.nickname} ${ch} :${topic}\r\n`); socket.write(`:${this.servername} 332 ${socket.nickname} ${ch} :${topic}\r\n`);
@@ -1275,6 +1611,7 @@ class WTVIRC {
this.deleteChannel(channel); this.deleteChannel(channel);
} }
} }
this.broadcastToAllServers(`:${socket.uniqueId} PART ${channel}\r\n`);
break; break;
case 'INVITE': case 'INVITE':
if (!socket.registered) { if (!socket.registered) {
@@ -1492,6 +1829,7 @@ class WTVIRC {
continue; continue;
} }
this.broadcastChannel(t, `:${socket.nickname}!${socket.username}@${socket.host} PRIVMSG ${t} :${msg}\r\n`, socket); this.broadcastChannel(t, `:${socket.nickname}!${socket.username}@${socket.host} PRIVMSG ${t} :${msg}\r\n`, socket);
this.broadcastToAllServers(`:${socket.uniqueId} PRIVMSG ${t} :${msg}\r\n`);
} else { } else {
if (this.awaymsgs.has(t)) { if (this.awaymsgs.has(t)) {
socket.write(`:${this.servername} 301 ${socket.nickname} ${t} :${this.awaymsgs.get(t)}\r\n`); socket.write(`:${this.servername} 301 ${socket.nickname} ${t} :${this.awaymsgs.get(t)}\r\n`);
@@ -1512,6 +1850,8 @@ class WTVIRC {
continue; continue;
} }
var targetUserModes = this.usermodes.get(t) || []; var targetUserModes = this.usermodes.get(t) || [];
if (this.usermodes.has(t) && this.usermodes.get(t).includes('r')) {
}
if (targetUserModes.includes('Z') && !socket.secure) { if (targetUserModes.includes('Z') && !socket.secure) {
socket.write(`:${this.servername} 484 ${socket.nickname} ${t} :Cannot send to user (+Z)\r\n`); socket.write(`:${this.servername} 484 ${socket.nickname} ${t} :Cannot send to user (+Z)\r\n`);
continue; continue;
@@ -1669,6 +2009,9 @@ class WTVIRC {
if (usermodes && usermodes.includes('s')) { if (usermodes && usermodes.includes('s')) {
socket.write(`:${this.servername} 671 ${socket.nickname} ${whoisNick} :is using a secure connection\r\n`); socket.write(`:${this.servername} 671 ${socket.nickname} ${whoisNick} :is using a secure connection\r\n`);
} }
if (usermodes && usermodes.includes('r')) {
socket.write(`:${this.servername} 307 ${socket.nickname} ${whoisNick} :is a registered nick\r\n`);
}
var now = Date.now(); var now = Date.now();
var userTimestamp = this.usertimestamps.get(whoisNick) || now; var userTimestamp = this.usertimestamps.get(whoisNick) || now;
var idleTime = Math.floor((now - userTimestamp) / 1000); var idleTime = Math.floor((now - userTimestamp) / 1000);
@@ -1688,7 +2031,7 @@ class WTVIRC {
// Found remote user // Found remote user
const sender_id = this.getUniqueId(socket.nickname); const sender_id = this.getUniqueId(socket.nickname);
const unique_id = this.getUniqueIDForRemoteUser(whoisNick); const unique_id = this.getUniqueIDForRemoteUser(whoisNick);
srvSocket.write(`:${sender_id} WHOIS ${unique_id}\r\n`); srvSocket.write(`:${sender_id} WHOIS :${unique_id}\r\n`);
foundRemote = true; foundRemote = true;
break; break;
} }
@@ -1880,7 +2223,15 @@ class WTVIRC {
}); });
this.nicknames.delete(socket); this.nicknames.delete(socket);
} }
this.broadcastToAllServers(`:${socket.uniqueId} QUIT :Client disconnected\r\n`, socket); var serverSocket = null;
for (const [srvSocket, users] of this.serverusers.entries()) {
if (users && typeof users.has === 'function' && users.has(nickname)) {
// Don't send QUIT to this server, as it owns the user
serverSocket = srvSocket;
continue;
}
}
this.broadcastToAllServers(`:${socket.uniqueId} QUIT :Client disconnected\r\n`, serverSocket);
this.clients = this.clients.filter(c => c !== socket); this.clients = this.clients.filter(c => c !== socket);
if (close) { if (close) {
socket.end(); socket.end();
@@ -2844,7 +3195,6 @@ class WTVIRC {
} }
processNickChange(socket, newNick) { processNickChange(socket, newNick) {
this.addUserUniqueId(socket.nickname, socket.uniqueId);
// Update nickname in all channels // Update nickname in all channels
for (const [ch, users] of this.channels.entries()) { for (const [ch, users] of this.channels.entries()) {
if (users.has(socket.nickname)) { if (users.has(socket.nickname)) {
@@ -2868,13 +3218,20 @@ class WTVIRC {
socket.nickname = newNick; socket.nickname = newNick;
} }
generateUniqueId(socket) {
const timestamp = Date.now();
const randomPart = Math.floor(Math.random() * 1000000);
const uniqueId = `${socket.remoteAddr}-${socket.port}-${timestamp}-${randomPart}`;
const hash = crypto.createHash('sha256').update(uniqueId).digest('hex').slice(0, 6).toUpperCase();
return hash;
}
doLogin(nickname, socket) { doLogin(nickname, socket) {
for (const [srvSocket, serverName] of this.servers.entries()) { for (const [srvSocket, serverName] of this.servers.entries()) {
if (srvSocket) { if (srvSocket) {
// Compose UID message for this client // Compose UID message for this client
const nickname = socket.nickname const nickname = socket.nickname;
const username = socket.username || 'unknown'; const username = socket.username || this.usernames.get(socket.nickname) || socket.nickname;
const host = socket.host || socket.remoteAddress;
const uniqueId = socket.uniqueId; const uniqueId = socket.uniqueId;
const signonTime = Math.floor(socket.timestamp / 1000); const signonTime = Math.floor(socket.timestamp / 1000);
const userModes = (this.usermodes.get(nickname) || []).join(''); const userModes = (this.usermodes.get(nickname) || []).join('');
@@ -2882,7 +3239,9 @@ class WTVIRC {
srvSocket.write(`:${this.serverId} UID ${nickname} 1 ${signonTime} +${userModes} ${username} ${socket.host} ${socket.realhost} ${socket.remoteAddress} ${uniqueId} * ${nickname} :${userinfo}\r\n`); srvSocket.write(`:${this.serverId} UID ${nickname} 1 ${signonTime} +${userModes} ${username} ${socket.host} ${socket.realhost} ${socket.remoteAddress} ${uniqueId} * ${nickname} :${userinfo}\r\n`);
} }
} }
this.addUserUniqueId(nickname, socket.uniqueId);
this.hostnames.set(nickname, socket.host);
this.realhosts.set(nickname, socket.realhost);
socket.write(`:${this.servername} 001 ${nickname} :Welcome to the IRC server, ${nickname}\r\n`); socket.write(`:${this.servername} 001 ${nickname} :Welcome to the IRC server, ${nickname}\r\n`);
socket.write(`:${this.servername} 002 ${nickname} :Your host is ${this.servername}, running version minisrv ${this.minisrv_config.version}\r\n`); socket.write(`:${this.servername} 002 ${nickname} :Your host is ${this.servername}, running version minisrv ${this.minisrv_config.version}\r\n`);
socket.write(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`); socket.write(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`);