feat: implement ISUPPORT and add support for a few easy features

fix: CAP ACK commands with no acknowledged capabilities causing an OOB.

Also log unsupported and unknown capabilities and features.
This commit is contained in:
2025-08-15 21:10:52 +02:00
parent 4be731c813
commit 41645fb679
4 changed files with 141 additions and 65 deletions
+4
View File
@@ -95,9 +95,13 @@ type Client struct {
// The acknowledged capabilities
EnabledCapabilities Capabilities
// The features currently enabled for this client
EnabledFeatures Features
}
type Capabilities map[string]string
type Features map[string]string
type MsgFmtOpts struct {
WithTimestamp bool
+57 -54
View File
@@ -58,7 +58,7 @@ const (
NOTICE = "NOTICE" // Send notices between users and to channels.
// Operator Messages
KILL = "KILL" // CLose connection between a client and the server. IRC Operators only (do we need this ??)
KILL = "KILL" // Close connection between a client and the server. IRC Operators only (do we need this ??)
// Optional Messages
AWAY = "AWAY" // Indicate that the client (user) is away/afk/etc..
@@ -305,64 +305,67 @@ const (
/* --- RPL_ISUPPORT Parameters --- */
var Features = map[string]bool{
"ACCEPT": true,
"ACCEPT": false,
// The AWAYLEN parameter indicates the maximum length for the <reason> of an AWAY command.
// If an AWAY <reason> has more characters than this parameter, it may be silently truncated by the server before being passed on to other clients.
// Clients MAY receive an AWAY <reason> that has more characters than this parameter
"AWAYLEN": true,
"BOT": true,
"CALLERID": true,
"CASEMAPPING": true,
"CHANLIMIT": true,
"CHANMODES": true,
"CHANNELLEN": true,
"CHANTYPES": true, // Channel Types. Default is #. Available are #&
"CHARSET": true, // Deprecated but might still be used
"CLIENTVER": true, // Deprecated but might still be used
"CNOTICE": true,
"CPRIVMSG": true,
"DEAF": true,
"ELIST": true,
"ESILENCE": true,
"ETRACE": true,
"EXCEPTS": true,
"EXTBAN": true,
"FNC": true, // Deprecated but might still be used
"INVEX": true,
"KEYLEN": true,
"BOT": false,
"CALLERID": false,
"CASEMAPPING": false,
"CHANLIMIT": false,
"CHANMODES": false,
"CHANNELLEN": false,
"CHANTYPES": false, // Channel Types. Default is #. Available are #&
"CHARSET": false, // Deprecated but might still be used
"CLIENTVER": false, // Deprecated but might still be used
"CNOTICE": false,
"CPRIVMSG": false,
"DEAF": false,
"ELIST": false,
"ESILENCE": false,
"ETRACE": false,
"EXCEPTS": false,
"EXTBAN": false,
"FNC": false, // Deprecated but might still be used
"INVEX": false,
"KEYLEN": false,
"KICKLEN": true,
"KNOCK": true,
"LINELEN": true, // Proposed
"MAP": true, // Deprecated but might still be used
"MAXBANS": true, // Deprecated but might still be used
"MAXCHANNELS": true, // Deprecated but might still be used
"MAXLIST": true,
"MAXNICKLEN": true,
"MAXPARA": true, // Deprecated but might still be used
"MAXTARGETS": true,
"METADATA": true,
"MODES": true,
"MONITOR": true,
"NAMESX": true, // Deprecated but might still be used
"KNOCK": false,
"LINELEN": false, // Proposed
"MAP": false, // Deprecated but might still be used
"MAXBANS": false, // Deprecated but might still be used
"MAXCHANNELS": false, // Deprecated but might still be used
"MAXLIST": false,
"MAXNICKLEN": false,
"MAXPARA": false, // Deprecated but might still be used
"MAXTARGETS": false,
"METADATA": false,
"MODES": false,
"MONITOR": false,
"NAMESX": false, // Deprecated but might still be used
"NETWORK": true,
"NICKLEN": true,
"OVERRIDE": true,
"PREFIX": true,
"SAFELIST": true,
"SECURELIST": true,
"SILENCE": true,
"SSL": true, // Deprecated but might still be used
"STARTTLS": true, // Deprecated but might still be used
"STATUSMSG": true,
"STD": true, // Deprecated but might still be used
"TARGMAX": true,
"OVERRIDE": false,
"PREFIX": false,
"SAFELIST": false,
"SECURELIST": false,
"SILENCE": false,
"SSL": false, // Deprecated but might still be used
"STARTTLS": false, // Deprecated but might still be used
"STATUSMSG": false,
"STD": false, // Deprecated but might still be used
"TARGMAX": false,
"TOPICLEN": true,
"UHNAMES": true, // Deprecated but might still be used
"USERIP": true,
"USERLEN": true, // Proposed
"VBANLIST": true, // Deprecared but might still be used
"VLIST": true,
"WALLCHOPS": true, // Deprecated but might still be used
"WALLVOICES": true, // Deprecated but might still be used
"WATCH": true,
"WHOX": true,
"UHNAMES": false, // Deprecated but might still be used
"USERIP": false,
"USERLEN": false, // Proposed
"VBANLIST": false, // Deprecared but might still be used
"VLIST": false,
"WALLCHOPS": false, // Deprecated but might still be used
"WALLVOICES": false, // Deprecated but might still be used
"WATCH": false,
"WHOX": false,
}
var Capabilities = map[string]bool{
+49 -11
View File
@@ -35,6 +35,7 @@ import (
func ReadLoop(client *irc.Client) {
// 512 bytes as a base + 8192 additional bytes for tags
// as specified here: https://modern.ircdocs.horse/#message-format
r := bufio.NewReaderSize(client.TCPConn, 8192+512)
for {
@@ -235,18 +236,22 @@ func handleCAP(msg irc.Message, client *irc.Client) {
value = parts[1]
}
if commands.Capabilities[key] {
// When the final parameter approaches 510 bytes,
// we send multiple REQ commands
// (https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-req-subcommand)
if length+len(capability) >= 500 {
recognizedCaps = append(recognizedCaps, capabilities)
capabilities = make(irc.Capabilities, 0)
length = 0
}
if capEnabled, ok := commands.Capabilities[key]; ok {
if capEnabled {
// When the final parameter approaches 510 bytes,
// we send multiple REQ commands
// (https://ircv3.net/specs/extensions/capability-negotiation.html#the-cap-req-subcommand)
if length+len(capability) >= 500 {
recognizedCaps = append(recognizedCaps, capabilities)
capabilities = make(irc.Capabilities, 0)
length = 0
}
capabilities[key] = value
length += len(capability)
capabilities[key] = value
length += len(capability)
} else {
log.Println("Unsupported capability:", capability)
}
} else {
log.Println("Unknown capability:", capability)
}
@@ -266,6 +271,9 @@ func handleCAP(msg irc.Message, client *irc.Client) {
}
client.SendCommand(commands.CAP, "END")
case "ACK":
if msg.Parameters[2] == "" {
return
}
caps := strings.Split(msg.Parameters[2], " ")
for _, capability := range caps {
if capability[0] == '-' {
@@ -342,6 +350,34 @@ func handleMYINFO(msg irc.Message, client *irc.Client) {
client.Channels[0].AppendMsg(msg.Timestamp, info, msgOpts)
}
func handleISUPPORT(msg irc.Message, client *irc.Client) {
msgOpts := irc.MsgFmtOpts{
WithTimestamp: true,
AsServerMsg: true,
}
for _, token := range msg.Parameters[1 : len(msg.Parameters)-1] {
splits := strings.Split(token, "=")
key := splits[0]
value := ""
if len(splits) > 1 {
value = splits[1]
}
if supported, known := commands.Features[key]; known {
if supported {
client.EnabledFeatures[key] = value
} else {
log.Printf("Unsupported feature: \"%v\" with value: \"%v\"\n", key, value)
}
} else {
log.Printf("Unknown feature: \"%v\" with value: \"%v\"\n", key, value)
}
client.Channels[0].AppendMsg(msg.Timestamp, token, msgOpts)
}
}
func handleLUSERCLIENT(msg irc.Message, client *irc.Client) {
message := msg.Parameters[1]
@@ -695,6 +731,8 @@ func HandleCommand(msg irc.Message, client *irc.Client) {
handleCREATED(msg, client)
case commands.RPL_MYINFO:
handleMYINFO(msg, client)
case commands.RPL_ISUPPORT:
handleISUPPORT(msg, client)
case commands.RPL_LUSERCLIENT:
handleLUSERCLIENT(msg, client)
case commands.RPL_LUSEROP: