// // Copyright (c) 2014 - 2018, Přemysl Eric Janouch
// // 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. // // hid is a straight-forward port of kike 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 = "hid" // 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)"}, {"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 "./hid-gen-replies.sh > hid-replies.go < hid-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 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 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{ "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}, "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 } 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) } 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() } }