summaryrefslogtreecommitdiff
path: root/xS/xS.go
diff options
context:
space:
mode:
authorPřemysl Eric Janouch <p@janouch.name>2022-09-26 12:39:26 +0200
committerPřemysl Eric Janouch <p@janouch.name>2022-09-26 13:24:24 +0200
commit5863040f9323e210df0908333c6e85d63af000c0 (patch)
tree8c8838c68b7566b42d9dd84abe7212e93b1740ea /xS/xS.go
parentf891e5ca638ead13485cc490320e74d698641623 (diff)
downloadxK-5863040f9323e210df0908333c6e85d63af000c0.tar.gz
xK-5863040f9323e210df0908333c6e85d63af000c0.tar.xz
xK-5863040f9323e210df0908333c6e85d63af000c0.zip
Update documentation, clean up
Diffstat (limited to 'xS/xS.go')
-rw-r--r--xS/xS.go3527
1 files changed, 3527 insertions, 0 deletions
diff --git a/xS/xS.go b/xS/xS.go
new file mode 100644
index 0000000..e6c3b3c
--- /dev/null
+++ b/xS/xS.go
@@ -0,0 +1,3527 @@
+//
+// Copyright (c) 2014 - 2022, Přemysl Eric Janouch <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// xS is a straight-forward port of xD IRCd from C.
+package main
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "errors"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log/syslog"
+ "net"
+ "os"
+ "os/signal"
+ "os/user"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+)
+
+var debugMode = false
+
+const (
+ projectName = "xS"
+ // TODO: Consider using the same version number for all subprojects.
+ projectVersion = "0"
+)
+
+// --- Logging -----------------------------------------------------------------
+
+type logPrio int
+
+const (
+ prioFatal logPrio = iota
+ prioError
+ prioWarning
+ prioStatus
+ prioDebug
+)
+
+func (lp logPrio) prefix() string {
+ switch lp {
+ case prioFatal:
+ return "fatal: "
+ case prioError:
+ return "error: "
+ case prioWarning:
+ return "warning: "
+ case prioStatus:
+ return ""
+ case prioDebug:
+ return "debug: "
+ default:
+ panic("unhandled log priority")
+ }
+}
+
+func (lp logPrio) syslogPrio() syslog.Priority {
+ switch lp {
+ case prioFatal:
+ return syslog.LOG_ERR
+ case prioError:
+ return syslog.LOG_ERR
+ case prioWarning:
+ return syslog.LOG_WARNING
+ case prioStatus:
+ return syslog.LOG_INFO
+ case prioDebug:
+ return syslog.LOG_DEBUG
+ default:
+ panic("unhandled log priority")
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func logMessageStdio(prio logPrio, format string, args ...interface{}) {
+ // TODO: isatty-enabled colors based on prio.
+ os.Stderr.WriteString(time.Now().Format("2006-01-02 15:04:05 ") +
+ prio.prefix() + fmt.Sprintf(format, args...) + "\n")
+}
+
+func logMessageSystemd(prio logPrio, format string, args ...interface{}) {
+ if prio == prioFatal {
+ // There is no corresponding syslog severity.
+ format = "fatal: " + format
+ }
+ fmt.Fprintf(os.Stderr, "<%d>%s\n",
+ prio.syslogPrio(), fmt.Sprintf(format, args...))
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+var logMessage = logMessageStdio
+
+func printDebug(format string, args ...interface{}) {
+ if debugMode {
+ logMessage(prioDebug, format, args...)
+ }
+}
+
+func printStatus(format string, args ...interface{}) {
+ logMessage(prioStatus, format, args...)
+}
+func printWarning(format string, args ...interface{}) {
+ logMessage(prioWarning, format, args...)
+}
+func printError(format string, args ...interface{}) {
+ logMessage(prioError, format, args...)
+}
+
+// "fatal" is reserved for failures that would harm further operation.
+
+func printFatal(format string, args ...interface{}) {
+ logMessage(prioFatal, format, args...)
+}
+
+func exitFatal(format string, args ...interface{}) {
+ printFatal(format, args...)
+ os.Exit(1)
+}
+
+// --- Utilities ---------------------------------------------------------------
+
+// Split a string by a set of UTF-8 delimiters, optionally ignoring empty items.
+func splitString(s, delims string, ignoreEmpty bool) (result []string) {
+ for {
+ end := strings.IndexAny(s, delims)
+ if end < 0 {
+ break
+ }
+ if !ignoreEmpty || end != 0 {
+ result = append(result, s[:end])
+ }
+ s = s[end+1:]
+ }
+ if !ignoreEmpty || s != "" {
+ result = append(result, s)
+ }
+ return
+}
+
+// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
+// must be at least three octets long for this to work reliably, but that should
+// not pose a problem in practice. We might try waiting for them.
+//
+// SSL2: 1xxx xxxx | xxxx xxxx | <1>
+// (message length) (client hello)
+// SSL3/TLS: <22> | <3> | xxxx xxxx
+// (handshake)| (protocol version)
+//
+// Note that Go 1.12's crypto/tls offers a slightly more straight-forward
+// solution: "If a client sends an initial message that does not look like TLS,
+// the server will no longer reply with an alert, and it will expose the
+// underlying net.Conn in the new field Conn of RecordHeaderError."
+func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
+ sysconn.Read(func(fd uintptr) (done bool) {
+ var buf [3]byte
+ n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
+ switch {
+ case n == 3:
+ isTLS = buf[0]&0x80 != 0 && buf[2] == 1
+ fallthrough
+ case n == 2:
+ isTLS = isTLS || buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
+ }
+ return true
+ })
+ return isTLS
+}
+
+// --- File system -------------------------------------------------------------
+
+// Look up the value of an XDG path from environment, or fall back to a default.
+func getXDGHomeDir(name, def string) string {
+ env := os.Getenv(name)
+ if env != "" && env[0] == filepath.Separator {
+ return env
+ }
+
+ home := ""
+ if v, ok := os.LookupEnv("HOME"); ok {
+ home = v
+ } else if u, _ := user.Current(); u != nil {
+ home = u.HomeDir
+ }
+ return filepath.Join(home, def)
+}
+
+func resolveRelativeFilenameGeneric(paths []string, filename string) string {
+ for _, path := range paths {
+ // As per XDG spec, relative paths are ignored.
+ if path == "" || path[0] != filepath.Separator {
+ continue
+ }
+
+ file := filepath.Join(path, filename)
+ if _, err := os.Stat(file); err == nil {
+ return file
+ }
+ }
+ return ""
+}
+
+// Retrieve all XDG base directories for configuration files.
+func getXDGConfigDirs() (result []string) {
+ home := getXDGHomeDir("XDG_CONFIG_HOME", ".config")
+ if home != "" {
+ result = append(result, home)
+ }
+ dirs := os.Getenv("XDG_CONFIG_DIRS")
+ if dirs == "" {
+ dirs = "/etc/xdg"
+ }
+ for _, path := range strings.Split(dirs, ":") {
+ if path != "" {
+ result = append(result, path)
+ }
+ }
+ return
+}
+
+func resolveRelativeConfigFilename(filename string) string {
+ return resolveRelativeFilenameGeneric(getXDGConfigDirs(),
+ filepath.Join(projectName, filename))
+}
+
+func findTildeHome(username string) string {
+ if username != "" {
+ if u, _ := user.Lookup(username); u != nil {
+ return u.HomeDir
+ }
+ } else if u, _ := user.Current(); u != nil {
+ return u.HomeDir
+ } else if v, ok := os.LookupEnv("HOME"); ok {
+ return v
+ }
+ printDebug("failed to expand the home directory for %s", username)
+ return "~" + username
+}
+
+func resolveFilename(filename string, relativeCB func(string) string) string {
+ // Absolute path is absolute.
+ if filename == "" || filename[0] == filepath.Separator {
+ return filename
+ }
+ if filename[0] != '~' {
+ return relativeCB(filename)
+ }
+
+ // Paths to home directories ought to be absolute.
+ var n int
+ for n = 0; n < len(filename); n++ {
+ if filename[n] == filepath.Separator {
+ break
+ }
+ }
+ return findTildeHome(filename[1:n]) + filename[n:]
+}
+
+// --- Simple file I/O ---------------------------------------------------------
+
+// Overwrites filename contents with data; creates directories as needed.
+func writeFile(path string, data []byte) error {
+ if dir := filepath.Dir(path); dir != "." {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return err
+ }
+ }
+ return ioutil.WriteFile(path, data, 0644)
+}
+
+// Wrapper for writeFile that makes sure that the new data has been written
+// to disk in its entirety before overriding the old file.
+func writeFileSafe(path string, data []byte) error {
+ temp := path + ".new"
+ if err := writeFile(temp, data); err != nil {
+ return err
+ }
+ return os.Rename(temp, path)
+}
+
+// --- Simple configuration ----------------------------------------------------
+
+// This is the bare minimum to make an application configurable.
+// Keys are stripped of surrounding whitespace, values are not.
+
+type simpleConfigItem struct {
+ key string // INI key
+ def string // default value
+ description string // documentation
+}
+
+type simpleConfig map[string]string
+
+func (sc simpleConfig) loadDefaults(table []simpleConfigItem) {
+ for _, item := range table {
+ sc[item.key] = item.def
+ }
+}
+
+func (sc simpleConfig) updateFromFile() error {
+ basename := projectName + ".conf"
+ path := resolveFilename(basename, resolveRelativeConfigFilename)
+ if path == "" {
+ return &os.PathError{
+ Op: "cannot find",
+ Path: basename,
+ Err: os.ErrNotExist,
+ }
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ for lineNo := 1; scanner.Scan(); lineNo++ {
+ line := strings.TrimLeft(scanner.Text(), " \t")
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ equals := strings.IndexByte(line, '=')
+ if equals <= 0 {
+ return fmt.Errorf("%s:%d: malformed line", path, lineNo)
+ }
+
+ sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:]
+ }
+ return scanner.Err()
+}
+
+func writeConfigurationFile(pathHint string, data []byte) (string, error) {
+ path := pathHint
+ if path == "" {
+ path = filepath.Join(getXDGHomeDir("XDG_CONFIG_HOME", ".config"),
+ projectName, projectName+".conf")
+ }
+
+ if err := writeFileSafe(path, data); err != nil {
+ return "", err
+ }
+ return path, nil
+}
+
+func simpleConfigWriteDefault(pathHint string, prolog string,
+ table []simpleConfigItem) (string, error) {
+ data := []byte(prolog)
+ for _, item := range table {
+ data = append(data, fmt.Sprintf("# %s\n%s=%s\n",
+ item.description, item.key, item.def)...)
+ }
+ return writeConfigurationFile(pathHint, data)
+}
+
+// Convenience wrapper suitable for most simple applications.
+func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) {
+ prologLines := []string{
+ `# ` + projectName + ` ` + projectVersion + ` configuration file`,
+ "#",
+ `# Relative paths are searched for in ${XDG_CONFIG_HOME:-~/.config}`,
+ `# /` + projectName + ` as well as in $XDG_CONFIG_DIRS/` + projectName,
+ ``,
+ ``,
+ }
+
+ path, err := simpleConfigWriteDefault(
+ pathHint, strings.Join(prologLines, "\n"), table)
+ if err != nil {
+ exitFatal("%s", err)
+ }
+
+ printStatus("configuration written to `%s'", path)
+}
+
+// --- Configuration -----------------------------------------------------------
+
+var configTable = []simpleConfigItem{
+ {"server_name", "", "Server name"},
+ {"server_info", "My server", "Brief server description"},
+ {"motd", "", "MOTD filename"},
+ {"catalog", "", "Localisation catalog"},
+
+ {"bind", ":6667", "Bind addresses of the IRC server"},
+ {"tls_cert", "", "Server TLS certificate (PEM)"},
+ {"tls_key", "", "Server TLS private key (PEM)"},
+ {"webirc_password", "", "Password for WebIRC"},
+
+ {"operators", "", "IRCop TLS certificate SHA-256 fingerprints"},
+
+ {"max_connections", "0", "Global connection limit"},
+ {"ping_interval", "180", "Interval between PINGs (sec)"},
+}
+
+// --- Rate limiter ------------------------------------------------------------
+
+type floodDetector struct {
+ interval time.Duration // interval for the limit in seconds
+ limit uint // maximum number of events allowed
+ timestamps []time.Time // timestamps of last events
+ pos uint // index of the oldest event
+}
+
+func newFloodDetector(interval time.Duration, limit uint) *floodDetector {
+ return &floodDetector{
+ interval: interval,
+ limit: limit,
+ timestamps: make([]time.Time, limit+1),
+ pos: 0,
+ }
+}
+
+func (fd *floodDetector) check() bool {
+ now := time.Now()
+ fd.timestamps[fd.pos] = now
+
+ fd.pos++
+ if fd.pos > fd.limit {
+ fd.pos = 0
+ }
+
+ var count uint
+ begin := now.Add(-fd.interval)
+ for _, ts := range fd.timestamps {
+ if ts.After(begin) {
+ count++
+ }
+ }
+ return count <= fd.limit
+}
+
+// --- IRC protocol ------------------------------------------------------------
+
+func ircToLower(c byte) byte {
+ switch c {
+ case '[':
+ return '{'
+ case ']':
+ return '}'
+ case '\\':
+ return '|'
+ case '~':
+ return '^'
+ }
+ if c >= 'A' && c <= 'Z' {
+ return c + ('a' - 'A')
+ }
+ return c
+}
+
+func ircToUpper(c byte) byte {
+ switch c {
+ case '{':
+ return '['
+ case '}':
+ return ']'
+ case '|':
+ return '\\'
+ case '^':
+ return '~'
+ }
+ if c >= 'a' && c <= 'z' {
+ return c - ('a' - 'A')
+ }
+ return c
+}
+
+// Convert identifier to a canonical form for case-insensitive comparisons.
+// ircToUpper is used so that statically initialized maps can be in uppercase.
+func ircToCanon(ident string) string {
+ var canon []byte
+ for _, c := range []byte(ident) {
+ canon = append(canon, ircToUpper(c))
+ }
+ return string(canon)
+}
+
+func ircEqual(s1, s2 string) bool {
+ return ircToCanon(s1) == ircToCanon(s2)
+}
+
+func ircFnmatch(pattern string, s string) bool {
+ pattern, s = ircToCanon(pattern), ircToCanon(s)
+ // FIXME: This should not support [] ranges and handle '/' specially.
+ // We could translate the pattern to a regular expression.
+ matched, _ := filepath.Match(pattern, s)
+ return matched
+}
+
+var reMsg = regexp.MustCompile(
+ `^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
+var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
+
+type message struct {
+ tags map[string]string // IRC 3.2 message tags
+ nick string // optional nickname
+ user string // optional username
+ host string // optional hostname or IP address
+ command string // command name
+ params []string // arguments
+}
+
+func ircUnescapeMessageTag(value string) string {
+ var buf []byte
+ escape := false
+ for i := 0; i < len(value); i++ {
+ if escape {
+ switch value[i] {
+ case ':':
+ buf = append(buf, ';')
+ case 's':
+ buf = append(buf, ' ')
+ case 'r':
+ buf = append(buf, '\r')
+ case 'n':
+ buf = append(buf, '\n')
+ default:
+ buf = append(buf, value[i])
+ }
+ escape = false
+ } else if value[i] == '\\' {
+ escape = true
+ } else {
+ buf = append(buf, value[i])
+ }
+ }
+ return string(buf)
+}
+
+func ircParseMessageTags(tags string, out map[string]string) {
+ for _, tag := range splitString(tags, ";", true /* ignoreEmpty */) {
+ if equal := strings.IndexByte(tag, '='); equal < 0 {
+ out[tag] = ""
+ } else {
+ out[tag[:equal]] = ircUnescapeMessageTag(tag[equal+1:])
+ }
+ }
+}
+
+func ircParseMessage(line string) *message {
+ m := reMsg.FindStringSubmatch(line)
+ if m == nil {
+ return nil
+ }
+
+ msg := message{nil, m[2], m[3], m[4], m[5], nil}
+ if m[1] != "" {
+ msg.tags = make(map[string]string)
+ ircParseMessageTags(m[1], msg.tags)
+ }
+ for _, x := range reArgs.FindAllString(m[6], -1) {
+ msg.params = append(msg.params, x[1:])
+ }
+ return &msg
+
+}
+
+// --- IRC token validation ----------------------------------------------------
+
+// Everything as per RFC 2812
+const (
+ ircMaxNickname = 9
+ ircMaxHostname = 63
+ ircMaxChannelName = 50
+ ircMaxMessageLength = 510
+)
+
+const (
+ reClassSpecial = "\\[\\]\\\\`_^{|}"
+ // "shortname" from RFC 2812 doesn't work how its author thought it would.
+ reShortname = "[0-9A-Za-z](-*[0-9A-Za-z])*"
+)
+
+var (
+ reHostname = regexp.MustCompile(
+ `^` + reShortname + `(\.` + reShortname + `)*$`)
+
+ // Extending ASCII to the whole range of Unicode letters.
+ reNickname = regexp.MustCompile(
+ `^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
+
+ // Notably, this won't match invalid UTF-8 characters, although the
+ // behaviour seems to be unstated in the documentation.
+ reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)
+
+ reChannelName = regexp.MustCompile(`^[^\0\007\r\n ,:]+$`)
+ reKey = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`)
+ reUserMask = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`)
+ reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
+)
+
+func ircValidateHostname(hostname string) error {
+ if hostname == "" {
+ return errors.New("the value is empty")
+ }
+ if !reHostname.MatchString(hostname) {
+ return errors.New("invalid format")
+ }
+ if len(hostname) > ircMaxHostname {
+ return errors.New("the value is too long")
+ }
+ return nil
+}
+
+func ircIsValidNickname(nickname string) bool {
+ return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
+}
+
+func ircIsValidUsername(username string) bool {
+ // XXX: We really should put a limit on this
+ // despite the RFC not mentioning one.
+ return reUsername.MatchString(username)
+}
+
+func ircIsValidChannelName(name string) bool {
+ return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
+}
+
+func ircIsValidKey(key string) bool {
+ // XXX: Should be 7-bit as well but whatever.
+ return reKey.MatchString(key)
+}
+
+func ircIsValidUserMask(mask string) bool {
+ return reUserMask.MatchString(mask)
+}
+
+func ircIsValidFingerprint(fp string) bool {
+ return reFingerprint.MatchString(fp)
+}
+
+// --- Clients (equals users) --------------------------------------------------
+
+type connCloseWriter interface {
+ net.Conn
+ CloseWrite() error
+}
+
+const ircSupportedUserModes = "aiwros"
+
+const (
+ ircUserModeInvisible uint = 1 << iota
+ ircUserModeRxWallops
+ ircUserModeRestricted
+ ircUserModeOperator
+ ircUserModeRxServerNotices
+)
+
+const (
+ ircCapMultiPrefix uint = 1 << iota
+ ircCapInviteNotify
+ ircCapEchoMessage
+ ircCapUserhostInNames
+ ircCapServerTime
+)
+
+type client struct {
+ transport net.Conn // underlying connection
+ tls *tls.Conn // TLS, if detected
+ conn connCloseWriter // high-level connection
+ recvQ []byte // unprocessed input
+ sendQ []byte // unprocessed output
+ reading bool // whether a reading goroutine is running
+ writing bool // whether a writing goroutine is running
+ closing bool // whether we're closing the connection
+ killTimer *time.Timer // hard kill timeout
+
+ opened time.Time // when the connection was opened
+ nSentMessages uint // number of sent messages total
+ sentBytes int // number of sent bytes total
+ nReceivedMessages uint // number of received messages total
+ receivedBytes int // number of received bytes total
+
+ hostname string // hostname or IP shown to the network
+ port string // port of the peer as a string
+ address string // full network address
+
+ pingTimer *time.Timer // we should send a PING
+ timeoutTimer *time.Timer // connection seems to be dead
+ registered bool // the user has registered
+ capNegotiating bool // negotiating capabilities
+ capsEnabled uint // enabled capabilities
+ capVersion uint // CAP protocol version
+
+ tlsCertFingerprint string // client certificate fingerprint
+
+ nickname string // IRC nickname (main identifier)
+ username string // IRC username
+ realname string // IRC realname (or e-mail)
+
+ mode uint // user's mode
+ awayMessage string // away message
+ lastActive time.Time // last PRIVMSG, to get idle time
+ invites map[string]bool // channel invitations by operators
+ antiflood *floodDetector // flood detector
+}
+
+// --- Channels ----------------------------------------------------------------
+
+const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
+
+const (
+ ircChanModeInviteOnly uint = 1 << iota
+ ircChanModeModerated
+ ircChanModeNoOutsideMsgs
+ ircChanModeQuiet
+ ircChanModePrivate
+ ircChanModeSecret
+ ircChanModeProtectedTopic
+
+ ircChanModeOperator
+ ircChanModeVoice
+)
+
+type channel struct {
+ name string // channel name
+ modes uint // channel modes
+ key string // channel key
+ userLimit int // user limit or -1
+ created time.Time // creation time
+
+ topic string // channel topic
+ topicWho string // who set the topic
+ topicTime time.Time // when the topic was set
+
+ userModes map[*client]uint // modes for all channel users
+
+ banList []string // ban list
+ exceptionList []string // exceptions from bans
+ inviteList []string // exceptions from +I
+}
+
+func (ch *channel) getMode(discloseSecrets bool) string {
+ var buf []byte
+ if 0 != ch.modes&ircChanModeInviteOnly {
+ buf = append(buf, 'i')
+ }
+ if 0 != ch.modes&ircChanModeModerated {
+ buf = append(buf, 'm')
+ }
+ if 0 != ch.modes&ircChanModeNoOutsideMsgs {
+ buf = append(buf, 'n')
+ }
+ if 0 != ch.modes&ircChanModeQuiet {
+ buf = append(buf, 'q')
+ }
+ if 0 != ch.modes&ircChanModePrivate {
+ buf = append(buf, 'p')
+ }
+ if 0 != ch.modes&ircChanModeSecret {
+ buf = append(buf, 's')
+ }
+ if 0 != ch.modes&ircChanModeProtectedTopic {
+ buf = append(buf, 'r')
+ }
+
+ if ch.userLimit != -1 {
+ buf = append(buf, 'l')
+ }
+ if ch.key != "" {
+ buf = append(buf, 'k')
+ }
+
+ // XXX: Is it correct to split it? Try it on an existing implementation.
+ if discloseSecrets {
+ if ch.userLimit != -1 {
+ buf = append(buf, fmt.Sprintf(" %d", ch.userLimit)...)
+ }
+ if ch.key != "" {
+ buf = append(buf, fmt.Sprintf(" %s", ch.key)...)
+ }
+ }
+ return string(buf)
+}
+
+// --- IRC server context ------------------------------------------------------
+
+type whowasInfo struct {
+ nickname, username, realname, hostname string
+}
+
+func newWhowasInfo(c *client) *whowasInfo {
+ return &whowasInfo{
+ nickname: c.nickname,
+ username: c.username,
+ realname: c.realname,
+ hostname: c.hostname,
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+type ircCommand struct {
+ requiresRegistration bool
+ handler func(*message, *client)
+
+ nReceived uint // number of commands received
+ bytesReceived int // number of bytes received total
+}
+
+type preparedEvent struct {
+ client *client
+ host string // client's hostname or literal IP address
+ isTLS bool // the client seems to use TLS
+}
+
+type readEvent struct {
+ client *client
+ data []byte // new data from the client
+ err error // read error
+}
+
+type writeEvent struct {
+ client *client
+ written int // amount of bytes written
+ err error // write error
+}
+
+// TODO: Maybe we want to keep it in a struct?
+// A better question might be: can we run multiple instances of it?
+var (
+ // network
+
+ listeners []net.Listener
+ clients = make(map[*client]bool)
+
+ // IRC state
+
+ // XXX: Beware that maps with identifier keys need to be indexed correctly.
+ // We might want to enforce accessor functions for users and channels.
+
+ started time.Time // when the server has been started
+ users = make(map[string]*client) // maps nicknames to clients
+ channels = make(map[string]*channel) // maps channel names to data
+ whowas = make(map[string]*whowasInfo) // WHOWAS registry
+
+ // event loop
+
+ quitting bool
+ quitTimer <-chan time.Time
+
+ sigs = make(chan os.Signal, 1)
+ conns = make(chan net.Conn)
+ prepared = make(chan preparedEvent)
+ reads = make(chan readEvent)
+ writes = make(chan writeEvent)
+ timers = make(chan func())
+
+ // configuration
+
+ config simpleConfig // server configuration
+ tlsConf *tls.Config // TLS connection configuration
+ serverName string // our server name
+ pingInterval time.Duration // ping interval
+ maxConnections int // max connections allowed or 0
+ motd []string // MOTD (none if empty)
+ catalog map[int]string // message catalog for server msgs
+ operators map[string]bool // TLS certificate fingerprints for IRCops
+)
+
+// Forcefully tear down all connections.
+func forceQuit(reason string) {
+ if !quitting {
+ exitFatal("forceQuit called without initiateQuit")
+ }
+
+ printStatus("forced shutdown (%s)", reason)
+ for c := range clients {
+ // initiateQuit has already unregistered the client.
+ c.kill("Shutting down")
+ }
+}
+
+// Initiate a clean shutdown of the whole daemon.
+func initiateQuit() {
+ printStatus("shutting down")
+ for _, ln := range listeners {
+ if err := ln.Close(); err != nil {
+ printError("%s", err)
+ }
+ }
+ for c := range clients {
+ c.closeLink("Shutting down")
+ }
+
+ quitTimer = time.After(5 * time.Second)
+ quitting = true
+}
+
+func ircChannelCreate(name string) *channel {
+ ch := &channel{
+ name: name,
+ userLimit: -1,
+ created: time.Now(),
+ userModes: make(map[*client]uint),
+ }
+ channels[ircToCanon(name)] = ch
+ return ch
+}
+
+func ircChannelDestroyIfEmpty(ch *channel) {
+ if len(ch.userModes) == 0 {
+ delete(channels, ircToCanon(ch.name))
+ }
+}
+
+func ircNotifyRoommates(c *client, message string) {
+ targets := make(map[*client]bool)
+ for _, ch := range channels {
+ _, present := ch.userModes[c]
+ if !present || 0 != ch.modes&ircChanModeQuiet {
+ continue
+ }
+ for client := range ch.userModes {
+ targets[client] = true
+ }
+ }
+
+ for roommate := range targets {
+ if roommate != c {
+ roommate.send(message)
+ }
+ }
+}
+
+// --- Clients (continued) -----------------------------------------------------
+
+func (c *client) printDebug(format string, args ...interface{}) {
+ if debugMode {
+ printDebug("(%s) %s", c.address, fmt.Sprintf(format, args...))
+ }
+}
+
+func ircAppendClientModes(m uint, mode []byte) []byte {
+ if 0 != m&ircUserModeInvisible {
+ mode = append(mode, 'i')
+ }
+ if 0 != m&ircUserModeRxWallops {
+ mode = append(mode, 'w')
+ }
+ if 0 != m&ircUserModeRestricted {
+ mode = append(mode, 'r')
+ }
+ if 0 != m&ircUserModeOperator {
+ mode = append(mode, 'o')
+ }
+ if 0 != m&ircUserModeRxServerNotices {
+ mode = append(mode, 's')
+ }
+ return mode
+}
+
+func (c *client) getMode() string {
+ var mode []byte
+ if c.awayMessage != "" {
+ mode = append(mode, 'a')
+ }
+ return string(ircAppendClientModes(c.mode, mode))
+}
+
+func (c *client) send(line string) {
+ if c.conn == nil || c.closing {
+ return
+ }
+
+ oldSendQLen := len(c.sendQ)
+
+ // So far there's only one message tag we use, so we can do it simple;
+ // note that a 1024-character limit applies to messages with tags on.
+ if 0 != c.capsEnabled&ircCapServerTime {
+ c.sendQ = time.Now().UTC().
+ AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ")
+ }
+
+ bytes := []byte(line)
+ if len(bytes) > ircMaxMessageLength {
+ bytes = bytes[:ircMaxMessageLength]
+ }
+
+ c.printDebug("<- %s", bytes)
+
+ // TODO: Kill the connection above some "SendQ" threshold (careful!)
+ c.sendQ = append(c.sendQ, bytes...)
+ c.sendQ = append(c.sendQ, "\r\n"...)
+ c.flushSendQ()
+
+ // Technically we haven't sent it yet but that's a minor detail
+ c.nSentMessages++
+ c.sentBytes += len(c.sendQ) - oldSendQLen
+}
+
+func (c *client) sendf(format string, a ...interface{}) {
+ c.send(fmt.Sprintf(format, a...))
+}
+
+func (c *client) addToWhowas() {
+ // Only keeping one entry for each nickname.
+ // TODO: Make sure this list doesn't get too long, for example by
+ // putting them in a linked list ordered by time.
+ whowas[ircToCanon(c.nickname)] = newWhowasInfo(c)
+}
+
+func (c *client) nicknameOrStar() string {
+ if c.nickname == "" {
+ return "*"
+ }
+ return c.nickname
+}
+
+func (c *client) unregister(reason string) {
+ if !c.registered {
+ return
+ }
+
+ ircNotifyRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
+ c.nickname, c.username, c.hostname, reason))
+
+ // The QUIT message will take care of state on clients.
+ for _, ch := range channels {
+ delete(ch.userModes, c)
+ ircChannelDestroyIfEmpty(ch)
+ }
+
+ c.addToWhowas()
+ delete(users, ircToCanon(c.nickname))
+ c.nickname = ""
+ c.registered = false
+}
+
+// Close the connection and forget about the client.
+func (c *client) kill(reason string) {
+ if reason == "" {
+ c.unregister("Client exited")
+ } else {
+ c.unregister(reason)
+ }
+
+ c.printDebug("client destroyed (%s)", reason)
+
+ // Try to send a "close notify" alert if the TLS object is ready,
+ // otherwise just tear down the transport.
+ if c.conn != nil {
+ _ = c.conn.Close()
+ } else {
+ _ = c.transport.Close()
+ }
+
+ c.cancelTimers()
+ delete(clients, c)
+}
+
+// Tear down the client connection, trying to do so in a graceful manner.
+func (c *client) closeLink(reason string) {
+ // Let's just cut the connection, the client can try again later.
+ // We also want to avoid accidentally writing to the socket before
+ // address resolution has finished.
+ if c.conn == nil {
+ c.kill(reason)
+ return
+ }
+ if c.closing {
+ return
+ }
+
+ // We push an "ERROR" message to the write buffer and let the writer send
+ // it, with some arbitrary timeout. The "closing" state makes sure
+ // that a/ we ignore any successive messages, and b/ that the connection
+ // is killed after the write buffer is transferred and emptied.
+ // (Since we send this message, we don't need to call CloseWrite here.)
+ c.sendf("ERROR :Closing link: %s[%s] (%s)",
+ c.nicknameOrStar(), c.hostname /* TODO host IP? */, reason)
+ c.closing = true
+
+ c.unregister(reason)
+ c.setKillTimer()
+}
+
+func (c *client) inMaskList(masks []string) bool {
+ client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
+ for _, mask := range masks {
+ if ircFnmatch(mask, client) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *client) getTLSCertFingerprint() string {
+ if c.tls == nil {
+ return ""
+ }
+
+ peerCerts := c.tls.ConnectionState().PeerCertificates
+ if len(peerCerts) == 0 {
+ return ""
+ }
+
+ hash := sha256.Sum256(peerCerts[0].Raw)
+ return hex.EncodeToString(hash[:])
+}
+
+// --- Timers ------------------------------------------------------------------
+
+// Free the resources of timers that haven't fired yet and for timers that are
+// in between firing and being collected by the event loop, mark that the event
+// should not be acted upon.
+func (c *client) cancelTimers() {
+ for _, timer := range []**time.Timer{
+ &c.killTimer, &c.timeoutTimer, &c.pingTimer,
+ } {
+ if *timer != nil {
+ (*timer).Stop()
+ *timer = nil
+ }
+ }
+}
+
+// Arrange for a function to be called later from the main goroutine.
+func (c *client) setTimer(timer **time.Timer, delay time.Duration, cb func()) {
+ c.cancelTimers()
+
+ var identityCapture *time.Timer
+ identityCapture = time.AfterFunc(delay, func() {
+ timers <- func() {
+ // The timer might have been cancelled or even replaced.
+ // When the client is killed, this will be nil.
+ if *timer == identityCapture {
+ cb()
+ }
+ }
+ })
+
+ *timer = identityCapture
+}
+
+func (c *client) setKillTimer() {
+ c.setTimer(&c.killTimer, pingInterval, func() {
+ c.kill("Timeout")
+ })
+}
+
+func (c *client) setTimeoutTimer() {
+ c.setTimer(&c.timeoutTimer, pingInterval, func() {
+ c.closeLink(fmt.Sprintf("Ping timeout: >%d seconds",
+ pingInterval/time.Second))
+ })
+}
+
+func (c *client) setPingTimer() {
+ c.setTimer(&c.pingTimer, pingInterval, func() {
+ c.sendf("PING :%s", serverName)
+ c.setTimeoutTimer()
+ })
+}
+
+// --- IRC command handling ----------------------------------------------------
+
+func (c *client) makeReply(id int, ap ...interface{}) string {
+ s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar())
+ if reply, ok := catalog[id]; ok {
+ return s + fmt.Sprintf(reply, ap...)
+ }
+ return s + fmt.Sprintf(defaultReplies[id], ap...)
+}
+
+// XXX: This way simple static analysis cannot typecheck the arguments, so we
+// need to be careful.
+func (c *client) sendReply(id int, args ...interface{}) {
+ c.send(c.makeReply(id, args...))
+}
+
+// Send a space-separated list of words across as many replies as needed.
+func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
+ common := c.makeReply(id, args...)
+
+ // We always send at least one message (there might be a client that
+ // expects us to send this message at least once).
+ if len(items) == 0 {
+ items = append(items, "")
+ }
+
+ for len(items) > 0 {
+ // If not even a single item fits in the limit (which may happen,
+ // in theory) it just gets cropped. We could also skip it.
+ reply := append([]byte(common), items[0]...)
+ items = items[1:]
+
+ // Append as many items as fits in a single message.
+ for len(items) > 0 &&
+ len(reply)+1+len(items[0]) <= ircMaxMessageLength {
+ reply = append(reply, ' ')
+ reply = append(reply, items[0]...)
+ items = items[1:]
+ }
+
+ c.send(string(reply))
+ }
+}
+
+func (c *client) sendMOTD() {
+ if len(motd) == 0 {
+ c.sendReply(ERR_NOMOTD)
+ return
+ }
+
+ c.sendReply(RPL_MOTDSTART, serverName)
+ for _, line := range motd {
+ c.sendReply(RPL_MOTD, line)
+ }
+ c.sendReply(RPL_ENDOFMOTD)
+}
+
+func (c *client) sendLUSERS() {
+ nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
+ for c := range clients {
+ if c.registered {
+ nUsers++
+ } else {
+ nUnknown++
+ }
+ if 0 != c.mode&ircUserModeOperator {
+ nOpers++
+ }
+ }
+
+ nChannels := 0
+ for _, ch := range channels {
+ if 0 != ch.modes&ircChanModeSecret {
+ nChannels++
+ }
+ }
+
+ c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
+ if nOpers != 0 {
+ c.sendReply(RPL_LUSEROP, nOpers)
+ }
+ if nUnknown != 0 {
+ c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
+ }
+ if nChannels != 0 {
+ c.sendReply(RPL_LUSERCHANNELS, nChannels)
+ }
+ c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
+}
+
+func ircIsThisMe(target string) bool {
+ // Target servers can also be matched by their users
+ if ircFnmatch(target, serverName) {
+ return true
+ }
+ _, ok := users[ircToCanon(target)]
+ return ok
+}
+
+func (c *client) sendISUPPORT() {
+ // Only # channels, +e supported, +I supported, unlimited arguments to MODE.
+ c.sendReply(RPL_ISUPPORT, fmt.Sprintf("CHANTYPES=# EXCEPTS INVEX MODES"+
+ " TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
+ " NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName))
+}
+
+func (c *client) tryFinishRegistration() {
+ if c.registered || c.capNegotiating {
+ return
+ }
+ if c.nickname == "" || c.username == "" {
+ return
+ }
+
+ c.registered = true
+ c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
+
+ c.sendReply(RPL_YOURHOST, serverName, projectVersion)
+ // The purpose of this message eludes me.
+ c.sendReply(RPL_CREATED, started.Format("Mon, 02 Jan 2006"))
+ c.sendReply(RPL_MYINFO, serverName, projectVersion,
+ ircSupportedUserModes, ircSupportedChanModes)
+
+ c.sendISUPPORT()
+ c.sendLUSERS()
+ c.sendMOTD()
+
+ if mode := c.getMode(); mode != "" {
+ c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
+ }
+
+ c.tlsCertFingerprint = c.getTLSCertFingerprint()
+ if c.tlsCertFingerprint != "" {
+ c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
+ serverName, c.nickname, c.tlsCertFingerprint)
+ }
+
+ delete(whowas, ircToCanon(c.nickname))
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// IRCv3 capability negotiation. See http://ircv3.org for details.
+
+type ircCapArgs struct {
+ subcommand string // the subcommand being processed
+ fullParams string // whole parameter string
+ params []string // split parameters
+ target string // target parameter for replies
+}
+
+var ircCapTable = []struct {
+ flag uint // flag
+ name string // name of the capability
+}{
+ {ircCapMultiPrefix, "multi-prefix"},
+ {ircCapInviteNotify, "invite-notify"},
+ {ircCapEchoMessage, "echo-message"},
+ {ircCapUserhostInNames, "userhost-in-names"},
+ {ircCapServerTime, "server-time"},
+}
+
+func (c *client) handleCAPLS(a *ircCapArgs) {
+ if len(a.params) == 1 {
+ if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "Ignoring invalid protocol version number")
+ } else {
+ c.capVersion = uint(ver)
+ }
+ }
+
+ c.capNegotiating = true
+ c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
+ " userhost-in-names server-time", serverName, a.target)
+}
+
+func (c *client) handleCAPLIST(a *ircCapArgs) {
+ caps := []string{}
+ for _, cap := range ircCapTable {
+ if 0 != c.capsEnabled&cap.flag {
+ caps = append(caps, cap.name)
+ }
+ }
+
+ c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
+ strings.Join(caps, " "))
+}
+
+func ircDecodeCapability(name string) uint {
+ for _, cap := range ircCapTable {
+ if cap.name == name {
+ return cap.flag
+ }
+ }
+ return 0
+}
+
+func (c *client) handleCAPREQ(a *ircCapArgs) {
+ c.capNegotiating = true
+
+ newCaps := c.capsEnabled
+ ok := true
+ for _, param := range a.params {
+ removing := false
+ name := param
+ if name[:1] == "-" {
+ removing = true
+ name = name[1:]
+ }
+
+ if cap := ircDecodeCapability(name); cap == 0 {
+ ok = false
+ } else if removing {
+ newCaps &= ^cap
+ } else {
+ newCaps |= cap
+ }
+ }
+
+ if ok {
+ c.capsEnabled = newCaps
+ c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
+ } else {
+ c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
+ }
+}
+
+func (c *client) handleCAPACK(a *ircCapArgs) {
+ if len(a.params) > 0 {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "No acknowledgable capabilities supported")
+ }
+}
+
+func (c *client) handleCAPEND(a *ircCapArgs) {
+ c.capNegotiating = false
+ c.tryFinishRegistration()
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+var ircCapHandlers = map[string]func(*client, *ircCapArgs){
+ "LS": (*client).handleCAPLS,
+ "LIST": (*client).handleCAPLIST,
+ "REQ": (*client).handleCAPREQ,
+ "ACK": (*client).handleCAPACK,
+ "END": (*client).handleCAPEND,
+}
+
+// XXX: Maybe these also deserve to be methods for client? They operate on
+// global state, though.
+
+func ircParseWEBIRCOptions(options string, out map[string]string) {
+ for _, option := range strings.Split(options, " ") {
+ if equal := strings.IndexByte(option, '='); equal < 0 {
+ out[option] = ""
+ } else {
+ out[option[:equal]] = ircUnescapeMessageTag(option[equal+1:])
+ }
+ }
+}
+
+func ircHandleWEBIRC(msg *message, c *client) {
+ if len(msg.params) < 4 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ password, gateway, hostname := msg.params[0], msg.params[1], msg.params[2]
+ if config["webirc_password"] != password {
+ c.closeLink("Invalid WebIRC password")
+ return
+ }
+
+ options := make(map[string]string)
+ if len(msg.params) >= 5 {
+ ircParseWEBIRCOptions(msg.params[4], options)
+ }
+
+ c.hostname = hostname
+ c.port = "WebIRC-" + gateway
+ c.address = net.JoinHostPort(hostname, c.port)
+
+ // Note that this overrides the gateway's certificate, conditionally.
+ fp, _ := options["certfp-sha-256"]
+ if _, secure := options["secure"]; secure && ircIsValidFingerprint(fp) {
+ c.tlsCertFingerprint = strings.ToLower(fp)
+ }
+}
+
+func ircHandleCAP(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ args := &ircCapArgs{
+ target: c.nicknameOrStar(),
+ subcommand: msg.params[0],
+ fullParams: "",
+ params: []string{},
+ }
+
+ if len(msg.params) > 1 {
+ args.fullParams = msg.params[1]
+ args.params = splitString(args.fullParams, " ", true)
+ }
+
+ if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok {
+ c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
+ "Invalid CAP subcommand")
+ } else {
+ fn(c, args)
+ }
+}
+
+func ircHandlePASS(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ }
+
+ // We have TLS client certificates for this purpose; ignoring.
+}
+
+func ircHandleNICK(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NONICKNAMEGIVEN)
+ return
+ }
+
+ nickname := msg.params[0]
+ if !ircIsValidNickname(nickname) {
+ c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
+ return
+ }
+
+ nicknameCanon := ircToCanon(nickname)
+ if client, ok := users[nicknameCanon]; ok && client != c {
+ c.sendReply(ERR_NICKNAMEINUSE, nickname)
+ return
+ }
+
+ if c.registered {
+ c.addToWhowas()
+
+ message := fmt.Sprintf(":%s!%s@%s NICK :%s",
+ c.nickname, c.username, c.hostname, nickname)
+ ircNotifyRoommates(c, message)
+ c.send(message)
+ }
+
+ // Release the old nickname and allocate a new one.
+ if c.nickname != "" {
+ delete(users, ircToCanon(c.nickname))
+ }
+
+ c.nickname = nickname
+ users[nicknameCanon] = c
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSER(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ return
+ }
+ if len(msg.params) < 4 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
+
+ // Unfortunately, the protocol doesn't give us any means of rejecting it.
+ if !ircIsValidUsername(username) {
+ username = "*"
+ }
+
+ c.username = username
+ c.realname = realname
+
+ c.mode = 0
+ if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
+ if 0 != m&4 {
+ c.mode |= ircUserModeRxWallops
+ }
+ if 0 != m&8 {
+ c.mode |= ircUserModeInvisible
+ }
+ }
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSERHOST(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ var reply []byte
+ for i := 0; i < 5 && i < len(msg.params); i++ {
+ nick := msg.params[i]
+ target := users[ircToCanon(nick)]
+ if target == nil {
+ continue
+ }
+ if i != 0 {
+ reply = append(reply, ' ')
+ }
+
+ reply = append(reply, nick...)
+ if 0 != target.mode&ircUserModeOperator {
+ reply = append(reply, '*')
+ }
+
+ if target.awayMessage != "" {
+ reply = append(reply, "=-"...)
+ } else {
+ reply = append(reply, "=+"...)
+ }
+ reply = append(reply, (target.username + "@" + target.hostname)...)
+ }
+ c.sendReply(RPL_USERHOST, string(reply))
+}
+
+func ircHandleLUSERS(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendLUSERS()
+}
+
+func ircHandleMOTD(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendMOTD()
+}
+
+func ircHandlePING(msg *message, c *client) {
+ // XXX: The RFC is pretty incomprehensible about the exact usage.
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ } else {
+ c.sendf(":%s PONG :%s", serverName, msg.params[0])
+ }
+}
+
+func ircHandlePONG(msg *message, c *client) {
+ // We are the only server, so we don't have to care too much.
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ return
+ }
+
+ // Set a new timer to send another PING
+ c.setPingTimer()
+}
+
+func ircHandleQUIT(msg *message, c *client) {
+ reason := c.nickname
+ if len(msg.params) > 0 {
+ reason = msg.params[0]
+ }
+
+ c.closeLink("Quit: " + reason)
+}
+
+func ircHandleTIME(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ c.sendReply(RPL_TIME, serverName,
+ time.Now().Format("Mon Jan _2 2006 15:04:05"))
+}
+
+func ircHandleVERSION(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ postVersion := 0
+ if debugMode {
+ postVersion = 1
+ }
+
+ c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName,
+ projectName+" "+projectVersion)
+ c.sendISUPPORT()
+}
+
+func ircChannelMulticast(ch *channel, msg string, except *client) {
+ for c := range ch.userModes {
+ if c != except {
+ c.send(msg)
+ }
+ }
+}
+
+func ircModifyMode(mask *uint, mode uint, add bool) bool {
+ orig := *mask
+ if add {
+ *mask |= mode
+ } else {
+ *mask &= ^mode
+ }
+ return *mask != orig
+}
+
+func ircUpdateUserMode(c *client, newMode uint) {
+ oldMode := c.mode
+ c.mode = newMode
+
+ added, removed := newMode & ^oldMode, oldMode & ^newMode
+
+ var diff []byte
+ if added != 0 {
+ diff = append(diff, '+')
+ diff = ircAppendClientModes(added, diff)
+ }
+ if removed != 0 {
+ diff = append(diff, '-')
+ diff = ircAppendClientModes(removed, diff)
+ }
+
+ if len(diff) > 0 {
+ c.sendf(":%s MODE %s :%s", c.nickname, c.nickname, string(diff))
+ }
+}
+
+func ircHandleUserModeChange(c *client, modeString string) {
+ newMode := c.mode
+ adding := true
+
+ for _, flag := range modeString {
+ switch flag {
+ case '+':
+ adding = true
+ case '-':
+ adding = false
+
+ case 'a':
+ // Ignore, the client should use AWAY.
+ case 'i':
+ ircModifyMode(&newMode, ircUserModeInvisible, adding)
+ case 'w':
+ ircModifyMode(&newMode, ircUserModeRxWallops, adding)
+ case 'r':
+ // It's not possible ot un-restrict yourself.
+ if adding {
+ newMode |= ircUserModeRestricted
+ }
+ case 'o':
+ if !adding {
+ newMode &= ^ircUserModeOperator
+ } else if operators[c.tlsCertFingerprint] {
+ newMode |= ircUserModeOperator
+ } else {
+ c.sendf(":%s NOTICE %s :Either you're not using an TLS"+
+ " client certificate, or the fingerprint doesn't match",
+ serverName, c.nickname)
+ }
+ case 's':
+ ircModifyMode(&newMode, ircUserModeRxServerNotices, adding)
+ default:
+ c.sendReply(ERR_UMODEUNKNOWNFLAG)
+ return
+ }
+ }
+ ircUpdateUserMode(c, newMode)
+}
+
+func ircSendChannelList(c *client, channelName string, list []string,
+ reply, endReply int) {
+ for _, line := range list {
+ c.sendReply(reply, channelName, line)
+ }
+ c.sendReply(endReply, channelName)
+}
+
+func ircCheckExpandUserMask(mask string) string {
+ var result []byte
+ result = append(result, mask...)
+
+ // Make sure it is a complete mask.
+ if bytes.IndexByte(result, '!') < 0 {
+ result = append(result, "!*"...)
+ }
+ if bytes.IndexByte(result, '@') < 0 {
+ result = append(result, "@*"...)
+ }
+
+ // And validate whatever the result is.
+ s := string(result)
+ if !ircIsValidUserMask(s) {
+ return ""
+ }
+
+ return s
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// Channel MODE command handling. This is by far the worst command to implement
+// from the whole RFC; don't blame me if it doesn't work exactly as expected.
+
+type modeProcessor struct {
+ params []string // mode string parameters
+
+ c *client // who does the changes
+ ch *channel // the channel we're modifying
+ present bool // c present on ch
+ modes uint // channel user modes
+
+ adding bool // currently adding modes
+ modeChar byte // currently processed mode char
+
+ added []byte // added modes
+ removed []byte // removed modes
+ output *[]byte // "added" or "removed"
+
+ addedParams []string // params for added modes
+ removedParams []string // params for removed modes
+ outputParams *[]string // "addedParams" or "removedParams"
+}
+
+func (mp *modeProcessor) nextParam() string {
+ if len(mp.params) == 0 {
+ return ""
+ }
+
+ param := mp.params[0]
+ mp.params = mp.params[1:]
+ return param
+}
+
+func (mp *modeProcessor) checkOperator() bool {
+ if (mp.present && 0 != mp.modes&ircChanModeOperator) ||
+ 0 != mp.c.mode&ircUserModeOperator {
+ return true
+ }
+
+ mp.c.sendReply(ERR_CHANOPRIVSNEEDED, mp.ch.name)
+ return false
+}
+
+func (mp *modeProcessor) doUser(mode uint) {
+ target := mp.nextParam()
+ if !mp.checkOperator() || target == "" {
+ return
+ }
+
+ if client := users[ircToCanon(target)]; client == nil {
+ mp.c.sendReply(ERR_NOSUCHNICK, target)
+ } else if modes, present := mp.ch.userModes[client]; !present {
+ mp.c.sendReply(ERR_USERNOTINCHANNEL, target, mp.ch.name)
+ } else if ircModifyMode(&modes, mode, mp.adding) {
+ mp.ch.userModes[client] = modes
+ *mp.output = append(*mp.output, mp.modeChar)
+ *mp.outputParams = append(*mp.outputParams, client.nickname)
+ }
+}
+
+func (mp *modeProcessor) doChan(mode uint) bool {
+ if !mp.checkOperator() || !ircModifyMode(&mp.ch.modes, mode, mp.adding) {
+ return false
+ }
+ *mp.output = append(*mp.output, mp.modeChar)
+ return true
+}
+
+func (mp *modeProcessor) doChanRemove(modeChar byte, mode uint) {
+ if mp.adding && ircModifyMode(&mp.ch.modes, mode, false) {
+ mp.removed = append(mp.removed, modeChar)
+ }
+}
+
+func (mp *modeProcessor) doList(list *[]string, listMsg, endMsg int) {
+ target := mp.nextParam()
+ if target == "" {
+ if mp.adding {
+ ircSendChannelList(mp.c, mp.ch.name, *list, listMsg, endMsg)
+ }
+ return
+ }
+
+ if !mp.checkOperator() {
+ return
+ }
+
+ mask := ircCheckExpandUserMask(target)
+ if mask == "" {
+ return
+ }
+
+ var i int
+ for i = 0; i < len(*list); i++ {
+ if ircEqual((*list)[i], mask) {
+ break
+ }
+ }
+
+ found := i < len(*list)
+ if found != mp.adding {
+ if mp.adding {
+ *list = append(*list, mask)
+ } else {
+ *list = append((*list)[:i], (*list)[i+1:]...)
+ }
+
+ *mp.output = append(*mp.output, mp.modeChar)
+ *mp.outputParams = append(*mp.outputParams, mask)
+ }
+}
+
+func (mp *modeProcessor) doKey() {
+ target := mp.nextParam()
+ if !mp.checkOperator() || target == "" {
+ return
+ }
+
+ if !mp.adding {
+ if mp.ch.key == "" || !ircEqual(target, mp.ch.key) {
+ return
+ }
+
+ mp.removed = append(mp.removed, mp.modeChar)
+ mp.removedParams = append(mp.removedParams, mp.ch.key)
+ mp.ch.key = ""
+ } else if !ircIsValidKey(target) {
+ // TODO: We should notify the user somehow.
+ return
+ } else if mp.ch.key != "" {
+ mp.c.sendReply(ERR_KEYSET, mp.ch.name)
+ } else {
+ mp.ch.key = target
+ mp.added = append(mp.added, mp.modeChar)
+ mp.addedParams = append(mp.addedParams, mp.ch.key)
+ }
+}
+
+func (mp *modeProcessor) doLimit() {
+ if !mp.checkOperator() {
+ return
+ }
+
+ if !mp.adding {
+ if mp.ch.userLimit == -1 {
+ return
+ }
+
+ mp.ch.userLimit = -1
+ mp.removed = append(mp.removed, mp.modeChar)
+ } else if target := mp.nextParam(); target != "" {
+ if x, err := strconv.ParseInt(target, 10, 32); err == nil && x > 0 {
+ mp.ch.userLimit = int(x)
+ mp.added = append(mp.added, mp.modeChar)
+ mp.addedParams = append(mp.addedParams, target)
+ }
+ }
+}
+
+func (mp *modeProcessor) step(modeChar byte) bool {
+ mp.modeChar = modeChar
+ switch mp.modeChar {
+ case '+':
+ mp.adding = true
+ mp.output = &mp.added
+ mp.outputParams = &mp.addedParams
+ case '-':
+ mp.adding = false
+ mp.output = &mp.removed
+ mp.outputParams = &mp.removedParams
+
+ case 'o':
+ mp.doUser(ircChanModeOperator)
+ case 'v':
+ mp.doUser(ircChanModeVoice)
+
+ case 'i':
+ mp.doChan(ircChanModeInviteOnly)
+ case 'm':
+ mp.doChan(ircChanModeModerated)
+ case 'n':
+ mp.doChan(ircChanModeNoOutsideMsgs)
+ case 'q':
+ mp.doChan(ircChanModeQuiet)
+ case 't':
+ mp.doChan(ircChanModeProtectedTopic)
+
+ case 'p':
+ if mp.doChan(ircChanModePrivate) {
+ mp.doChanRemove('s', ircChanModeSecret)
+ }
+ case 's':
+ if mp.doChan(ircChanModeSecret) {
+ mp.doChanRemove('p', ircChanModePrivate)
+ }
+
+ case 'b':
+ mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST)
+ case 'e':
+ mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST)
+ case 'I':
+ mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST)
+
+ case 'k':
+ mp.doKey()
+ case 'l':
+ mp.doLimit()
+
+ default:
+ // It's not safe to continue, results could be undesired.
+ mp.c.sendReply(ERR_UNKNOWNMODE, modeChar, mp.ch.name)
+ return false
+ }
+ return true
+}
+
+func ircHandleChanModeChange(c *client, ch *channel, params []string) {
+ modes, present := ch.userModes[c]
+ mp := &modeProcessor{
+ c: c,
+ ch: ch,
+ present: present,
+ modes: modes,
+ params: params,
+ }
+
+Outer:
+ for {
+ modeString := mp.nextParam()
+ if modeString == "" {
+ break
+ }
+
+ mp.step('+')
+ for _, modeChar := range []byte(modeString) {
+ if !mp.step(modeChar) {
+ break Outer
+ }
+ }
+ }
+
+ // TODO: Limit to three changes with parameter per command.
+ if len(mp.added) > 0 || len(mp.removed) > 0 {
+ buf := []byte(fmt.Sprintf(":%s!%s@%s MODE %s ",
+ mp.c.nickname, mp.c.username, mp.c.hostname, mp.ch.name))
+ if len(mp.added) > 0 {
+ buf = append(buf, '+')
+ buf = append(buf, mp.added...)
+ }
+ if len(mp.removed) > 0 {
+ buf = append(buf, '-')
+ buf = append(buf, mp.removed...)
+ }
+ for _, param := range mp.addedParams {
+ buf = append(buf, ' ')
+ buf = append(buf, param...)
+ }
+ for _, param := range mp.removedParams {
+ buf = append(buf, ' ')
+ buf = append(buf, param...)
+ }
+ ircChannelMulticast(mp.ch, string(buf), nil)
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+func ircHandleMODE(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ target := msg.params[0]
+ client := users[ircToCanon(target)]
+ ch := channels[ircToCanon(target)]
+
+ if client != nil {
+ if !ircEqual(target, c.nickname) {
+ c.sendReply(ERR_USERSDONTMATCH)
+ return
+ }
+
+ if len(msg.params) < 2 {
+ c.sendReply(RPL_UMODEIS, c.getMode())
+ } else {
+ ircHandleUserModeChange(c, msg.params[1])
+ }
+ } else if ch != nil {
+ if len(msg.params) < 2 {
+ _, present := ch.userModes[c]
+ c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present))
+ c.sendReply(RPL_CREATIONTIME, target, ch.created.Unix())
+ } else {
+ ircHandleChanModeChange(c, ch, msg.params[1:])
+ }
+ } else {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ }
+}
+
+func ircHandleUserMessage(msg *message, c *client,
+ command string, allowAwayReply bool) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NORECIPIENT, msg.command)
+ return
+ }
+ if len(msg.params) < 2 || msg.params[1] == "" {
+ c.sendReply(ERR_NOTEXTTOSEND)
+ return
+ }
+
+ target, text := msg.params[0], msg.params[1]
+ message := fmt.Sprintf(":%s!%s@%s %s %s :%s",
+ c.nickname, c.username, c.hostname, command, target, text)
+
+ if client := users[ircToCanon(target)]; client != nil {
+ client.send(message)
+ if allowAwayReply && client.awayMessage != "" {
+ c.sendReply(RPL_AWAY, target, client.awayMessage)
+ }
+
+ // Acknowledging a message from the client to itself would be silly.
+ if client != c && (0 != c.capsEnabled&ircCapEchoMessage) {
+ c.send(message)
+ }
+ } else if ch := channels[ircToCanon(target)]; ch != nil {
+ modes, present := ch.userModes[c]
+
+ outsider := !present && 0 != ch.modes&ircChanModeNoOutsideMsgs
+ moderated := 0 != ch.modes&ircChanModeModerated &&
+ 0 == modes&(ircChanModeVoice|ircChanModeOperator)
+ banned := c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList)
+
+ if outsider || moderated || banned {
+ c.sendReply(ERR_CANNOTSENDTOCHAN, target)
+ return
+ }
+
+ except := c
+ if 0 != c.capsEnabled&ircCapEchoMessage {
+ except = nil
+ }
+
+ ircChannelMulticast(ch, message, except)
+ } else {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ }
+}
+
+func ircHandlePRIVMSG(msg *message, c *client) {
+ ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */)
+ c.lastActive = time.Now()
+}
+
+func ircHandleNOTICE(msg *message, c *client) {
+ ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */)
+}
+
+func ircHandleLIST(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+
+ // XXX: Maybe we should skip ircUserModeInvisible from user counts.
+ if len(msg.params) == 0 {
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
+ c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
+ }
+ }
+ } else {
+ for _, target := range splitString(msg.params[0], ",", true) {
+ if ch := channels[ircToCanon(target)]; ch != nil &&
+ 0 == ch.modes&ircChanModeSecret {
+ c.sendReply(RPL_LIST, ch.name, len(ch.userModes), ch.topic)
+ }
+ }
+ }
+ c.sendReply(RPL_LISTEND)
+}
+
+func ircAppendPrefixes(c *client, modes uint, buf []byte) []byte {
+ var all []byte
+ if 0 != modes&ircChanModeOperator {
+ all = append(all, '@')
+ }
+ if 0 != modes&ircChanModeVoice {
+ all = append(all, '+')
+ }
+
+ if len(all) > 0 {
+ if 0 != c.capsEnabled&ircCapMultiPrefix {
+ buf = append(buf, all...)
+ } else {
+ buf = append(buf, all[0])
+ }
+ }
+ return buf
+}
+
+func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string {
+ result := string(ircAppendPrefixes(c, modes, nil)) + target.nickname
+ if 0 != c.capsEnabled&ircCapUserhostInNames {
+ result += fmt.Sprintf("!%s@%s", target.username, target.hostname)
+ }
+ return result
+}
+
+func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[*client]bool) {
+ kind := '='
+ if 0 != ch.modes&ircChanModeSecret {
+ kind = '@'
+ } else if 0 != ch.modes&ircChanModePrivate {
+ kind = '*'
+ }
+
+ _, present := ch.userModes[c]
+
+ var nicks []string
+ for client, modes := range ch.userModes {
+ if !present && 0 != client.mode&ircUserModeInvisible {
+ continue
+ }
+ if usedNicks != nil {
+ usedNicks[client] = true
+ }
+ nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes))
+ }
+ c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "")
+}
+
+func ircSendDisassociatedNames(c *client, usedNicks map[*client]bool) {
+ var nicks []string
+ for _, client := range users {
+ if 0 == client.mode&ircUserModeInvisible && !usedNicks[client] {
+ nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0))
+ }
+ }
+ if len(nicks) > 0 {
+ c.sendReplyVector(RPL_NAMREPLY, nicks, '*', "*", "")
+ }
+}
+
+func ircHandleNAMES(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+
+ if len(msg.params) == 0 {
+ usedNicks := make(map[*client]bool)
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) {
+ ircSendRPLNAMREPLY(c, ch, usedNicks)
+ }
+ }
+
+ // Also send all visible users we haven't listed yet.
+ ircSendDisassociatedNames(c, usedNicks)
+ c.sendReply(RPL_ENDOFNAMES, "*")
+ } else {
+ for _, target := range splitString(msg.params[0], ",", true) {
+ if ch := channels[ircToCanon(target)]; ch == nil {
+ } else if _, present := ch.userModes[c]; present ||
+ 0 == ch.modes&ircChanModeSecret {
+ ircSendRPLNAMREPLY(c, ch, nil)
+ c.sendReply(RPL_ENDOFNAMES, target)
+ }
+ }
+ }
+}
+
+func ircSendRPLWHOREPLY(c *client, ch *channel, target *client) {
+ var chars []byte
+ if target.awayMessage != "" {
+ chars = append(chars, 'G')
+ } else {
+ chars = append(chars, 'H')
+ }
+
+ if 0 != target.mode&ircUserModeOperator {
+ chars = append(chars, '*')
+ }
+
+ channelName := "*"
+ if ch != nil {
+ channelName = ch.name
+ if modes, present := ch.userModes[target]; present {
+ chars = ircAppendPrefixes(c, modes, chars)
+ }
+ }
+
+ c.sendReply(RPL_WHOREPLY, channelName,
+ target.username, target.hostname, serverName,
+ target.nickname, string(chars), 0 /* hop count */, target.realname)
+}
+
+func ircMatchSendRPLWHOREPLY(c, target *client, mask string) {
+ isRoommate := false
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ _, presentTarget := ch.userModes[target]
+ if presentClient && presentTarget {
+ isRoommate = true
+ break
+ }
+ }
+ if !isRoommate && 0 != target.mode&ircUserModeInvisible {
+ return
+ }
+
+ if !ircFnmatch(mask, target.hostname) &&
+ !ircFnmatch(mask, target.nickname) &&
+ !ircFnmatch(mask, target.realname) &&
+ !ircFnmatch(mask, serverName) {
+ return
+ }
+
+ // Try to find a channel they're on that's visible to us.
+ var userCh *channel
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ _, presentTarget := ch.userModes[target]
+ if presentTarget && (presentClient ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
+ userCh = ch
+ break
+ }
+ }
+ ircSendRPLWHOREPLY(c, userCh, target)
+}
+
+func ircHandleWHO(msg *message, c *client) {
+ onlyOps := len(msg.params) > 1 && msg.params[1] == "o"
+
+ shownMask, usedMask := "*", "*"
+ if len(msg.params) > 0 {
+ shownMask = msg.params[0]
+ if shownMask != "0" {
+ usedMask = shownMask
+ }
+ }
+
+ if ch := channels[ircToCanon(usedMask)]; ch != nil {
+ _, present := ch.userModes[c]
+ if present || 0 == ch.modes&ircChanModeSecret {
+ for client := range ch.userModes {
+ if (present || 0 == client.mode&ircUserModeInvisible) &&
+ (!onlyOps || 0 != client.mode&ircUserModeOperator) {
+ ircSendRPLWHOREPLY(c, ch, client)
+ }
+ }
+ }
+ } else {
+ for _, client := range users {
+ if !onlyOps || 0 != client.mode&ircUserModeOperator {
+ ircMatchSendRPLWHOREPLY(c, client, usedMask)
+ }
+ }
+ }
+ c.sendReply(RPL_ENDOFWHO, shownMask)
+}
+
+func ircSendWHOISReply(c, target *client) {
+ nick := target.nickname
+ c.sendReply(RPL_WHOISUSER, nick,
+ target.username, target.hostname, target.realname)
+ c.sendReply(RPL_WHOISSERVER, nick, serverName, config["server_info"])
+ if 0 != target.mode&ircUserModeOperator {
+ c.sendReply(RPL_WHOISOPERATOR, nick)
+ }
+ c.sendReply(RPL_WHOISIDLE, nick,
+ time.Now().Sub(target.lastActive)/time.Second)
+ if target.awayMessage != "" {
+ c.sendReply(RPL_AWAY, nick, target.awayMessage)
+ }
+
+ var chans []string
+ for _, ch := range channels {
+ _, presentClient := ch.userModes[c]
+ modes, presentTarget := ch.userModes[target]
+ if presentTarget && (presentClient ||
+ 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret)) {
+ // TODO: Deduplicate, ircAppendPrefixes just also cuts prefixes.
+ var all []byte
+ if 0 != modes&ircChanModeOperator {
+ all = append(all, '@')
+ }
+ if 0 != modes&ircChanModeVoice {
+ all = append(all, '+')
+ }
+ chans = append(chans, string(all)+ch.name)
+ }
+ }
+ c.sendReplyVector(RPL_WHOISCHANNELS, chans, nick, "")
+ c.sendReply(RPL_ENDOFWHOIS, nick)
+}
+
+func ircHandleWHOIS(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ masksStr := msg.params[0]
+ if len(msg.params) > 1 {
+ masksStr = msg.params[1]
+ }
+
+ for _, mask := range splitString(masksStr, ",", true /* ignoreEmpty */) {
+ if strings.IndexAny(mask, "*?") < 0 {
+ if target := users[ircToCanon(mask)]; target == nil {
+ c.sendReply(ERR_NOSUCHNICK, mask)
+ } else {
+ ircSendWHOISReply(c, target)
+ }
+ } else {
+ found := false
+ for _, target := range users {
+ if ircFnmatch(mask, target.nickname) {
+ ircSendWHOISReply(c, target)
+ found = true
+ }
+ }
+ if !found {
+ c.sendReply(ERR_NOSUCHNICK, mask)
+ }
+ }
+ }
+}
+
+func ircHandleWHOWAS(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if len(msg.params) > 2 && !ircIsThisMe(msg.params[2]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[2])
+ return
+ }
+ // The "count" parameter is ignored, we only store one entry for a nick.
+
+ for _, nick := range splitString(msg.params[0], ",", true) {
+ if info := whowas[ircToCanon(nick)]; info == nil {
+ c.sendReply(ERR_WASNOSUCHNICK, nick)
+ } else {
+ c.sendReply(RPL_WHOWASUSER, nick,
+ info.username, info.hostname, info.realname)
+ c.sendReply(RPL_WHOISSERVER, nick,
+ serverName, config["server_info"])
+ }
+ c.sendReply(RPL_ENDOFWHOWAS, nick)
+ }
+}
+
+func ircSendRPLTOPIC(c *client, ch *channel) {
+ if ch.topic == "" {
+ c.sendReply(RPL_NOTOPIC, ch.name)
+ } else {
+ c.sendReply(RPL_TOPIC, ch.name, ch.topic)
+ c.sendReply(RPL_TOPICWHOTIME,
+ ch.name, ch.topicWho, ch.topicTime.Unix())
+ }
+}
+
+func ircHandleTOPIC(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ channelName := msg.params[0]
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if len(msg.params) < 2 {
+ ircSendRPLTOPIC(c, ch)
+ return
+ }
+
+ modes, present := ch.userModes[c]
+ if !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+
+ if 0 != ch.modes&ircChanModeProtectedTopic &&
+ 0 == modes&ircChanModeOperator {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ ch.topic = msg.params[1]
+ ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname)
+ ch.topicTime = time.Now()
+
+ message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s",
+ c.nickname, c.username, c.hostname, channelName, ch.topic)
+ ircChannelMulticast(ch, message, nil)
+}
+
+func ircTryPart(c *client, channelName string, reason string) {
+ if reason == "" {
+ reason = c.nickname
+ }
+
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if _, present := ch.userModes[c]; !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+
+ message := fmt.Sprintf(":%s@%s@%s PART %s :%s",
+ c.nickname, c.username, c.hostname, channelName, reason)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ delete(ch.userModes, c)
+ ircChannelDestroyIfEmpty(ch)
+}
+
+func ircPartAllChannels(c *client) {
+ for _, ch := range channels {
+ if _, present := ch.userModes[c]; present {
+ ircTryPart(c, ch.name, "")
+ }
+ }
+}
+
+func ircHandlePART(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ reason := ""
+ if len(msg.params) > 1 {
+ reason = msg.params[1]
+ }
+
+ for _, channelName := range splitString(msg.params[0], ",", true) {
+ ircTryPart(c, channelName, reason)
+ }
+}
+
+func ircTryKick(c *client, channelName, nick, reason string) {
+ ch := channels[ircToCanon(channelName)]
+ if ch == nil {
+ c.sendReply(ERR_NOSUCHCHANNEL, channelName)
+ return
+ }
+
+ if modes, present := ch.userModes[c]; !present {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ } else if 0 == modes&ircChanModeOperator {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ client := users[ircToCanon(nick)]
+ if _, present := ch.userModes[client]; client == nil || !present {
+ c.sendReply(ERR_USERNOTINCHANNEL, nick, channelName)
+ return
+ }
+
+ message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s",
+ c.nickname, c.username, c.hostname, channelName, nick, reason)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ delete(ch.userModes, client)
+ ircChannelDestroyIfEmpty(ch)
+}
+
+func ircHandleKICK(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ reason := c.nickname
+ if len(msg.params) > 2 {
+ reason = msg.params[2]
+ }
+
+ targetChannels := splitString(msg.params[0], ",", true)
+ targetUsers := splitString(msg.params[1], ",", true)
+
+ if len(channels) == 1 {
+ for i := 0; i < len(targetUsers); i++ {
+ ircTryKick(c, targetChannels[0], targetUsers[i], reason)
+ }
+ } else {
+ for i := 0; i < len(channels) && i < len(targetUsers); i++ {
+ ircTryKick(c, targetChannels[i], targetUsers[i], reason)
+ }
+ }
+}
+
+func ircSendInviteNotifications(ch *channel, c, target *client) {
+ for client := range ch.userModes {
+ if client != target && 0 != client.capsEnabled&ircCapInviteNotify {
+ client.sendf(":%s!%s@%s INVITE %s %s",
+ c.nickname, c.username, c.hostname, target.nickname, ch.name)
+ }
+ }
+}
+
+func ircHandleINVITE(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ target, channelName := msg.params[0], msg.params[1]
+ client := users[ircToCanon(target)]
+ if client == nil {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ return
+ }
+
+ if ch := channels[ircToCanon(channelName)]; ch != nil {
+ invitingModes, invitingPresent := ch.userModes[c]
+ if !invitingPresent {
+ c.sendReply(ERR_NOTONCHANNEL, channelName)
+ return
+ }
+ if _, present := ch.userModes[client]; present {
+ c.sendReply(ERR_USERONCHANNEL, target, channelName)
+ return
+ }
+
+ if 0 != invitingModes&ircChanModeOperator {
+ client.invites[ircToCanon(channelName)] = true
+ } else if 0 != ch.modes&ircChanModeInviteOnly {
+ c.sendReply(ERR_CHANOPRIVSNEEDED, channelName)
+ return
+ }
+
+ // It's not specified when and how we should send out invite-notify.
+ if 0 != ch.modes&ircChanModeInviteOnly {
+ ircSendInviteNotifications(ch, c, client)
+ }
+ }
+
+ client.sendf(":%s!%s@%s INVITE %s %s",
+ c.nickname, c.username, c.hostname, client.nickname, channelName)
+ if client.awayMessage != "" {
+ c.sendReply(RPL_AWAY, client.nickname, client.awayMessage)
+ }
+ c.sendReply(RPL_INVITING, client.nickname, channelName)
+}
+
+func ircTryJoin(c *client, channelName, key string) {
+ ch := channels[ircToCanon(channelName)]
+ var userMode uint
+ if ch == nil {
+ if !ircIsValidChannelName(channelName) {
+ c.sendReply(ERR_BADCHANMASK, channelName)
+ return
+ }
+ ch = ircChannelCreate(channelName)
+ userMode = ircChanModeOperator
+ } else if _, present := ch.userModes[c]; present {
+ return
+ }
+
+ _, invitedByChanop := c.invites[ircToCanon(channelName)]
+ if 0 != ch.modes&ircChanModeInviteOnly && c.inMaskList(ch.inviteList) &&
+ !invitedByChanop {
+ c.sendReply(ERR_INVITEONLYCHAN, channelName)
+ return
+ }
+ if ch.key != "" && (key == "" || key != ch.key) {
+ c.sendReply(ERR_BADCHANNELKEY, channelName)
+ return
+ }
+ if ch.userLimit != -1 && len(ch.userModes) >= ch.userLimit {
+ c.sendReply(ERR_CHANNELISFULL, channelName)
+ return
+ }
+ if c.inMaskList(ch.banList) && !c.inMaskList(ch.exceptionList) &&
+ !invitedByChanop {
+ c.sendReply(ERR_BANNEDFROMCHAN, channelName)
+ return
+ }
+
+ // Destroy any invitation as there's no other way to get rid of it.
+ delete(c.invites, ircToCanon(channelName))
+
+ ch.userModes[c] = userMode
+
+ message := fmt.Sprintf(":%s!%s@%s JOIN %s",
+ c.nickname, c.username, c.hostname, channelName)
+ if 0 == ch.modes&ircChanModeQuiet {
+ ircChannelMulticast(ch, message, nil)
+ } else {
+ c.send(message)
+ }
+
+ ircSendRPLTOPIC(c, ch)
+ ircSendRPLNAMREPLY(c, ch, nil)
+ c.sendReply(RPL_ENDOFNAMES, ch.name)
+}
+
+func ircHandleJOIN(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ if msg.params[0] == "0" {
+ ircPartAllChannels(c)
+ return
+ }
+
+ targetChannels := splitString(msg.params[0], ",", true)
+
+ var keys []string
+ if len(msg.params) > 1 {
+ keys = splitString(msg.params[1], ",", true)
+ }
+
+ for i, name := range targetChannels {
+ key := ""
+ if i < len(keys) {
+ key = keys[i]
+ }
+ ircTryJoin(c, name, key)
+ }
+}
+
+func ircHandleSUMMON(msg *message, c *client) {
+ c.sendReply(ERR_SUMMONDISABLED)
+}
+
+func ircHandleUSERS(msg *message, c *client) {
+ c.sendReply(ERR_USERSDISABLED)
+}
+
+func ircHandleAWAY(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.awayMessage = ""
+ c.sendReply(RPL_UNAWAY)
+ } else {
+ c.awayMessage = msg.params[0]
+ c.sendReply(RPL_NOWAWAY)
+ }
+}
+
+func ircHandleISON(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ var on []string
+ for _, nick := range msg.params {
+ if client := users[ircToCanon(nick)]; client != nil {
+ on = append(on, nick)
+ }
+ }
+ c.sendReply(RPL_ISON, strings.Join(on, " "))
+}
+
+func ircHandleADMIN(msg *message, c *client) {
+ if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+ c.sendReply(ERR_NOADMININFO, serverName)
+}
+
+func ircHandleStatsLinks(c *client, msg *message) {
+ // There is only an "l" query in RFC 2812 but we cannot link,
+ // so instead we provide the "L" query giving information for all users.
+ filter := ""
+ if len(msg.params) > 1 {
+ filter = msg.params[1]
+ }
+
+ for _, client := range users {
+ if filter != "" && !ircEqual(client.nickname, filter) {
+ continue
+ }
+ c.sendReply(RPL_STATSLINKINFO,
+ client.address, // linkname
+ len(client.sendQ), // sendq
+ client.nSentMessages, client.sentBytes/1024,
+ client.nReceivedMessages, client.receivedBytes/1024,
+ time.Now().Sub(client.opened)/time.Second)
+ }
+}
+
+func ircHandleStatsCommands(c *client) {
+ for name, handler := range ircHandlers {
+ if handler.nReceived > 0 {
+ c.sendReply(RPL_STATSCOMMANDS, name,
+ handler.nReceived, handler.bytesReceived, 0)
+ }
+ }
+}
+
+// We need to do it this way because of an initialization loop concerning
+// ircHandlers. Workaround proposed by rsc in go #1817.
+var ircHandleStatsCommandsIndirect func(c *client)
+
+func init() {
+ ircHandleStatsCommandsIndirect = ircHandleStatsCommands
+}
+
+func ircHandleStatsUptime(c *client) {
+ uptime := time.Now().Sub(started) / time.Second
+
+ days := uptime / 60 / 60 / 24
+ hours := (uptime % (60 * 60 * 24)) / 60 / 60
+ mins := (uptime % (60 * 60)) / 60
+ secs := uptime % 60
+
+ c.sendReply(RPL_STATSUPTIME, days, hours, mins, secs)
+}
+
+func ircHandleSTATS(msg *message, c *client) {
+ var query byte
+ if len(msg.params) > 0 && len(msg.params[0]) > 0 {
+ query = msg.params[0][0]
+ }
+
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ return
+ }
+
+ switch query {
+ case 'L':
+ ircHandleStatsLinks(c, msg)
+ case 'm':
+ ircHandleStatsCommandsIndirect(c)
+ case 'u':
+ ircHandleStatsUptime(c)
+ }
+ c.sendReply(RPL_ENDOFSTATS, query)
+}
+
+func ircHandleLINKS(msg *message, c *client) {
+ if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ mask := "*"
+ if len(msg.params) > 0 {
+ if len(msg.params) > 1 {
+ mask = msg.params[1]
+ } else {
+ mask = msg.params[0]
+ }
+ }
+
+ if ircFnmatch(mask, serverName) {
+ c.sendReply(RPL_LINKS, mask, serverName,
+ 0 /* hop count */, config["server_info"])
+ }
+ c.sendReply(RPL_ENDOFLINKS, mask)
+}
+
+func ircHandleWALLOPS(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ return
+ }
+
+ // Our interpretation: anonymize the sender,
+ // and target all users who want to receive these messages.
+ for _, client := range users {
+ if client == c || 0 != client.mode&ircUserModeRxWallops {
+ client.sendf(":%s WALLOPS :%s", serverName, msg.params[0])
+ }
+ }
+}
+
+func ircHandleKILL(msg *message, c *client) {
+ if len(msg.params) < 2 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ return
+ }
+
+ target := users[ircToCanon(msg.params[0])]
+ if target == nil {
+ c.sendReply(ERR_NOSUCHNICK, msg.params[0])
+ return
+ }
+
+ c.sendf(":%s!%s@%s KILL %s :%s",
+ c.nickname, c.username, c.hostname, target.nickname, msg.params[1])
+ target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1]))
+}
+
+func ircHandleDIE(msg *message, c *client) {
+ if 0 == c.mode&ircUserModeOperator {
+ c.sendReply(ERR_NOPRIVILEGES)
+ } else if !quitting {
+ initiateQuit()
+ }
+}
+
+// -----------------------------------------------------------------------------
+
+// TODO: Add an index for ERR_NOSUCHSERVER validation?
+// TODO: Add a minimal parameter count?
+// TODO: Add a field for oper-only commands? Use flags?
+var ircHandlers = map[string]*ircCommand{
+ "WEBIRC": {false, ircHandleWEBIRC, 0, 0},
+ "CAP": {false, ircHandleCAP, 0, 0},
+ "PASS": {false, ircHandlePASS, 0, 0},
+ "NICK": {false, ircHandleNICK, 0, 0},
+ "USER": {false, ircHandleUSER, 0, 0},
+
+ "USERHOST": {true, ircHandleUSERHOST, 0, 0},
+ "LUSERS": {true, ircHandleLUSERS, 0, 0},
+ "MOTD": {true, ircHandleMOTD, 0, 0},
+ "PING": {true, ircHandlePING, 0, 0},
+ "PONG": {false, ircHandlePONG, 0, 0},
+ "QUIT": {false, ircHandleQUIT, 0, 0},
+ "TIME": {true, ircHandleTIME, 0, 0},
+ "VERSION": {true, ircHandleVERSION, 0, 0},
+ "USERS": {true, ircHandleUSERS, 0, 0},
+ "SUMMON": {true, ircHandleSUMMON, 0, 0},
+ "AWAY": {true, ircHandleAWAY, 0, 0},
+ "ADMIN": {true, ircHandleADMIN, 0, 0},
+ "STATS": {true, ircHandleSTATS, 0, 0},
+ "LINKS": {true, ircHandleLINKS, 0, 0},
+ "WALLOPS": {true, ircHandleWALLOPS, 0, 0},
+
+ "MODE": {true, ircHandleMODE, 0, 0},
+ "PRIVMSG": {true, ircHandlePRIVMSG, 0, 0},
+ "NOTICE": {true, ircHandleNOTICE, 0, 0},
+ "JOIN": {true, ircHandleJOIN, 0, 0},
+ "PART": {true, ircHandlePART, 0, 0},
+ "KICK": {true, ircHandleKICK, 0, 0},
+ "INVITE": {true, ircHandleINVITE, 0, 0},
+ "TOPIC": {true, ircHandleTOPIC, 0, 0},
+ "LIST": {true, ircHandleLIST, 0, 0},
+ "NAMES": {true, ircHandleNAMES, 0, 0},
+ "WHO": {true, ircHandleWHO, 0, 0},
+ "WHOIS": {true, ircHandleWHOIS, 0, 0},
+ "WHOWAS": {true, ircHandleWHOWAS, 0, 0},
+ "ISON": {true, ircHandleISON, 0, 0},
+
+ "KILL": {true, ircHandleKILL, 0, 0},
+ "DIE": {true, ircHandleDIE, 0, 0},
+}
+
+func ircProcessMessage(c *client, msg *message, raw string) {
+ if c.closing {
+ return
+ }
+
+ c.nReceivedMessages++
+ c.receivedBytes += len(raw) + 2
+
+ if !c.antiflood.check() {
+ c.closeLink("Excess flood")
+ return
+ }
+
+ if cmd, ok := ircHandlers[ircToCanon(msg.command)]; !ok {
+ c.sendReply(ERR_UNKNOWNCOMMAND, msg.command)
+ } else {
+ cmd.nReceived++
+ cmd.bytesReceived += len(raw) + 2
+
+ if cmd.requiresRegistration && !c.registered {
+ c.sendReply(ERR_NOTREGISTERED)
+ } else {
+ cmd.handler(msg, c)
+ }
+ }
+}
+
+// --- Network I/O -------------------------------------------------------------
+
+// Handle the results from initializing the client's connection.
+func (c *client) onPrepared(host string, isTLS bool) {
+ c.printDebug("client resolved to %s, TLS %t", host, isTLS)
+ if !isTLS {
+ c.conn = c.transport.(connCloseWriter)
+ } else if tlsConf != nil {
+ c.tls = tls.Server(c.transport, tlsConf)
+ c.conn = c.tls
+ } else {
+ c.printDebug("could not initialize TLS: disabled")
+ c.kill("TLS support disabled")
+ return
+ }
+
+ c.hostname = host
+ c.address = net.JoinHostPort(host, c.port)
+
+ // If we tried to send any data before now, we would need to flushSendQ.
+ go read(c)
+ c.reading = true
+ c.setPingTimer()
+}
+
+// Handle the results from trying to read from the client connection.
+func (c *client) onRead(data []byte, readErr error) {
+ if !c.reading {
+ // Abusing the flag to emulate CloseRead and skip over data, see below.
+ return
+ }
+
+ c.recvQ = append(c.recvQ, data...)
+ for {
+ // XXX: This accepts even simple LF newlines, even though they're not
+ // really allowed by the protocol.
+ advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */)
+ if advance == 0 {
+ break
+ }
+
+ // XXX: And since it accepts LF, we miscalculate receivedBytes within.
+ c.recvQ = c.recvQ[advance:]
+ line := string(token)
+ c.printDebug("-> %s", line)
+
+ if msg := ircParseMessage(line); msg == nil {
+ c.printDebug("error: invalid line")
+ } else {
+ ircProcessMessage(c, msg, line)
+ }
+ }
+
+ if readErr != nil {
+ c.reading = false
+
+ if readErr != io.EOF {
+ c.printDebug("%s", readErr)
+ c.kill(readErr.Error())
+ } else if c.closing {
+ // Disregarding whether a clean shutdown has happened or not.
+ c.printDebug("client finished shutdown")
+ c.kill("")
+ } else {
+ c.printDebug("client EOF")
+ c.closeLink("")
+ }
+ } else if len(c.recvQ) > 8192 {
+ c.closeLink("recvQ overrun")
+
+ // tls.Conn doesn't have the CloseRead method (and it needs to be able
+ // to read from the TCP connection even for writes, so there isn't much
+ // sense in expecting the implementation to do anything useful),
+ // otherwise we'd use it to block incoming packet data.
+ c.reading = false
+ }
+}
+
+// Spawn a goroutine to flush the sendQ if possible and necessary.
+func (c *client) flushSendQ() {
+ if !c.writing && c.conn != nil {
+ go write(c, c.sendQ)
+ c.writing = true
+ }
+}
+
+// Handle the results from trying to write to the client connection.
+func (c *client) onWrite(written int, writeErr error) {
+ c.sendQ = c.sendQ[written:]
+ c.writing = false
+
+ if writeErr != nil {
+ c.printDebug("%s", writeErr)
+ c.kill(writeErr.Error())
+ } else if len(c.sendQ) > 0 {
+ c.flushSendQ()
+ } else if c.closing {
+ if c.reading {
+ c.conn.CloseWrite()
+ } else {
+ c.kill("")
+ }
+ }
+}
+
+// --- Worker goroutines -------------------------------------------------------
+
+func accept(ln net.Listener) {
+ for {
+ // Error handling here may be tricky, see go #6163, #24808.
+ if conn, err := ln.Accept(); err != nil {
+ // See go #4373, they're being dicks. Another solution would be to
+ // pass a done channel to this function and close it before closing
+ // all the listeners, returning from here if it's readable.
+ if strings.Contains(err.Error(),
+ "use of closed network connection") {
+ return
+ }
+ // XXX: net.Error.Temporary() has been deprecated in 1.18.
+ if op, ok := err.(net.Error); !ok || !op.Temporary() {
+ exitFatal("%s", err)
+ } else {
+ printError("%s", err)
+ }
+ } else {
+ // TCP_NODELAY is set by default on TCPConns.
+ conns <- conn
+ }
+ }
+}
+
+func prepare(client *client) {
+ conn, host := client.transport, client.hostname
+
+ // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
+ // bothering with pointless contexts.
+ ch := make(chan string, 1)
+ go func() {
+ defer close(ch)
+ if names, err := net.LookupAddr(host); err != nil {
+ printError("%s", err)
+ } else {
+ ch <- names[0]
+ }
+ }()
+
+ // While we can't cancel it, we still want to set a timeout on it.
+ select {
+ case <-time.After(5 * time.Second):
+ case resolved, ok := <-ch:
+ if ok {
+ host = resolved
+ }
+ }
+
+ // Note that in this demo application the autodetection prevents non-TLS
+ // clients from receiving any messages until they send something.
+ isTLS := false
+ if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
+ // This is just for the TLS detection and doesn't need to be fatal.
+ printError("%s", err)
+ } else {
+ isTLS = detectTLS(sysconn)
+ }
+
+ prepared <- preparedEvent{client, host, isTLS}
+}
+
+func read(client *client) {
+ // A new buffer is allocated each time we receive some bytes, because of
+ // thread-safety. Therefore the buffer shouldn't be too large, or we'd
+ // need to copy it each time into a precisely sized new buffer.
+ var err error
+ for err == nil {
+ var (
+ buf [512]byte
+ n int
+ )
+ n, err = client.conn.Read(buf[:])
+ reads <- readEvent{client, buf[:n], err}
+ }
+}
+
+// Flush sendQ, which is passed by parameter so that there are no data races.
+func write(client *client, data []byte) {
+ // We just write as much as we can, the main goroutine does the looping.
+ n, err := client.conn.Write(data)
+ writes <- writeEvent{client, n, err}
+}
+
+// --- Event loop --------------------------------------------------------------
+
+func processOneEvent() {
+ select {
+ case <-sigs:
+ if quitting {
+ forceQuit("requested by user")
+ } else {
+ initiateQuit()
+ }
+
+ case <-quitTimer:
+ forceQuit("timeout")
+
+ case callback := <-timers:
+ callback()
+
+ case conn := <-conns:
+ if maxConnections > 0 && len(clients) >= maxConnections {
+ printDebug("connection limit reached, refusing connection")
+ conn.Close()
+ break
+ }
+
+ address := conn.RemoteAddr().String()
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ // In effect, we require TCP/UDP, as they have port numbers.
+ exitFatal("%s", err)
+ }
+
+ c := &client{
+ transport: conn,
+ address: address,
+ hostname: host,
+ port: port,
+ capVersion: 301,
+ opened: time.Now(),
+ lastActive: time.Now(),
+ // TODO: Make this configurable and more fine-grained.
+ antiflood: newFloodDetector(10*time.Second, 20),
+ }
+
+ clients[c] = true
+ c.printDebug("new client")
+ go prepare(c)
+
+ // The TLS autodetection in prepare needs to have a timeout.
+ c.setKillTimer()
+
+ case ev := <-prepared:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onPrepared(ev.host, ev.isTLS)
+ }
+
+ case ev := <-reads:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onRead(ev.data, ev.err)
+ }
+
+ case ev := <-writes:
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onWrite(ev.written, ev.err)
+ }
+ }
+}
+
+// --- Application setup -------------------------------------------------------
+
+func ircInitializeTLS() error {
+ configCert, configKey := config["tls_cert"], config["tls_key"]
+
+ // Only try to enable SSL support if the user configures it; it is not
+ // a failure if no one has requested it.
+ if configCert == "" && configKey == "" {
+ return nil
+ } else if configCert == "" {
+ return errors.New("no TLS certificate set")
+ } else if configKey == "" {
+ return errors.New("no TLS private key set")
+ }
+
+ pathCert := resolveFilename(configCert, resolveRelativeConfigFilename)
+ if pathCert == "" {
+ return fmt.Errorf("cannot find file: %s", configCert)
+ }
+
+ pathKey := resolveFilename(configKey, resolveRelativeConfigFilename)
+ if pathKey == "" {
+ return fmt.Errorf("cannot find file: %s", configKey)
+ }
+
+ cert, err := tls.LoadX509KeyPair(pathCert, pathKey)
+ if err != nil {
+ return err
+ }
+
+ tlsConf = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ClientAuth: tls.RequestClientCert,
+ SessionTicketsDisabled: true,
+ }
+ return nil
+}
+
+func ircInitializeCatalog() error {
+ configCatalog := config["catalog"]
+ if configCatalog == "" {
+ return nil
+ }
+
+ path := resolveFilename(configCatalog, resolveRelativeConfigFilename)
+ if path == "" {
+ return fmt.Errorf("cannot find file: %s", configCatalog)
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed reading the MOTD file: %s", err)
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ catalog = make(map[int]string)
+ for lineNo := 1; scanner.Scan(); lineNo++ {
+ line := strings.TrimLeft(scanner.Text(), " \t")
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+
+ delim := strings.IndexAny(line, " \t")
+ if delim < 0 {
+ return fmt.Errorf("%s:%d: malformed line", path, lineNo)
+ }
+
+ id, err := strconv.ParseUint(line[:delim], 10, 16)
+ if err != nil {
+ return fmt.Errorf("%s:%d: %s", path, lineNo, err)
+ }
+
+ catalog[int(id)] = line[delim+1:]
+ }
+ return scanner.Err()
+}
+
+func ircInitializeMOTD() error {
+ configMOTD := config["motd"]
+ if configMOTD == "" {
+ return nil
+ }
+
+ path := resolveFilename(configMOTD, resolveRelativeConfigFilename)
+ if path == "" {
+ return fmt.Errorf("cannot find file: %s", configMOTD)
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("failed reading the MOTD file: %s", err)
+ }
+ defer f.Close()
+
+ scanner := bufio.NewScanner(f)
+ motd = nil
+ for scanner.Scan() {
+ motd = append(motd, scanner.Text())
+ }
+ return scanner.Err()
+}
+
+type configProcessor struct {
+ err error // any error that has occurred so far
+}
+
+func (cp *configProcessor) read(name string, process func(string) string) {
+ if cp.err != nil {
+ return
+ }
+ if err := process(config[name]); err != "" {
+ cp.err = fmt.Errorf("invalid configuration value for `%s': %s",
+ name, err)
+ }
+}
+
+// This function handles values that require validation before their first use,
+// or some kind of a transformation (such as conversion to an integer) needs
+// to be done before they can be used directly.
+func ircParseConfig() error {
+ cp := &configProcessor{}
+ cp.read("ping_interval", func(value string) string {
+ if u, err := strconv.ParseUint(
+ config["ping_interval"], 10, 32); err != nil {
+ return err.Error()
+ } else if u < 1 {
+ return "the value is out of range"
+ } else {
+ pingInterval = time.Second * time.Duration(u)
+ }
+ return ""
+ })
+ cp.read("max_connections", func(value string) string {
+ if i, err := strconv.ParseInt(
+ value, 10, 32); err != nil {
+ return err.Error()
+ } else if i < 0 {
+ return "the value is out of range"
+ } else {
+ maxConnections = int(i)
+ }
+ return ""
+ })
+ cp.read("operators", func(value string) string {
+ operators = make(map[string]bool)
+ for _, fp := range splitString(value, ",", true) {
+ if !ircIsValidFingerprint(fp) {
+ return "invalid fingerprint value"
+ }
+ operators[strings.ToLower(fp)] = true
+ }
+ return ""
+ })
+ return cp.err
+}
+
+func ircInitializeServerName() error {
+ if value := config["server_name"]; value != "" {
+ if err := ircValidateHostname(value); err != nil {
+ return err
+ }
+ serverName = value
+ return nil
+ }
+
+ if hostname, err := os.Hostname(); err != nil {
+ return err
+ } else if err := ircValidateHostname(hostname); err != nil {
+ return err
+ } else {
+ serverName = hostname
+ }
+ return nil
+}
+
+func ircSetupListenFDs() error {
+ for _, address := range splitString(config["bind"], ",", true) {
+ ln, err := net.Listen("tcp", address)
+ if err != nil {
+ return err
+ }
+ listeners = append(listeners, ln)
+ printStatus("listening on %s", address)
+ }
+ if len(listeners) == 0 {
+ return errors.New("network setup failed: no ports to listen on")
+ }
+ for _, ln := range listeners {
+ go accept(ln)
+ }
+ return nil
+}
+
+// --- Main --------------------------------------------------------------------
+
+func main() {
+ flag.BoolVar(&debugMode, "debug", false, "run in verbose debug mode")
+ version := flag.Bool("version", false, "show version and exit")
+ writeDefaultCfg := flag.Bool("writedefaultcfg", false,
+ "write a default configuration file and exit")
+ systemd := flag.Bool("systemd", false, "log in systemd format")
+
+ flag.Parse()
+
+ if *version {
+ fmt.Printf("%s %s\n", projectName, projectVersion)
+ return
+ }
+ if *writeDefaultCfg {
+ callSimpleConfigWriteDefault("", configTable)
+ return
+ }
+ if *systemd {
+ logMessage = logMessageSystemd
+ }
+ if flag.NArg() > 0 {
+ flag.Usage()
+ os.Exit(2)
+ }
+
+ // Note that this has become unnecessary since Go 1.19.
+ var limit syscall.Rlimit
+ if err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &limit); err == nil &&
+ limit.Cur != limit.Max {
+ limit.Cur = limit.Max
+ syscall.Setrlimit(syscall.RLIMIT_NOFILE, &limit)
+ }
+
+ config = make(simpleConfig)
+ config.loadDefaults(configTable)
+ if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) {
+ printError("error loading configuration: %s", err)
+ os.Exit(1)
+ }
+
+ started = time.Now()
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for _, fn := range []func() error{
+ ircInitializeTLS,
+ ircInitializeServerName,
+ ircInitializeMOTD,
+ ircInitializeCatalog,
+ ircParseConfig,
+ ircSetupListenFDs,
+ } {
+ if err := fn(); err != nil {
+ exitFatal("%s", err)
+ }
+ }
+
+ for !quitting || len(clients) > 0 {
+ processOneEvent()
+ }
+}