soju/xirc/whox.go
delthas 8b558e39b7 xirc: Fix sending hostnames starting with ':' in WHO replies
Some IPv6 hostnames can start with a colon (eg '::1'). This breaks
the IRC line format.

To work around this issue, prefix the hostname with a '0'. This
changes the representation of the IP but not its value.

References: https://todo.sr.ht/~taiite/senpai/109
Co-authored-by: Simon Ser <contact@emersion.fr>
2023-01-25 00:02:26 +01:00

168 lines
3.6 KiB
Go

package xirc
import (
"gopkg.in/irc.v4"
"fmt"
"strings"
)
// whoxFields is the list of all WHOX field letters, by order of appearance in
// RPL_WHOSPCRPL messages.
var whoxFields = []byte("tcuihsnfdlaor")
type WHOXInfo struct {
Token string
Username string
Hostname string
Server string
Nickname string
Flags string
Account string
Realname string
}
func (info *WHOXInfo) get(k byte) string {
switch k {
case 't':
return info.Token
case 'c':
return "*"
case 'u':
return info.Username
case 'i':
return "255.255.255.255"
case 'h':
hostname := info.Hostname
if strings.HasPrefix(info.Hostname, ":") {
// The hostname cannot start with a colon as this would get parsed
// as a trailing parameter. IPv6 addresses such as "::1" are
// prefixed with a zero to ensure this.
hostname = "0" + hostname
}
return hostname
case 's':
return info.Server
case 'n':
return info.Nickname
case 'f':
return info.Flags
case 'd':
return "0"
case 'l': // idle time
return "0"
case 'a':
account := "0" // WHOX uses "0" to mean "no account"
if info.Account != "" && info.Account != "*" {
account = info.Account
}
return account
case 'o':
return "0"
case 'r':
return info.Realname
}
return ""
}
func (info *WHOXInfo) set(k byte, v string) {
switch k {
case 't':
info.Token = v
case 'u':
info.Username = v
case 'h':
info.Hostname = v
case 's':
info.Server = v
case 'n':
info.Nickname = v
case 'f':
info.Flags = v
case 'a':
info.Account = v
case 'r':
info.Realname = v
}
}
func GenerateWHOXReply(prefix *irc.Prefix, nick, fields string, info *WHOXInfo) *irc.Message {
if fields == "" {
hostname := info.Hostname
if strings.HasPrefix(info.Hostname, ":") {
// The hostname cannot start with a colon as this would get parsed
// as a trailing parameter. IPv6 addresses such as "::1" are
// prefixed with a zero to ensure this.
hostname = "0" + hostname
}
return &irc.Message{
Prefix: prefix,
Command: irc.RPL_WHOREPLY,
Params: []string{nick, "*", info.Username, hostname, info.Server, info.Nickname, info.Flags, "0 " + info.Realname},
}
}
fieldSet := make(map[byte]bool)
for i := 0; i < len(fields); i++ {
fieldSet[fields[i]] = true
}
var values []string
for _, field := range whoxFields {
if !fieldSet[field] {
continue
}
values = append(values, info.get(field))
}
return &irc.Message{
Prefix: prefix,
Command: RPL_WHOSPCRPL,
Params: append([]string{nick}, values...),
}
}
func ParseWHOXOptions(options string) (fields, whoxToken string) {
optionsParts := strings.SplitN(options, "%", 2)
// TODO: add support for WHOX flags in optionsParts[0]
if len(optionsParts) == 2 {
optionsParts := strings.SplitN(optionsParts[1], ",", 2)
fields = strings.ToLower(optionsParts[0])
if len(optionsParts) == 2 && strings.Contains(fields, "t") {
whoxToken = optionsParts[1]
}
}
return fields, whoxToken
}
func ParseWHOXReply(msg *irc.Message, fields string) (*WHOXInfo, error) {
if msg.Command != RPL_WHOSPCRPL {
return nil, fmt.Errorf("invalid WHOX reply %q", msg.Command)
} else if len(msg.Params) == 0 {
return nil, fmt.Errorf("invalid RPL_WHOSPCRPL: no params")
}
fieldSet := make(map[byte]bool)
for i := 0; i < len(fields); i++ {
fieldSet[fields[i]] = true
}
var info WHOXInfo
values := msg.Params[1:]
for _, field := range whoxFields {
if !fieldSet[field] {
continue
}
if len(values) == 0 {
return nil, fmt.Errorf("invalid RPL_WHOSPCRPL: missing value for field %q", string(field))
}
info.set(field, values[0])
values = values[1:]
}
return &info, nil
}