diff options
Diffstat (limited to 'xS/main.go')
-rw-r--r-- | xS/main.go | 3529 |
1 files changed, 0 insertions, 3529 deletions
diff --git a/xS/main.go b/xS/main.go deleted file mode 100644 index 21851f1..0000000 --- a/xS/main.go +++ /dev/null @@ -1,3529 +0,0 @@ -// -// 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 ------------------------------------------------------------ - -//go:generate sh -c "LC_ALL=C awk -f xS-gen-replies.awk > xS-replies.go < xS-replies" - -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() - } -} |