From d43f7dc213fd48ec61cebab3b8f38e883af94863 Mon Sep 17 00:00:00 2001 From: zefie Date: Wed, 18 Jun 2025 14:49:51 -0400 Subject: [PATCH] throttle some stuff for kvirc tls, fix hostnames --- zefie_wtvp_minisrv/includes/classes/WTVIRC.js | 221 +++++++++--------- 1 file changed, 115 insertions(+), 106 deletions(-) diff --git a/zefie_wtvp_minisrv/includes/classes/WTVIRC.js b/zefie_wtvp_minisrv/includes/classes/WTVIRC.js index d9988aea..70560a47 100644 --- a/zefie_wtvp_minisrv/includes/classes/WTVIRC.js +++ b/zefie_wtvp_minisrv/includes/classes/WTVIRC.js @@ -100,7 +100,7 @@ class WTVIRC { this.supported_user_modes = "BRZciorswxz"; this.supported_prefixes = ["ohv", "@%+"]; this.supported_client_caps = ['chghost', 'away-notify', 'echo-message', 'invite-notify', 'multi-prefix', 'userhost-in-names', 'account-notify', 'extended-join']; - this.supported_capabilities = ['TBURST', 'EOB', 'IE', 'EX']; + this.supported_server_caps = ['TBURST', 'EOB', 'IE', 'EX']; this.caps = [ `AWAYLEN=${this.awaylen} CASEMAPPING=rfc1459 BOT=B CHANMODES=${this.supported_channel_modes} CHANNELLEN=${this.channellen} CHANTYPES=${this.channelprefixes.join('')} PREFIX=(${this.supported_prefixes[0]})${this.supported_prefixes[1]} USERMODES=${this.supported_user_modes} MAXLIST=b:${this.maxbans},e:${this.maxexcept},i:${this.maxinvite},k:${this.maxkeylen},l:${this.maxlimit}`, `CHARSET=ascii MODES=3 EXCEPTS=e INVEX=I NETWORK=${this.network} CHANLIMIT=${this.channelprefixes.join('')}:${this.channellimit} NICKLEN=${this.nicklen} TOPICLEN=${this.topiclen} KICKLEN=${this.kicklen}` @@ -123,7 +123,7 @@ class WTVIRC { } } this.server_start_time = this.getDate(); - this.server = net.createServer(async (socket) => { + this.server = net.createServer(async socket => { // Detect SSL handshake and wrap socket if needed socket.once('data', async firstChunk => { this.totalConnections++; @@ -158,6 +158,9 @@ class WTVIRC { socket.push(firstChunk); secureSocket.on('error', (err) => { + if (this.debug) { + console.error('Secure socket error:', err); + } this.terminateSession(secureSocket, true); }); @@ -227,7 +230,7 @@ class WTVIRC { console.log('<', ...log_args); return originalWrite.apply(socket, args); }; - } + } socket.registered = false; socket.nickname = ''; socket.username = ''; @@ -239,7 +242,7 @@ class WTVIRC { socket.upgrading_to_tls = false; socket.client_version = ''; socket.client_caps = []; - socket.host = this.filterHostname(socket, socket.remoteAddress); + this.filterHostname(socket, socket.remoteAddress); socket.timestamp = this.getDate(); socket.secure = false; socket.uniqueId = `${this.serverId}${this.generateUniqueId(socket)}`; @@ -328,7 +331,7 @@ class WTVIRC { } var output_reply = []; for (const cap of capabilities) { - if (this.supported_capabilities.includes(cap)) { + if (this.supported_server_caps.includes(cap)) { output_reply.push(cap); } else { if (this.debug) { @@ -566,8 +569,8 @@ class WTVIRC { } var oldNick = this.findUserByUniqueId(parts[1]); var newNick = parts[3]; - var targetSocket = this.findSocketByUniqueId(parts[1]); - this.broadcastUser(oldNick, `:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} NICK :${newNick}\r\n`); + var targetSocket = this.findSocketByUniqueId(parts[1]); + this.broadcastUser(oldNick, `:${oldNick}!${this.usernames.get(oldNick)}@${targetSocket.host} NICK :${newNick}\r\n`); this.processNickChange(targetSocket, newNick); this.broadcastToAllServers(line, socket); break; @@ -1108,7 +1111,7 @@ class WTVIRC { for (const user of users) { const userSocket = Array.from(this.nicknames.keys()).find(s => this.nicknames.get(s) === user); if (userSocket && userSocket.uniqueId !== sourceUniqueId) { - await this.sendThrottled(userSocket, [`:${nickname}!${sourceUsername}@${sourceSocket.host} ${srvCommand} ${targetUniqueId} :${message}`], 30); + await this.sendThrottled(userSocket, [`:${nickname}!${sourceUsername}@${sourceSocket.host} ${srvCommand} ${targetUniqueId} :${message}\r\n`], 30); this.broadcastToAllServers(`:${sourceUniqueId} ${srvCommand} ${targetUniqueId} :${message}\r\n`, socket); } } @@ -1129,7 +1132,7 @@ class WTVIRC { if (this.clientIsWebTV(targetSocket)) { srvCommand = 'PRIVMSG'; } - await this.sendThrottled(targetSocket, [`:${nickname}!${sourceUsername}@${sourceSocket.host} ${srvCommand} ${targetNickname} :${message}`], 30); + await this.sendThrottled(targetSocket, [`:${nickname}!${sourceUsername}@${sourceSocket.host} ${srvCommand} ${targetNickname} :${message}\r\n`], 30); this.broadcastToAllServers(`:${sourceUniqueId} ${srvCommand} ${targetUniqueId} :${message}\r\n`, socket); break; case "WHOIS": @@ -1157,9 +1160,10 @@ class WTVIRC { whoisNick = whoisSocket.nickname; const whois_username = this.usernames.get(whoisNick); var userinfo = this.userinfo.get(whoisNick) || whoisSocket.userinfo || ''; - socket.write(`:${this.serverId} 311 ${targetUniqueId} ${whoisNick} ${whois_username} ${whoisSocket.host} * :${userinfo}\r\n`); + var output_lines = []; + output_lines.push(`:${this.serverId} 311 ${targetUniqueId} ${whoisNick} ${whois_username} ${whoisSocket.host} * :${userinfo}\r\n`); if (this.awaymsgs.has(whoisNick)) { - socket.write(`:${this.serverId} 301 ${targetUniqueId} ${whoisNick} :${this.awaymsgs.get(whoisNick)}\r\n`); + output_lines.push(`:${this.serverId} 301 ${targetUniqueId} ${whoisNick} :${this.awaymsgs.get(whoisNick)}\r\n`); } const userChannels = []; for (const [ch, users] of this.channels.entries()) { @@ -1179,26 +1183,27 @@ class WTVIRC { userChannels.push(prefix + ch); } } - socket.write(`:${this.serverId} 312 ${targetUniqueId} ${whoisNick} :${this.servername} :minisrv-${this.minisrv_config.version}\r\n`); + output_lines.push(`:${this.serverId} 312 ${targetUniqueId} ${whoisNick} :${this.servername} :minisrv-${this.minisrv_config.version}\r\n`); if (this.isIRCOp(whoisNick)) { - socket.write(`:${this.serverId} 313 ${targetUniqueId} ${whoisNick} :is an IRC operator\r\n`); + output_lines.push(`:${this.serverId} 313 ${targetUniqueId} ${whoisNick} :is an IRC operator\r\n`); } if (usermodes && this.getUserModes(whoisNick).includes('s')) { - socket.write(`:${this.serverId} 671 ${targetUniqueId} ${whoisNick} :is using a secure connection\r\n`); + output_lines.push(`:${this.serverId} 671 ${targetUniqueId} ${whoisNick} :is using a secure connection\r\n`); } if (usermodes && this.getUserModes(whoisNick).includes('r')) { - socket.write(`:${this.serverId} 307 ${targetUniqueId} ${whoisNick} :is a registered nick\r\n`); - } + output_lines.push(`:${this.serverId} 307 ${targetUniqueId} ${whoisNick} :is a registered nick\r\n`); + } var now = this.getDate(); var userTimestamp = this.usertimestamps.get(whoisNick) || now; var idleTime = now - userTimestamp; - socket.write(`:${this.serverId} 317 ${targetUniqueId} ${whoisNick} ${idleTime} ${this.usersignontimestamps.get(whoisNick) || 0} :seconds idle, signon time\r\n`); + output_lines.push(`:${this.serverId} 317 ${targetUniqueId} ${whoisNick} ${idleTime} ${this.usersignontimestamps.get(whoisNick) || 0} :seconds idle, signon time\r\n`); if (userChannels.length > 0) { - socket.write(`:${this.serverId} 319 ${targetUniqueId} ${whoisNick} :${userChannels.join(' ')}\r\n`); + output_lines.push(`:${this.serverId} 319 ${targetUniqueId} ${whoisNick} :${userChannels.join(' ')}\r\n`); } - socket.write(`:${this.serverId} 318 ${targetUniqueId} ${whoisNick} :End of /WHOIS list\r\n`); - } - break; + output_lines.push(`:${this.serverId} 318 ${targetUniqueId} ${whoisNick} :End of /WHOIS list\r\n`); + await this.sendThrottled(socket, output_lines); + } + break; case "SVSJOIN": if (parts.length < 3) { if (this.debug) { @@ -1343,7 +1348,6 @@ class WTVIRC { }; } socket.removeAllListeners('error'); - secureSocket.setEncoding('ascii'); secureSocket.registered = socket.registered; secureSocket.nickname = socket.nickname secureSocket.username = socket.username @@ -1382,7 +1386,7 @@ class WTVIRC { // Ensure data is a string if (typeof data !== 'string') { if (Buffer.isBuffer(data)) { - data = data.toString('utf8'); + data = data.toString('ascii'); } else if (data && typeof data.toString === 'function') { data = data.toString(); } else { @@ -2171,11 +2175,13 @@ class WTVIRC { } } var users = this.getUsersInChannel(ch); + var output_lines = []; + var prefixRegex = new RegExp(`^[${this.supported_prefixes[1]}]`); if (users.length > 0) { users.sort((a, b) => { - // Remove any prefixes for comparison - const cleanA = a.replace(/^[@%+]/, ''); - const cleanB = b.replace(/^[@%+]/, ''); + // Remove any prefixes for comparison + const cleanA = a.replace(prefixRegex, ''); + const cleanB = b.replace(prefixRegex, ''); // Get privilege for each user const ops = this.channelops.get(ch) || new Set(); const halfops = this.channelhalfops.get(ch) || new Set(); @@ -2194,17 +2200,18 @@ class WTVIRC { }); if (socket.client_caps.includes('userhost-in-names')) { const userHosts = users.map(user => { - var nick = this.findUser(user.replace(/^[@%+]/, '')); + var nick = this.findUser(user.replace(prefixRegex, '')); var username = this.usernames.get(nick) || 'unknown'; var host = this.hostnames.get(nick) || 'unknown'; return `${user}!${username}@${host}`; }); - socket.write(`:${this.servername} 353 ${socket.nickname} = ${ch} :${userHosts.join(' ')}\r\n`); + output_lines.push(`:${this.servername} 353 ${socket.nickname} = ${ch} :${userHosts.join(' ')}\r\n`); } else { - socket.write(`:${this.servername} 353 ${socket.nickname} = ${ch} :${users.join(' ')}\r\n`); + output_lines.push(`:${this.servername} 353 ${socket.nickname} = ${ch} :${users.join(' ')}\r\n`); } } - socket.write(`:${this.servername} 366 ${socket.nickname} ${ch} :End of /NAMES list\r\n`); + output_lines.push(`:${this.servername} 366 ${socket.nickname} ${ch} :End of /NAMES list\r\n`); + this.sendThrottled(socket, output_lines); if (this.isReservedChannel(ch)) { if (this.checkIfReservedChannelOp(socket, ch)) { if (!this.channelops.has(ch) || this.channelops.get(ch) === true) { @@ -2235,20 +2242,23 @@ class WTVIRC { break; } var users = this.getUsersInChannel(channel); + var output_lines = []; if (users.length > 0) { if (socket.client_caps.includes('userhost-in-names')) { const userHosts = users.map(user => { - var nick = this.findUser(user.replace(/^[@%+]/, '')); + var prefixRegex = new RegExp(`^[${this.supported_prefixes[1]}]`); + var nick = this.findUser(user.replace(prefixRegex, '')); var username = this.usernames.get(nick) || 'unknown'; var host = this.hostnames.get(nick) || 'unknown'; return `${user}!${username}@${host}`; }); - socket.write(`:${this.servername} 353 ${socket.nickname} = ${ch} :${userHosts.join(' ')}\r\n`); + output_lines.push(`:${this.servername} 353 ${socket.nickname} = ${ch} :${userHosts.join(' ')}\r\n`); } else { - socket.write(`:${this.servername} 353 ${socket.nickname} = ${ch} :${users.join(' ')}\r\n`); + output_lines.push(`:${this.servername} 353 ${socket.nickname} = ${ch} :${users.join(' ')}\r\n`); } } - socket.write(`:${this.servername} 366 ${socket.nickname} ${channel} :End of /NAMES list\r\n`); + output_lines.push(`:${this.servername} 366 ${socket.nickname} ${channel} :End of /NAMES list\r\n`); + this.sendThrottled(socket, output_lines); break; case 'PART': if (!socket.registered) { @@ -2457,6 +2467,7 @@ class WTVIRC { socket.write(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); } else { // WHO for nickname + var output_lines = []; if (target.includes('*') || target.includes('?')) { // Wildcard mask search for nicknames const maskRegex = new RegExp('^' + target.replace(/\*/g, '.*').replace(/\?/g, '.') + '$', 'i'); @@ -2467,13 +2478,14 @@ class WTVIRC { continue; } found = true; - socket.write(`:${this.servername} 352 ${socket.nickname} * ${nick} ${sock.host} ${this.servername} ${nick} ${(this.awaymsgs.has(nick)) ? 'G' : 'H'}${(sock.secure) ? 'z' : ''} :0 ${sock.userinfo}\r\n`); + output_lines.push(`:${this.servername} 352 ${socket.nickname} * ${nick} ${sock.host} ${this.servername} ${nick} ${(this.awaymsgs.has(nick)) ? 'G' : 'H'}${(sock.secure) ? 'z' : ''} :0 ${sock.userinfo}\r\n`); } } if (!found) { - socket.write(`:${this.servername} 401 ${socket.nickname} ${target} :No such nick/channel\r\n`); + output_lines.push(`:${this.servername} 401 ${socket.nickname} ${target} :No such nick/channel\r\n`); } - socket.write(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); + output_lines.push(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); + this.sendThrottled(socket, output_lines); break; } else { var whoisSocket = Array.from(this.nicknames.keys()).find( @@ -2482,14 +2494,15 @@ class WTVIRC { if (whoisSocket) { if (this.getUserModes(whoisSocket.nickname).includes('s')) { // Skip invisible users - socket.write(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); + output_lines.push(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); break; } - socket.write(`:${this.servername} 352 ${socket.nickname} * ${whoisSocket.nickname} ${whoisSocket.host} ${this.servername} ${whoisSocket.nickname} ${(this.awaymsgs.has(target)) ? 'G' : 'H'}${(whoisSocket.secure) ? 'z' : ''} :0 ${whoisSocket.userinfo}\r\n`); + output_lines.push(`:${this.servername} 352 ${socket.nickname} * ${whoisSocket.nickname} ${whoisSocket.host} ${this.servername} ${whoisSocket.nickname} ${(this.awaymsgs.has(target)) ? 'G' : 'H'}${(whoisSocket.secure) ? 'z' : ''} :0 ${whoisSocket.userinfo}\r\n`); } else { - socket.write(`:${this.servername} 401 ${socket.nickname} ${target} :No such nick/channel\r\n`); + output_lines.push(`:${this.servername} 401 ${socket.nickname} ${target} :No such nick/channel\r\n`); } - socket.write(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); + output_lines.push(`:${this.servername} 315 ${socket.nickname} ${target} :End of /WHO list\r\n`); + this.sendThrottled(socket, output_lines); break; } } @@ -3031,7 +3044,7 @@ class WTVIRC { socket.write(`:${this.servername} 396 ${socket.nickname} :Your VHost has been changed to ${socket.host}\r\n`); }); break; - case 'STARTTLS': + case 'STARTTLS': socket.upgrading_to_tls = true; socket.write(`:${this.servername} 670 ${socket.uniqueId} :STARTTLS successful, go ahead with TLS handshake\r\n`); break; @@ -3350,50 +3363,6 @@ class WTVIRC { return matches; } - async getHostname(socket) { - // Resolve the hostname for a socket, using reverse DNS lookup - // Requires a callback - if (socket && socket.remoteAddress) { - try { - var hostname = socket.remoteAddress - hostname = await new Promise((resolve) => { - // dns.reverse does not support a native timeout option, so we implement our own timeout - let timeoutHandle; - const timeoutPromise = new Promise((resolveTimeout) => { - timeoutHandle = setTimeout(() => { - if (!socket.hostname_resolved) { - socket.hostname_resolved = true; - socket.write(`:${this.servername} NOTICE AUTH :*** Could not resolve your hostname: Timeout; using your IP address (${socket.remoteAddress}) instead.\r\n`); - } - resolveTimeout(socket.remoteAddress); - }, 5000); // 5 seconds timeout - }); - dns.reverse(socket.remoteAddress, (err, hostnames) => { - if (!err && hostnames && hostnames.length > 0) { - socket.hostname_resolved = true; - socket.write(`:${this.servername} NOTICE AUTH :*** Hostname found: ${hostnames[0]}\r\n`); - resolve(hostnames[0]); - } else { - if (!err) { - err = 'Domain name not found'; - } - socket.hostname_resolved = true; - socket.write(`:${this.servername} NOTICE AUTH :*** Could not resolve your hostname: ${err}; using your IP address (${socket.remoteAddress}) instead.\r\n`); - resolve(socket.remoteAddress); - } - }); - }); - } catch (e) { - if (this.debug) { - console.error(`Error resolving hostname for ${socket.remoteAddress}:`, e); - } - socket.hostname_resolved = true; - socket.write(`:${this.servername} NOTICE AUTH :*** Could not resolve your hostname: ${e}; using your IP address (${socket.remoteAddress}) instead.\r\n`); - hostname = socket.remoteAddress; - } - } - } - filterHostname(socket, hostname) { // Masks the user's hostname or IP for privacy const username = this.nicknames.get(socket); @@ -4275,9 +4244,9 @@ class WTVIRC { return id; } - async sendThrottled(socket, lines, delayMs = 30) { + async sendThrottled(socket, lines, delayMs = 10) { for (const line of lines) { - socket.write(line + '\r\n'); + socket.write(line); await new Promise(res => setTimeout(res, delayMs)); } } @@ -4401,6 +4370,38 @@ class WTVIRC { this.usermodes.set(nickname, modes); } + async getHostname(socket) { + // Resolve the hostname for a socket, using reverse DNS lookup + if (socket && socket.remoteAddress) { + try { + var hostname = socket.remoteAddress + hostname = await new Promise((resolve) => { + dns.reverse(socket.remoteAddress, (err, hostnames) => { + if (!err && hostnames && hostnames.length > 0) { + socket.hostname_resolved = true; + socket.write(`:${this.servername} NOTICE AUTH :*** Hostname found: ${hostnames[0]}\r\n`); + resolve(hostnames[0]); + } else { + if (!err) { + err = 'Domain name not found'; + } + socket.hostname_resolved = true; + socket.write(`:${this.servername} NOTICE AUTH :*** Could not resolve your hostname: ${err}; using your IP address (${socket.remoteAddress}) instead.\r\n`); + resolve(socket.remoteAddress); + } + }); + }); + } catch (e) { + if (this.debug) { + console.error(`Error resolving hostname for ${socket.remoteAddress}:`, e); + } + socket.hostname_resolved = true; + socket.write(`:${this.servername} NOTICE AUTH :*** Could not resolve your hostname: ${e}; using your IP address (${socket.remoteAddress}) instead.\r\n`); + } + return hostname; + } + } + async doInitialHandshake(socket) { // Perform the initial handshake with the client // We pause the socket to prevent sending data before the hostname is resolved @@ -4427,11 +4428,11 @@ class WTVIRC { this.addUserUniqueId(nickname, socket.uniqueId); this.hostnames.set(nickname, socket.host); this.realhosts.set(nickname, socket.realhost); - - socket.write(`:${this.servername} NOTICE AUTH :Welcome to \x02${this.network}\x0F\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 zefIRCd v${this.version}\r\n`); - socket.write(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`); + var output_lines = [] + output_lines.push(`:${this.servername} NOTICE AUTH :Welcome to \x02${this.network}\x0F\r\n`); + output_lines.push(`:${this.servername} 001 ${nickname} :Welcome to the IRC server, ${nickname}\r\n`); + output_lines.push(`:${this.servername} 002 ${nickname} :Your host is ${this.servername}, running version zefIRCd v${this.version}\r\n`); + output_lines.push(`:${this.servername} 003 ${nickname} :This server is ready to accept commands\r\n`); // Sort supported_channel_modes and supported_user_modes alphabetically with capitals first function sortModesAlphaCapsFirst(modes) { // Remove commas, split to chars, sort, then re-insert commas if needed @@ -4469,11 +4470,11 @@ class WTVIRC { var channelModes = this.supported_channel_modes.split(',').join('') + this.supported_prefixes[0]; var sortedChannelModes = sortModesAlphaCapsFirst(channelModes).replace(/,/g, ''); var sortedUserModes = sortModesAlphaCapsFirst(this.supported_user_modes); - socket.write(`:${this.servername} 004 ${nickname} ${this.servername} zefIRCd-${this.version} ${sortedUserModes} ${sortedChannelModes} ${sortedModesWithParams}\r\n`); + output_lines.push(`:${this.servername} 004 ${nickname} ${this.servername} zefIRCd-${this.version} ${sortedUserModes} ${sortedChannelModes} ${sortedModesWithParams}\r\n`); for (const caps of this.caps) { - socket.write(`:${this.servername} 005 ${caps}\r\n`); - } - socket.write(`:${this.servername} 042 ${nickname} ${socket.uniqueId} :your unique ID\r\n`); + output_lines.push(`:${this.servername} 005 ${caps}\r\n`); + } + output_lines.push(`:${this.servername} 042 ${nickname} ${socket.uniqueId} :your unique ID\r\n`); this.doMOTD(nickname, socket); @@ -4490,22 +4491,22 @@ class WTVIRC { return modes.includes('o'); }); const serverCount = this.servers.size + 1; // Include this server - socket.write(`:${this.servername} 251 ${nickname} :There are ${visibleClients.length} visible users and ${invisibleClients.length} invisible users on this server\r\n`); + output_lines.push(`:${this.servername} 251 ${nickname} :There are ${visibleClients.length} visible users and ${invisibleClients.length} invisible users on this server\r\n`); if (operClients.length > 0) { - socket.write(`:${this.servername} 252 ${nickname} ${operClients.length} :operator(s) online\r\n`); + output_lines.push(`:${this.servername} 252 ${nickname} ${operClients.length} :operator(s) online\r\n`); } if (this.channels.size > 0) { - socket.write(`:${this.servername} 253 ${nickname} ${this.channels.size} :channels formed\r\n`); + output_lines.push(`:${this.servername} 253 ${nickname} ${this.channels.size} :channels formed\r\n`); } - socket.write(`:${this.servername} 255 ${nickname} :I have ${this.clients.length} clients and ${serverCount} servers\r\n`); - socket.write(`:${this.servername} 265 ${nickname} :Current Local Users: ${this.clients.length} Max: ${this.clientpeak}\r\n`); + output_lines.push(`:${this.servername} 255 ${nickname} :I have ${this.clients.length} clients and ${serverCount} servers\r\n`); + output_lines.push(`:${this.servername} 265 ${nickname} :Current Local Users: ${this.clients.length} Max: ${this.clientpeak}\r\n`); const globalUsers = this.countGlobalUsers(); this.globalpeak = Math.max(this.globalpeak, this.countGlobalUsers()); var totalSockets = this.clients.length + this.servers.size; this.socketpeak = Math.max(this.socketpeak, totalSockets); - socket.write(`:${this.servername} 266 ${nickname} :Current Global Users: ${globalUsers} Max: ${this.globalpeak}\r\n`); - socket.write(`:${this.servername} 250 ${nickname} :Highest connection count: ${this.socketpeak} (${this.clientpeak} clients) (${this.totalConnections} connections received)\r\n`); + output_lines.push(`:${this.servername} 266 ${nickname} :Current Global Users: ${globalUsers} Max: ${this.globalpeak}\r\n`); + output_lines.push(`:${this.servername} 250 ${nickname} :Highest connection count: ${this.socketpeak} (${this.clientpeak} clients) (${this.totalConnections} connections received)\r\n`); var usermodes = this.usermodes.get(nickname); if (!usermodes || usermodes === true) { usermodes = []; @@ -4521,8 +4522,16 @@ class WTVIRC { if (usermodes) { this.usermodes.set(nickname, usermodes); } - socket.write(`:${this.servername} 221 ${nickname} :+${this.usermodes.get(nickname).join('')}\r\n`); - socket.write(`:${this.servername} PRIVMSG ${socket.nickname} :\x01VERSION\x01\r\n`); + if (usermodes.includes('x')) { + socket.host = this.filterHostname(socket, socket.realhost); + if (socket.client_caps && socket.client_caps.includes('CHGHOST')) { + output_lines.push(`:${socket.nickname}!${socket.username}@${socket.host} CHGHOST ${socket.username} ${socket.host}\r\n`); + } + output_lines.push(`:${this.servername} 396 ${socket.nickname} ${socket.host} :is now your visible host\r\n`); + } + output_lines.push(`:${this.servername} 221 ${nickname} :+${this.usermodes.get(nickname).join('')}\r\n`); + output_lines.push(`:${this.servername} PRIVMSG ${socket.nickname} :\x01VERSION\x01\r\n`); + await this.sendThrottled(socket, output_lines); this.broadcastConnection(socket); } }