add CHGHOST cap

This commit is contained in:
zefie
2025-06-16 23:28:57 -04:00
parent 84b5f2d7ef
commit a25bf417f3

View File

@@ -18,7 +18,7 @@ class WTVIRC {
* Basic IRCOp functionality is included. * Basic IRCOp functionality is included.
* hybridircd compatible server link protocol (tested with Anope IRC Services, and partially with hybridircd itself). * hybridircd compatible server link protocol (tested with Anope IRC Services, and partially with hybridircd itself).
* Channel modes are supported, including invite-only, topic protection, password protection, and user modes (op/halfop/voice), and more. * Channel modes are supported, including invite-only, topic protection, password protection, and user modes (op/halfop/voice), and more.
* SSL only channel mode +z is supported. As is usermode +Z (no DMs from non-SSL users) * SSL only channel mode +Z is supported. As is usermode +Z (no DMs from non-SSL users)
* *
* TODO: k-line? other "lines"? * TODO: k-line? other "lines"?
* TODO: Test for crashes with arbitrary data, or malformed commands (especially SSL handshake, or server interface). * TODO: Test for crashes with arbitrary data, or malformed commands (especially SSL handshake, or server interface).
@@ -89,11 +89,11 @@ class WTVIRC {
this.maxtargets = this.irc_config.max_targets || 4; this.maxtargets = this.irc_config.max_targets || 4;
this.serverId = this.irc_config.server_id || '00A'; // Default server ID, can be overridden in config this.serverId = this.irc_config.server_id || '00A'; // Default server ID, can be overridden in config
this.allow_public_vhosts = this.irc_config.allow_public_vhosts || true; // If true, users can set their host to a virtual host that is not a real hostname or IP address, if false, only opers can. this.allow_public_vhosts = this.irc_config.allow_public_vhosts || true; // If true, users can set their host to a virtual host that is not a real hostname or IP address, if false, only opers can.
this.kick_insecure_on_z = this.irc_config.kick_insecure_on_z || true; // If true, users without SSL connections will be kicked from a channel when +z is applied this.kick_insecure_on_z = this.irc_config.kick_insecure_on_z || true; // If true, users without SSL connections will be kicked from a channel when +Z is applied
this.clientpeak = 0; this.clientpeak = 0;
this.globalpeak = 0; this.globalpeak = 0;
this.caps = [ this.caps = [
`AWAYLEN=${this.awaylen} CASEMAPPING=rfc1459 CHANMODES=beI,k,l,itmnpcTVZRrNQO CHANNELLEN=${this.channellen} CHANTYPES=${this.channelprefixes.join('')} PREFIX=(ohv)@%+ USERMODES=oxirzZws MAXLIST=b:${this.maxbans},e:${this.maxexcept},i:${this.maxinvite},k:${this.maxkeylen},l:${this.maxlimit}`, `AWAYLEN=${this.awaylen} CASEMAPPING=rfc1459 CHANMODES=beI,k,l,itmnpcTVZRrNQOZ CHANNELLEN=${this.channellen} CHANTYPES=${this.channelprefixes.join('')} PREFIX=(ohv)@%+ USERMODES=oxirzZws 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}` `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}`
]; ];
} }
@@ -471,7 +471,10 @@ class WTVIRC {
} }
this.hostnames.set(this.findUserByUniqueId(uniqueId), hostname); this.hostnames.set(this.findUserByUniqueId(uniqueId), hostname);
targetSocket.host = hostname; targetSocket.host = hostname;
targetSocket.write(`:${this.servername} 396 ${targetSocket.nickname} ${targetSocket.host} :is now your displayed host\r\n`); if (targetSocket.client_caps && targetSocket.client_caps.includes('CHGHOST')) {
targetSocket.write(`:${targetSocket.nickname}!${targetSocket.username}@${targetSocket.host} CHGHOST ${targetSocket.username} ${targetSocket.host}\r\n`);
}
targetSocket.write(`:${this.servername} 396 ${targetSocket.nickname} ${targetSocket.host} :is now your visible host\r\n`);
this.broadcastToAllServers(`:${socket.servername} SVSHOST ${uniqueId} ${hostname}\r\n`, socket); this.broadcastToAllServers(`:${socket.servername} SVSHOST ${uniqueId} ${hostname}\r\n`, socket);
break; break;
case 'SVSNICK': case 'SVSNICK':
@@ -498,7 +501,11 @@ class WTVIRC {
} }
var channel = parts[2]; var channel = parts[2];
var modes = parts[3]; var modes = parts[3];
var uniqueId = parts[4].slice(1); var uniqueId = parts[4].slice(1);
if (!uniqueId) {
this.broadcastToAllServers(`:${socket.servername} SJOIN ${this.getDate()} ${channel} +${modes} :\r\n`, socket);
break;
}
if (['@', '%', '+'].includes(uniqueId[0])) { if (['@', '%', '+'].includes(uniqueId[0])) {
uniqueId = uniqueId.slice(1); uniqueId = uniqueId.slice(1);
} }
@@ -1427,9 +1434,21 @@ class WTVIRC {
case 'CAP': case 'CAP':
// Minimal CAP support: just acknowledge LS // Minimal CAP support: just acknowledge LS
if (params[0] && params[0].toUpperCase() === 'LS') { if (params[0] && params[0].toUpperCase() === 'LS') {
socket.write('CAP * LS :\r\n'); socket.write('CAP * LS :chghost\r\n');
} }
break; if (params[0] && params[0].toUpperCase() === 'REQ') {
socket.client_caps = params.slice(1).map(cap => {
if (cap.startsWith(':')) {
return cap.slice(1).toUpperCase();
}
return cap.toUpperCase();
});
socket.write(`CAP * ACK :${socket.client_caps.join(' ')}\r\n`);
if (this.debug) {
console.log(`Client capabilities for ${socket.uniqueId}: ${socket.client_caps.join(', ')}`);
}
}
break;
case 'MODE': case 'MODE':
if (!socket.registered) { if (!socket.registered) {
socket.write(`:${this.servername} 451 ${socket.nickname} :You have not registered\r\n`); socket.write(`:${this.servername} 451 ${socket.nickname} :You have not registered\r\n`);
@@ -1461,7 +1480,7 @@ class WTVIRC {
const mode = params[1]; const mode = params[1];
if (isUser) { if (isUser) {
if (!this.isIRCOp(socket.nickname) && channel !== socket.nickname) { if (!this.isIRCOp(socket.nickname) && channel !== socket.nickname) {
socket.write(`:${this.servername} 501 ${socket.nickname} :Cannot set modes on other users\r\n`); socket.write(`:${this.servername} 502 ${socket.nickname} :Cannot set modes on other users\r\n`);
} else { } else {
var usermodes = this.usermodes.get(socket.nickname) || []; var usermodes = this.usermodes.get(socket.nickname) || [];
@@ -1480,13 +1499,19 @@ class WTVIRC {
this.usermodes.set(socket.nickname, [...usermodes, 'x']); this.usermodes.set(socket.nickname, [...usermodes, 'x']);
socket.host = this.filterHostname(socket, socket.realhost); socket.host = this.filterHostname(socket, socket.realhost);
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} MODE ${socket.nickname} +x\r\n`); socket.write(`:${socket.nickname}!${socket.username}@${socket.host} MODE ${socket.nickname} +x\r\n`);
socket.write(`:${this.servername} 396 ${socket.nickname} ${socket.host} :is now your displayed host\r\n`); if (socket.client_caps && socket.client_caps.includes('CHGHOST')) {
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} CHGHOST ${socket.username} ${socket.host}\r\n`);
}
socket.write(`:${this.servername} 396 ${socket.nickname} ${socket.host} :is now your visible host\r\n`);
this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} +x\r\n`); this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} +x\r\n`);
} else if (mode.startsWith('-x')) { } else if (mode.startsWith('-x')) {
this.usermodes.set(socket.nickname, (usermodes).filter(m => m !== 'x')); this.usermodes.set(socket.nickname, (usermodes).filter(m => m !== 'x'));
socket.host = socket.realhost socket.host = socket.realhost
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} MODE ${socket.nickname} -x\r\n`); socket.write(`:${socket.nickname}!${socket.username}@${socket.host} MODE ${socket.nickname} -x\r\n`);
socket.write(`:${this.servername} 396 ${socket.nickname} ${socket.host} :is now your displayed host\r\n`); if (socket.client_caps && socket.client_caps.includes('CHGHOST')) {
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} CHGHOST ${socket.username} ${socket.host}\r\n`);
}
socket.write(`:${this.servername} 396 ${socket.nickname} ${socket.host} :is now your visible host\r\n`);
this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} -x\r\n`); this.broadcastToAllServers(`:${socket.uniqueId} MODE ${socket.uniqueId} -x\r\n`);
} else if (mode.startsWith('+w')) { } else if (mode.startsWith('+w')) {
this.usermodes.set(socket.nickname, [...usermodes, 'w']); this.usermodes.set(socket.nickname, [...usermodes, 'w']);
@@ -1545,7 +1570,7 @@ class WTVIRC {
} }
let validPrefix = this.channelprefixes.some(prefix => channel.startsWith(prefix)); let validPrefix = this.channelprefixes.some(prefix => channel.startsWith(prefix));
if (!validPrefix) { if (!validPrefix) {
socket.write(`:${this.servername} 403 ${socket.nickname} ${channel} :No such channel\r\n`); socket.write(`:${this.servername} 476 ${socket.nickname} ${channel} :Bad channel mask\r\n`);
break; break;
} }
if (!this.channels.has(channel)) { if (!this.channels.has(channel)) {
@@ -1710,14 +1735,14 @@ class WTVIRC {
// Simulate a JOIN command for each channel // Simulate a JOIN command for each channel
for (let i = 0; i < ch.length; i++) { for (let i = 0; i < ch.length; i++) {
if (i == 0 && !this.channelprefixes.includes(ch[i])) { if (i == 0 && !this.channelprefixes.includes(ch[i])) {
socket.write(`:${this.servername} 403 ${socket.nickname} ${ch} :No such channel\r\n`); socket.write(`:${this.servername} 476 ${socket.nickname} ${ch} :Bad channel mask\r\n`);
return; return;
} }
if (i == 0) { if (i == 0) {
continue; continue;
} }
if (!this.allowed_characters.includes(ch[i])) { if (!this.allowed_characters.includes(ch[i])) {
socket.write(`:${this.servername} 403 ${socket.nickname} ${ch} :No such channel\r\n`); socket.write(`:${this.servername} 476 ${socket.nickname} ${ch} :Bad channel mask\r\n`);
return; return;
} }
} }
@@ -1819,7 +1844,7 @@ class WTVIRC {
if (this.channelmodes.has(ch) && this.channelmodes.get(ch).includes('Z')) { if (this.channelmodes.has(ch) && this.channelmodes.get(ch).includes('Z')) {
// Channel is restricted to users with a secure connection (+Z) // Channel is restricted to users with a secure connection (+Z)
if (!socket.secure) { if (!socket.secure) {
socket.write(`:${this.servername} 474 ${socket.nickname} ${ch} :Cannot join channel (+Z)\r\n`); socket.write(`:${this.servername} 468 ${socket.nickname} ${ch} :Cannot join channel (+Z)\r\n`);
continue; // Skip joining this channel continue; // Skip joining this channel
} }
} }
@@ -2493,6 +2518,9 @@ class WTVIRC {
} }
// Set the new VHost for the socket // Set the new VHost for the socket
socket.host = newVHost; socket.host = newVHost;
if (socket.client_caps && socket.client_caps.includes('CHGHOST')) {
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} CHGHOST ${socket.username} ${socket.host}\r\n`);
}
socket.write(`:${this.servername} 396 ${socket.nickname} :Your VHost has been changed to ${socket.host}\r\n`); socket.write(`:${this.servername} 396 ${socket.nickname} :Your VHost has been changed to ${socket.host}\r\n`);
}); });
break; break;
@@ -2514,6 +2542,8 @@ class WTVIRC {
this.channelexemptions.delete(channel); this.channelexemptions.delete(channel);
this.channelinvites.delete(channel); this.channelinvites.delete(channel);
this.channelmodes.delete(channel); this.channelmodes.delete(channel);
this.channellimits.delete(channel);
this.channelkeys.delete(channel);
this.channeltimestamps.delete(channel); this.channeltimestamps.delete(channel);
if (this.debug) { if (this.debug) {
console.log(`Channel ${channel} deleted`); console.log(`Channel ${channel} deleted`);
@@ -3056,7 +3086,7 @@ class WTVIRC {
} }
const key = params[2]; const key = params[2];
if (key.length < 1 || key.length > this.max_keylen) { if (key.length < 1 || key.length > this.max_keylen) {
socket.write(`:${this.servername} 501 ${nickname} :Invalid channel key\r\n`); socket.write(`:${this.servername} 525 ${nickname} :Invalid channel key\r\n`);
return; return;
} }
var chan_modes = this.channelmodes.get(channel); var chan_modes = this.channelmodes.get(channel);
@@ -3448,7 +3478,7 @@ class WTVIRC {
chan_modes = []; chan_modes = [];
} }
if (!socket.secure) { if (!socket.secure) {
socket.write(`:${this.servername} 484 ${nickname} ${channel} :You must be connected via SSL/TLS to set +z\r\n`); socket.write(`:${this.servername} 484 ${nickname} ${channel} :You must be connected via SSL/TLS to set +Z\r\n`);
return; return;
} }
if (!chan_modes.includes('Z')) { if (!chan_modes.includes('Z')) {
@@ -3684,7 +3714,7 @@ class WTVIRC {
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`);
socket.write(`:${this.servername} 004 ${nickname} ${this.servername} minisrv ${this.minisrv_config.version} oxizrZws obtkmeZIlhvTVROQrnc beIklohv\r\n`); socket.write(`:${this.servername} 004 ${nickname} ${this.servername} minisrv ${this.minisrv_config.version} oxizrZws obtkmeZIlhvTVROQrncZ beIklohv\r\n`);
for (const caps of this.caps) { for (const caps of this.caps) {
socket.write(`:${this.servername} 005 ${caps}\r\n`); socket.write(`:${this.servername} 005 ${caps}\r\n`);
} }
@@ -3735,7 +3765,10 @@ class WTVIRC {
this.getHostname(socket, (hostname) => { this.getHostname(socket, (hostname) => {
socket.host = this.filterHostname(socket, hostname); socket.host = this.filterHostname(socket, hostname);
socket.realhost = hostname; socket.realhost = hostname;
socket.write(`:${this.servername} 396 ${nickname} ${socket.host} :is now your displayed host\r\n`); if (socket.client_caps && socket.client_caps.includes('CHGHOST')) {
socket.write(`:${socket.nickname}!${socket.username}@${socket.host} CHGHOST ${socket.username} ${socket.host}\r\n`);
}
socket.write(`:${this.servername} 396 ${nickname} ${socket.host} :is now your visible host\r\n`);
}); });
} }
} }