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