aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.adoc22
-rwxr-xr-xhid/hid-gen-replies.sh16
-rw-r--r--hid/hid-replies87
-rw-r--r--hid/main.go3530
-rw-r--r--hid/main_test.go168
5 files changed, 3 insertions, 3820 deletions
diff --git a/README.adoc b/README.adoc
index b6f728b..985fef8 100644
--- a/README.adoc
+++ b/README.adoc
@@ -34,7 +34,6 @@ figured out it would make sense:
- hbfe - bitmap font editor
- he - text editor
- hfm - file manager
- - hid - IRC daemon
- hm - mail client
- hnc - netcat-alike
- ho - all-powerful organizer
@@ -57,26 +56,11 @@ permeating the entire list.
Some information is omitted from these descriptions and lies either in my head
or in my other notes.
-hid -- IRC daemon
-~~~~~~~~~~~~~~~~~
-This project is unimportant by itself, its sole purpose is to gain experience
-with Go on something that I have already done and understand well. Nothing
-beyond achieving feature parity is in the initial scope.
-
-One possibility of complicating would be adding simple WebSocket listeners but
-that's already been done for me https://github.com/kiwiirc/webircgateway and
-it's even in Go, I just need to set up kiwiirc.
-
-Later, when we have a pleasant IRC client, implement either the P10 or the TS6
-server-linking protocol and make atheme work with a generic module.
-Alternatively add support for plugins. The goal is to allow creating integrated
-bridges to various public forums.
-
hnc -- netcat-alike
~~~~~~~~~~~~~~~~~~~
-The result of testing hid with telnet, OpenSSL s_client, OpenBSD nc, GNU nc and
-Ncat is that neither of them can properly shutdown the connection. We need
-a good implementation with TLS support.
+The result of testing xK/xS with telnet, OpenSSL s_client, OpenBSD nc, GNU nc,
+and Ncat is that neither of them can properly shutdown the connection.
+We need a good implementation with TLS support.
hpcu -- PRIMARY-CLIPBOARD unifier
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/hid/hid-gen-replies.sh b/hid/hid-gen-replies.sh
deleted file mode 100755
index c32b000..0000000
--- a/hid/hid-gen-replies.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/bin/sh
-LC_ALL=C exec awk '
- /^[0-9]+ *(ERR|RPL)_[A-Z]+ *".*"$/ {
- match($0, /".*"/);
- ids[$1] = $2;
- texts[$2] = substr($0, RSTART, RLENGTH);
- }
- END {
- print "package " ENVIRON["GOPACKAGE"] "\n\nconst ("
- for (i in ids)
- printf("\t%s = %s\n", ids[i], i)
- print ")\n\nvar defaultReplies = map[int]string{"
- for (i in ids)
- print "\t" ids[i] ": " texts[ids[i]] ","
- print "}"
- }'
diff --git a/hid/hid-replies b/hid/hid-replies
deleted file mode 100644
index c539520..0000000
--- a/hid/hid-replies
+++ /dev/null
@@ -1,87 +0,0 @@
-1 RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s"
-2 RPL_YOURHOST ":Your host is %s, running version %s"
-3 RPL_CREATED ":This server was created %s"
-4 RPL_MYINFO "%s %s %s %s"
-5 RPL_ISUPPORT "%s :are supported by this server"
-211 RPL_STATSLINKINFO "%s %d %d %d %d %d %d"
-212 RPL_STATSCOMMANDS "%s %d %d %d"
-219 RPL_ENDOFSTATS "%c :End of STATS report"
-221 RPL_UMODEIS "+%s"
-242 RPL_STATSUPTIME ":Server Up %d days %d:%02d:%02d"
-251 RPL_LUSERCLIENT ":There are %d users and %d services on %d servers"
-252 RPL_LUSEROP "%d :operator(s) online"
-253 RPL_LUSERUNKNOWN "%d :unknown connection(s)"
-254 RPL_LUSERCHANNELS "%d :channels formed"
-255 RPL_LUSERME ":I have %d clients and %d servers"
-301 RPL_AWAY "%s :%s"
-302 RPL_USERHOST ":%s"
-303 RPL_ISON ":%s"
-305 RPL_UNAWAY ":You are no longer marked as being away"
-306 RPL_NOWAWAY ":You have been marked as being away"
-311 RPL_WHOISUSER "%s %s %s * :%s"
-312 RPL_WHOISSERVER "%s %s :%s"
-313 RPL_WHOISOPERATOR "%s :is an IRC operator"
-314 RPL_WHOWASUSER "%s %s %s * :%s"
-315 RPL_ENDOFWHO "%s :End of WHO list"
-317 RPL_WHOISIDLE "%s %d :seconds idle"
-318 RPL_ENDOFWHOIS "%s :End of WHOIS list"
-319 RPL_WHOISCHANNELS "%s :%s"
-322 RPL_LIST "%s %d :%s"
-323 RPL_LISTEND ":End of LIST"
-324 RPL_CHANNELMODEIS "%s +%s"
-329 RPL_CREATIONTIME "%s %d"
-331 RPL_NOTOPIC "%s :No topic is set"
-332 RPL_TOPIC "%s :%s"
-333 RPL_TOPICWHOTIME "%s %s %d"
-341 RPL_INVITING "%s %s"
-346 RPL_INVITELIST "%s %s"
-347 RPL_ENDOFINVITELIST "%s :End of channel invite list"
-348 RPL_EXCEPTLIST "%s %s"
-349 RPL_ENDOFEXCEPTLIST "%s :End of channel exception list"
-351 RPL_VERSION "%s.%d %s :%s"
-352 RPL_WHOREPLY "%s %s %s %s %s %s :%d %s"
-353 RPL_NAMREPLY "%c %s :%s"
-364 RPL_LINKS "%s %s :%d %s"
-365 RPL_ENDOFLINKS "%s :End of LINKS list"
-366 RPL_ENDOFNAMES "%s :End of NAMES list"
-367 RPL_BANLIST "%s %s"
-368 RPL_ENDOFBANLIST "%s :End of channel ban list"
-369 RPL_ENDOFWHOWAS "%s :End of WHOWAS"
-372 RPL_MOTD ":- %s"
-375 RPL_MOTDSTART ":- %s Message of the day - "
-376 RPL_ENDOFMOTD ":End of MOTD command"
-391 RPL_TIME "%s :%s"
-401 ERR_NOSUCHNICK "%s :No such nick/channel"
-402 ERR_NOSUCHSERVER "%s :No such server"
-403 ERR_NOSUCHCHANNEL "%s :No such channel"
-404 ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel"
-406 ERR_WASNOSUCHNICK "%s :There was no such nickname"
-409 ERR_NOORIGIN ":No origin specified"
-410 ERR_INVALIDCAPCMD "%s :%s"
-411 ERR_NORECIPIENT ":No recipient given (%s)"
-412 ERR_NOTEXTTOSEND ":No text to send"
-421 ERR_UNKNOWNCOMMAND "%s: Unknown command"
-422 ERR_NOMOTD ":MOTD File is missing"
-423 ERR_NOADMININFO "%s :No administrative info available"
-431 ERR_NONICKNAMEGIVEN ":No nickname given"
-432 ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname"
-433 ERR_NICKNAMEINUSE "%s :Nickname is already in use"
-441 ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel"
-442 ERR_NOTONCHANNEL "%s :You're not on that channel"
-443 ERR_USERONCHANNEL "%s %s :is already on channel"
-445 ERR_SUMMONDISABLED ":SUMMON has been disabled"
-446 ERR_USERSDISABLED ":USERS has been disabled"
-451 ERR_NOTREGISTERED ":You have not registered"
-461 ERR_NEEDMOREPARAMS "%s :Not enough parameters"
-462 ERR_ALREADYREGISTERED ":Unauthorized command (already registered)"
-467 ERR_KEYSET "%s :Channel key already set"
-471 ERR_CHANNELISFULL "%s :Cannot join channel (+l)"
-472 ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s"
-473 ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)"
-474 ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)"
-475 ERR_BADCHANNELKEY "%s :Cannot join channel (+k)"
-476 ERR_BADCHANMASK "%s :Bad Channel Mask"
-481 ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator"
-482 ERR_CHANOPRIVSNEEDED "%s :You're not channel operator"
-501 ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag"
-502 ERR_USERSDONTMATCH ":Cannot change mode for other users"
diff --git a/hid/main.go b/hid/main.go
deleted file mode 100644
index 40f47bc..0000000
--- a/hid/main.go
+++ /dev/null
@@ -1,3530 +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.
-//
-
-// hid 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 = "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)"},
- {"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 "./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 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()
- }
-}
diff --git a/hid/main_test.go b/hid/main_test.go
deleted file mode 100644
index 8241b4e..0000000
--- a/hid/main_test.go
+++ /dev/null
@@ -1,168 +0,0 @@
-//
-// Copyright (c) 2015 - 2018, 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.
-//
-
-package main
-
-import (
- "crypto/tls"
- "net"
- "os"
- "reflect"
- "syscall"
- "testing"
-)
-
-func TestSplitString(t *testing.T) {
- var splitStringTests = []struct {
- s, delims string
- ignoreEmpty bool
- result []string
- }{
- {",a,,bc", ",", false, []string{"", "a", "", "bc"}},
- {",a,,bc", ",", true, []string{"a", "bc"}},
- {"a,;bc,", ",;", false, []string{"a", "", "bc", ""}},
- {"a,;bc,", ",;", true, []string{"a", "bc"}},
- {"", ",", false, []string{""}},
- {"", ",", true, nil},
- }
-
- for i, d := range splitStringTests {
- got := splitString(d.s, d.delims, d.ignoreEmpty)
- if !reflect.DeepEqual(got, d.result) {
- t.Errorf("case %d: %v should be %v\n", i, got, d.result)
- }
- }
-}
-
-func socketpair() (*os.File, *os.File, error) {
- pair, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
- if err != nil {
- return nil, nil, err
- }
-
- // See go #24331, this makes 1.11 use the internal poller
- // while there wasn't a way to achieve that before.
- if err := syscall.SetNonblock(int(pair[0]), true); err != nil {
- return nil, nil, err
- }
- if err := syscall.SetNonblock(int(pair[1]), true); err != nil {
- return nil, nil, err
- }
-
- fa := os.NewFile(uintptr(pair[0]), "a")
- if fa == nil {
- return nil, nil, os.ErrInvalid
- }
-
- fb := os.NewFile(uintptr(pair[1]), "b")
- if fb == nil {
- fa.Close()
- return nil, nil, os.ErrInvalid
- }
-
- return fa, fb, nil
-}
-
-func TestDetectTLS(t *testing.T) {
- detectTLSFromFunc := func(t *testing.T, writer func(net.Conn)) bool {
- // net.Pipe doesn't use file descriptors, we need a socketpair.
- sockA, sockB, err := socketpair()
- if err != nil {
- t.Fatal(err)
- }
- defer sockA.Close()
- defer sockB.Close()
-
- fcB, err := net.FileConn(sockB)
- if err != nil {
- t.Fatal(err)
- }
- go writer(fcB)
-
- fcA, err := net.FileConn(sockA)
- if err != nil {
- t.Fatal(err)
- }
- sc, err := fcA.(syscall.Conn).SyscallConn()
- if err != nil {
- t.Fatal(err)
- }
- return detectTLS(sc)
- }
-
- t.Run("SSL_2.0", func(t *testing.T) {
- if !detectTLSFromFunc(t, func(fc net.Conn) {
- // The obsolete, useless, unsupported SSL 2.0 record format.
- _, _ = fc.Write([]byte{0x80, 0x01, 0x01})
- }) {
- t.Error("could not detect SSL")
- }
- })
- t.Run("crypto_tls", func(t *testing.T) {
- if !detectTLSFromFunc(t, func(fc net.Conn) {
- conn := tls.Client(fc, &tls.Config{InsecureSkipVerify: true})
- _ = conn.Handshake()
- }) {
- t.Error("could not detect TLS")
- }
- })
- t.Run("text", func(t *testing.T) {
- if detectTLSFromFunc(t, func(fc net.Conn) {
- _, _ = fc.Write([]byte("ПРЕВЕД"))
- }) {
- t.Error("detected UTF-8 as TLS")
- }
- })
- t.Run("EOF", func(t *testing.T) {
- type connCloseWriter interface {
- net.Conn
- CloseWrite() error
- }
- if detectTLSFromFunc(t, func(fc net.Conn) {
- _ = fc.(connCloseWriter).CloseWrite()
- }) {
- t.Error("detected EOF as TLS")
- }
- })
-}
-
-func TestIRC(t *testing.T) {
- msg := ircParseMessage(
- `@first=a\:\s\r\n\\;2nd :srv hi there :good m8 :how are you?`)
-
- if !reflect.DeepEqual(msg.tags, map[string]string{
- "first": "a; \r\n\\",
- "2nd": "",
- }) {
- t.Error("tags parsed incorrectly")
- }
-
- if msg.nick != "srv" || msg.user != "" || msg.host != "" {
- t.Error("server name parsed incorrectly")
- }
- if msg.command != "hi" {
- t.Error("command name parsed incorrectly")
- }
- if !reflect.DeepEqual(msg.params,
- []string{"there", "good m8 :how are you?"}) {
- t.Error("params parsed incorrectly")
- }
-
- if !ircEqual("[fag]^", "{FAG}~") {
- t.Error("string case comparison not according to RFC 2812")
- }
-
- // TODO: More tests.
-}