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