check / check (push) Canceled after 0s
The IRC listener relayed only the first line of a PRIVMSG or NOTICE body; it now sends one line per body line. A MODE change reached IRC listener members without its key, limit or mask, and "+o bob" went out as one trailing parameter; each value is now its own parameter, in the relay and in the echo to the IRC listener user who made the change. CR, LF and NUL are now removed from each parameter of a line sent to an IRC listener connection, before the leading ':' is decided, and from the whole line, so a stored body line, key or mask cannot start a line of its own. The channel name check moved from the HTTP handler into service.JoinChannel, so the IRC listener's JOIN refuses the same names with 403. Model: opus-5-5
131 lines
2.8 KiB
Go
131 lines
2.8 KiB
Go
// Package ircserver implements a traditional IRC wire protocol
|
|
// listener (RFC 1459/2812) that bridges to the neoirc HTTP/JSON
|
|
// server internals.
|
|
package ircserver
|
|
|
|
import "strings"
|
|
|
|
// Message represents a parsed IRC wire protocol message.
|
|
type Message struct {
|
|
// Prefix is the optional :prefix at the start (may be
|
|
// empty for client-to-server messages).
|
|
Prefix string
|
|
// Command is the IRC command (e.g., "PRIVMSG", "NICK").
|
|
Command string
|
|
// Params holds the positional parameters, including the
|
|
// trailing parameter (which was preceded by ':' on the
|
|
// wire).
|
|
Params []string
|
|
}
|
|
|
|
// ParseMessage parses a single IRC wire protocol line
|
|
// (without the trailing CR-LF) into a Message.
|
|
// Returns nil if the line is empty.
|
|
//
|
|
// IRC message format (RFC 1459 §2.3.1):
|
|
//
|
|
// [":" prefix SPACE] command { SPACE param } [SPACE ":" trailing]
|
|
func ParseMessage(line string) *Message {
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
msg := &Message{} //nolint:exhaustruct // fields set below
|
|
|
|
// Extract prefix if present.
|
|
if line[0] == ':' {
|
|
idx := strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
// Only a prefix, no command — invalid.
|
|
return nil
|
|
}
|
|
|
|
msg.Prefix = line[1:idx]
|
|
line = line[idx+1:]
|
|
}
|
|
|
|
// Skip leading spaces.
|
|
line = strings.TrimLeft(line, " ")
|
|
if line == "" {
|
|
return nil
|
|
}
|
|
|
|
// Extract command.
|
|
idx := strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
msg.Command = strings.ToUpper(line)
|
|
|
|
return msg
|
|
}
|
|
|
|
msg.Command = strings.ToUpper(line[:idx])
|
|
line = line[idx+1:]
|
|
|
|
// Extract parameters.
|
|
for line != "" {
|
|
line = strings.TrimLeft(line, " ")
|
|
if line == "" {
|
|
break
|
|
}
|
|
|
|
// Trailing parameter (everything after ':').
|
|
if line[0] == ':' {
|
|
msg.Params = append(msg.Params, line[1:])
|
|
|
|
break
|
|
}
|
|
|
|
idx = strings.IndexByte(line, ' ')
|
|
if idx < 0 {
|
|
msg.Params = append(msg.Params, line)
|
|
|
|
break
|
|
}
|
|
|
|
msg.Params = append(msg.Params, line[:idx])
|
|
line = line[idx+1:]
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// FormatMessage formats an IRC message into wire protocol
|
|
// format (without the trailing CR-LF). CR, LF and NUL are
|
|
// removed from each parameter, because on the wire they would
|
|
// end the line early; this happens before deciding whether
|
|
// the last parameter needs a leading ':'.
|
|
func FormatMessage(
|
|
prefix, command string,
|
|
params ...string,
|
|
) string {
|
|
var buf strings.Builder
|
|
|
|
if prefix != "" {
|
|
buf.WriteByte(':')
|
|
buf.WriteString(prefix)
|
|
buf.WriteByte(' ')
|
|
}
|
|
|
|
buf.WriteString(command)
|
|
|
|
for i, param := range params {
|
|
param = strings.ReplaceAll(param, "\r", "")
|
|
param = strings.ReplaceAll(param, "\n", "")
|
|
param = strings.ReplaceAll(param, "\x00", "")
|
|
|
|
buf.WriteByte(' ')
|
|
|
|
isLast := i == len(params)-1
|
|
needsColon := strings.Contains(param, " ") ||
|
|
param == "" || param[0] == ':'
|
|
|
|
if isLast && needsColon {
|
|
buf.WriteByte(':')
|
|
}
|
|
|
|
buf.WriteString(param)
|
|
}
|
|
|
|
return buf.String()
|
|
}
|