diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-07-28 16:21:34 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-07-28 16:21:34 +0200 | 
| commit | f5def2e579f6c9f1f10fe84ac7435fad4c928116 (patch) | |
| tree | 17fcb1f957ca8753e12446703516a934c56df9fe | |
| parent | b2cd8b46c9d84b4eacf6aa2c1ff871d99b2221a7 (diff) | |
| download | haven-f5def2e579f6c9f1f10fe84ac7435fad4c928116.tar.gz haven-f5def2e579f6c9f1f10fe84ac7435fad4c928116.tar.xz haven-f5def2e579f6c9f1f10fe84ac7435fad4c928116.zip | |
hid: add a work in progress IRC daemon
The port is more than viable but it's also sort of all-or-nothing
and versioning needs have come before I've had a chance to finish it.
| -rwxr-xr-x | hid/hid-gen-replies.sh | 16 | ||||
| -rw-r--r-- | hid/hid-replies | 87 | ||||
| -rw-r--r-- | hid/main.go | 1526 | 
3 files changed, 1629 insertions, 0 deletions
| diff --git a/hid/hid-gen-replies.sh b/hid/hid-gen-replies.sh new file mode 100755 index 0000000..c32b000 --- /dev/null +++ b/hid/hid-gen-replies.sh @@ -0,0 +1,16 @@ +#!/bin/sh +LC_ALL=C exec awk ' +	/^[0-9]+ *(ERR|RPL)_[A-Z]+ *".*"$/ { +		match($0, /".*"/); +		ids[$1] = $2; +		texts[$2] = substr($0, RSTART, RLENGTH); +	} +	END { +		print "package " ENVIRON["GOPACKAGE"] "\n\nconst (" +		for (i in ids) +			printf("\t%s = %s\n", ids[i], i) +		print ")\n\nvar defaultReplies = map[int]string{" +		for (i in ids) +			print "\t" ids[i] ": " texts[ids[i]] "," +		print "}" +	}' diff --git a/hid/hid-replies b/hid/hid-replies new file mode 100644 index 0000000..24affae --- /dev/null +++ b/hid/hid-replies @@ -0,0 +1,87 @@ +1 RPL_WELCOME ":Welcome to the Internet Relay Network %s!%s@%s" +2 RPL_YOURHOST ":Your host is %s, running version %s" +3 RPL_CREATED ":This server was created %s" +4 RPL_MYINFO "%s %s %s %s" +5 RPL_ISUPPORT "%s :are supported by this server" +211 RPL_STATSLINKINFO "%s %zu %zu %zu %zu %zu %lld" +212 RPL_STATSCOMMANDS "%s %zu %zu %zu" +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 %lld" +331 RPL_NOTOPIC "%s :No topic is set" +332 RPL_TOPIC "%s :%s" +333 RPL_TOPICWHOTIME "%s %s %lld" +341 RPL_INVITING "%s %s" +346 RPL_INVITELIST "%s %s" +347 RPL_ENDOFINVITELIST "%s :End of channel invite list" +348 RPL_EXCEPTLIST "%s %s" +349 RPL_ENDOFEXCEPTLIST "%s :End of channel exception list" +351 RPL_VERSION "%s.%d %s :%s" +352 RPL_WHOREPLY "%s %s %s %s %s %s :%d %s" +353 RPL_NAMREPLY "%c %s :%s" +364 RPL_LINKS "%s %s :%d %s" +365 RPL_ENDOFLINKS "%s :End of LINKS list" +366 RPL_ENDOFNAMES "%s :End of NAMES list" +367 RPL_BANLIST "%s %s" +368 RPL_ENDOFBANLIST "%s :End of channel ban list" +369 RPL_ENDOFWHOWAS "%s :End of WHOWAS" +372 RPL_MOTD ":- %s" +375 RPL_MOTDSTART ":- %s Message of the day - " +376 RPL_ENDOFMOTD ":End of MOTD command" +391 RPL_TIME "%s :%s" +401 ERR_NOSUCHNICK "%s :No such nick/channel" +402 ERR_NOSUCHSERVER "%s :No such server" +403 ERR_NOSUCHCHANNEL "%s :No such channel" +404 ERR_CANNOTSENDTOCHAN "%s :Cannot send to channel" +406 ERR_WASNOSUCHNICK "%s :There was no such nickname" +409 ERR_NOORIGIN ":No origin specified" +410 ERR_INVALIDCAPCMD "%s :%s" +411 ERR_NORECIPIENT ":No recipient given (%s)" +412 ERR_NOTEXTTOSEND ":No text to send" +421 ERR_UNKNOWNCOMMAND "%s: Unknown command" +422 ERR_NOMOTD ":MOTD File is missing" +423 ERR_NOADMININFO "%s :No administrative info available" +431 ERR_NONICKNAMEGIVEN ":No nickname given" +432 ERR_ERRONEOUSNICKNAME "%s :Erroneous nickname" +433 ERR_NICKNAMEINUSE "%s :Nickname is already in use" +441 ERR_USERNOTINCHANNEL "%s %s :They aren't on that channel" +442 ERR_NOTONCHANNEL "%s :You're not on that channel" +443 ERR_USERONCHANNEL "%s %s :is already on channel" +445 ERR_SUMMONDISABLED ":SUMMON has been disabled" +446 ERR_USERSDISABLED ":USERS has been disabled" +451 ERR_NOTREGISTERED ":You have not registered" +461 ERR_NEEDMOREPARAMS "%s :Not enough parameters" +462 ERR_ALREADYREGISTERED ":Unauthorized command (already registered)" +467 ERR_KEYSET "%s :Channel key already set" +471 ERR_CHANNELISFULL "%s :Cannot join channel (+l)" +472 ERR_UNKNOWNMODE "%c :is unknown mode char to me for %s" +473 ERR_INVITEONLYCHAN "%s :Cannot join channel (+i)" +474 ERR_BANNEDFROMCHAN "%s :Cannot join channel (+b)" +475 ERR_BADCHANNELKEY "%s :Cannot join channel (+k)" +476 ERR_BADCHANMASK "%s :Bad Channel Mask" +481 ERR_NOPRIVILEGES ":Permission Denied- You're not an IRC operator" +482 ERR_CHANOPRIVSNEEDED "%s :You're not channel operator" +501 ERR_UMODEUNKNOWNFLAG ":Unknown MODE flag" +502 ERR_USERSDONTMATCH ":Cannot change mode for other users" diff --git a/hid/main.go b/hid/main.go new file mode 100644 index 0000000..72a6df0 --- /dev/null +++ b/hid/main.go @@ -0,0 +1,1526 @@ +// +// Copyright (c) 2014 - 2018, Přemysl Janouch <p@janouch.name> +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +// hid is a straight-forward port of kike IRCd from C. +package main + +/* + +// ANSI terminal formatting, would be better if we had isatty() available +func tf(text string, ansi string) string { +	return "\x1b[0;" + ansi + "m" + text + "\x1b[0m" +} + +func logErrorf(format string, args ...interface{}) { +	fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...) +} + +func logFatalf(format string, args ...interface{}) { +	fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...) +	os.Exit(1) +} + +func logFatal(object interface{}) { +	logFatalf("%s", object) +} + +func getHome() (home string) { +	if u, _ := user.Current(); u != nil { +		home = u.HomeDir +	} else { +		home = os.Getenv("HOME") +	} +	return +} + +// Only handling the simple case as that's what one mostly wants. +// TODO(p): Handle the generic case as well. +func expandTilde(path string) string { +	if strings.HasPrefix(path, "~/") { +		return getHome() + path[1:] +	} +	return path +} + +func getXdgHomeDir(name, def string) string { +	env := os.Getenv(name) +	if env != "" && env[0] == '/' { +		return env +	} +	return filepath.Join(getHome(), def) +} + +// 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 +} + +// Read a configuration file with the given basename w/o extension +func readConfigFile(name string, output interface{}) error { +	var suffix = filepath.Join(projectName, name+".json") +	for _, path := range getXdgConfigDirs() { +		full := filepath.Join(path, suffix) +		file, err := os.Open(full) +		if err != nil { +			if !os.IsNotExist(err) { +				return err +			} +			continue +		} +		defer file.Close() + +		decoder := json.NewDecoder(file) +		err = decoder.Decode(output) +		if err != nil { +			return fmt.Errorf("%s: %s", full, err) +		} +		return nil +	} +	return errors.New("configuration file not found") +} + +*/ + +import ( +	"bufio" +	"crypto/sha256" +	"crypto/tls" +	"encoding/hex" +	"flag" +	"fmt" +	"io" +	"log" +	"net" +	"os" +	"os/signal" +	"path/filepath" +	"regexp" +	"strconv" +	"strings" +	"syscall" +	"time" +) + +var debugMode = false + +// --- Utilities --------------------------------------------------------------- + +// +// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom +// must be at least three octets long for this to work reliably, but that should +// not pose a problem in practice. We might try waiting for them. +// +//  SSL2:      1xxx xxxx | xxxx xxxx |    <1> +//                (message length)  (client hello) +//  SSL3/TLS:    <22>    |    <3>    | xxxx xxxx +//            (handshake)|  (protocol version) +// +func detectTLS(sysconn syscall.RawConn) (isTLS bool) { +	sysconn.Read(func(fd uintptr) (done bool) { +		var buf [3]byte +		n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK) +		switch { +		case n == 3: +			isTLS = buf[0]&0x80 != 0 && buf[2] == 1 +			fallthrough +		case n == 2: +			isTLS = buf[0] == 22 && buf[1] == 3 +		case n == 1: +			isTLS = buf[0] == 22 +		case err == syscall.EAGAIN: +			return false +		} +		return true +	}) +	return isTLS +} + +// --- Configuration ----------------------------------------------------------- + +// XXX: Do we really want to support default nil values? +var config = []struct { +	key         string // INI key +	def         []rune // default value, may be nil +	description string // documentation +}{ +	// XXX: I'm not sure if Go will cooperate here. +	{"pid_file", nil, "Path or name of the PID file"}, +	{"bind", []rune(":6667"), "Address of the IRC server"}, +} + +// --- Rate limiter ------------------------------------------------------------ + +type floodDetector struct { +	interval   uint    // interval for the limit +	limit      uint    // maximum number of events allowed +	timestamps []int64 // timestamps of last events +	pos        uint    // index of the oldest event +} + +func newFloodDetector(interval, limit uint) *floodDetector { +	return &floodDetector{ +		interval:   interval, +		limit:      limit, +		timestamps: make([]int64, limit+1), +		pos:        0, +	} +} + +func (fd *floodDetector) check() bool { +	now := time.Now().Unix() +	fd.timestamps[fd.pos] = now + +	fd.pos++ +	if fd.pos > fd.limit { +		fd.pos = 0 +	} + +	var count uint +	begin := now - int64(fd.interval) +	for _, ts := range fd.timestamps { +		if ts >= 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 +} + +// TODO: To support ALL CAPS initialization of maps, perhaps we should use +// ircToUpper instead. +// FIXME: This doesn't follow the meaning of strxfrm and perhaps should be +// renamed to ircNormalize. +func ircStrxfrm(ident string) string { +	var canon []byte +	for _, c := range []byte(ident) { +		canon = append(canon, ircToLower(c)) +	} +	return string(canon) +} + +func ircFnmatch(pattern string, s string) bool { +	pattern, s = ircStrxfrm(pattern), ircStrxfrm(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 +} + +// TODO: We will need to add support for IRCv3 tags. +var reMsg = regexp.MustCompile( +	`^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`) +var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`) + +type message struct { +	nick    string   // optional nickname +	user    string   // optional username +	host    string   // optional hostname or IP address +	command string   // command name +	params  []string // arguments +} + +// Everything as per RFC 2812 +const ( +	ircMaxNickname      = 9 +	ircMaxHostname      = 63 +	ircMaxChannelName   = 50 +	ircMaxMessageLength = 510 +) + +// TODO: Port the IRC token validation part as needed. + +const reClassSpecial = "\\[\\]\\\\`_^{|}" + +var ( +	// 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\7\r\n ,:]+$`) +) + +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) +} + +// --- Clients (equals users) -------------------------------------------------- + +type connCloseWrite interface { +	net.Conn +	CloseWrite() error +} + +const ircSupportedUserModes = "aiwros" + +const ( +	ircUserModeInvisible = 1 << iota +	ircUserModeRxWallops +	ircUserModeRestricted +	ircUserModeOperator +	ircUserModeRxServerNotices +) + +const ( +	ircCapMultiPrefix = 1 << iota +	ircCapInviteNotify +	ircCapEchoMessage +	ircCapUserhostInNames +	ircCapServerTime +) + +type client struct { +	transport net.Conn       // underlying connection +	tls       *tls.Conn      // TLS, if detected +	conn      connCloseWrite // high-level connection +	inQ       []byte         // unprocessed input +	outQ      []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            int64 // 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  int64           // 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 = 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   int64  // creation time + +	topic     string // channel topic +	topicWho  string // who set the topic +	topicTime int64  // 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 newChannel() *channel { +	return &channel{userLimit: -1} +} + +// TODO: Port struct channel methods. + +// --- 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 { +	name                 string +	requiresRegistration bool +	handler              func(*message, *client) + +	nReceived     uint // number of commands received +	bytesReceived uint // 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: Port server_context. Maybe we want to keep it in a struct? +// XXX: Beware that maps with identifier keys need to be indexed correctly. +// We might want to enforce accessor functions for users and channels. +var ( +	started int64 // when has the server been started + +	users       map[string]*client  // maps nicknames to clients +	channels    map[string]*channel // maps channel names to data +	handlers    map[string]bool     // TODO message handlers +	capHandlers map[string]bool     // TODO CAP message handlers + +	whowas map[string]*whowasInfo // WHOWAS registry + +	serverName     string          // our server name +	pingInterval   uint            // ping interval in seconds +	maxConnections int             // max connections allowed or 0 +	motd           []string        // MOTD (none if empty) +	operators      map[string]bool // TLS certificate fingerprints for IRCops +) + +var ( +	sigs     = make(chan os.Signal, 1) +	conns    = make(chan net.Conn) +	prepared = make(chan preparedEvent) +	reads    = make(chan readEvent) +	writes   = make(chan writeEvent) +	timeouts = make(chan *client) + +	tlsConf  *tls.Config +	clients  = make(map[*client]bool) +	listener net.Listener +	// TODO: quitting, quitTimer as they are named in kike? +	inShutdown    bool +	shutdownTimer <-chan time.Time +) + +// Forcefully tear down all connections. +func forceShutdown(reason string) { +	if !inShutdown { +		log.Fatalln("forceShutdown called without initiateShutdown") +	} + +	log.Printf("forced shutdown (%s)\n", reason) +	for c := range clients { +		c.destroy("TODO") +	} +} + +// Initiate a clean shutdown of the whole daemon. +func initiateShutdown() { +	log.Println("shutting down") +	if err := listener.Close(); err != nil { +		log.Println(err) +	} +	for c := range clients { +		c.closeLink("TODO") +	} + +	shutdownTimer = time.After(5 * time.Second) +	inShutdown = true +} + +// TODO: ircChannelCreate + +func ircChannelDestroyIfEmpty(ch *channel) { +	// TODO +} + +// TODO: ircSendToRoommates +// Broadcast to all /other/ clients (telnet-friendly, also in accordance to +// the plan of extending this to an IRCd). +func broadcast(line string, except *client) { +	for c := range clients { +		if c != except { +			c.send(line) +		} +	} +} + +func ircSendToRoommates(c *client, message string) { +	// TODO +} + +// --- Clients (continued) ----------------------------------------------------- + +// TODO: Perhaps we should append to *[]byte for performance. +func clientModeToString(m uint, mode *string) { +	if 0 != m&ircUserModeInvisible { +		*mode += "i" +	} +	if 0 != m&ircUserModeRxWallops { +		*mode += "w" +	} +	if 0 != m&ircUserModeRestricted { +		*mode += "r" +	} +	if 0 != m&ircUserModeOperator { +		*mode += "o" +	} +	if 0 != m&ircUserModeRxServerNotices { +		*mode += "s" +	} +} + +func (c *client) getMode() string { +	mode := "" +	if c.awayMessage != "" { +		mode += "a" +	} +	clientModeToString(c.mode, &mode) +	return mode +} + +func (c *client) send(line string) { +	if c.conn == nil || c.closing { +		return +	} + +	// TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named. +	oldOutQ := len(c.outQ) + +	// 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.outQ = time.Now().UTC(). +			AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ") +	} + +	bytes := []byte(line) +	if len(bytes) > ircMaxMessageLength { +		bytes = bytes[:ircMaxMessageLength] +	} + +	// TODO: Kill the connection above some "SendQ" threshold (careful!) +	c.outQ = append(c.outQ, bytes...) +	c.outQ = append(c.outQ, "\r\n"...) +	c.flushOutQ() + +	// Technically we haven't sent it yet but that's a minor detail +	c.nSentMessages++ +	c.sentBytes += len(c.outQ) - oldOutQ +} + +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[ircStrxfrm(c.nickname)] = newWhowasInfo(c) +} + +func (c *client) unregister(reason string) { +	if !c.registered { +		return +	} + +	ircSendToRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s", +		c.nickname, c.username, c.hostname, reason)) + +	// The eventual QUIT message will take care of state at clients. +	for _, ch := range channels { +		delete(ch.userModes, c) +		ircChannelDestroyIfEmpty(ch) +	} + +	c.addToWhowas() +	delete(users, ircStrxfrm(c.nickname)) +	c.nickname = "" +	c.registered = false +} + +// TODO: Rename to kill. +// Close the connection and forget about the client. +func (c *client) destroy(reason string) { +	if reason == "" { +		reason = "Client exited" +	} +	c.unregister(reason) + +	// TODO: Log the address; seems like we always have c.address. +	log.Println("client destroyed") + +	// 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() +	} + +	// Clean up the goroutine, although a spurious event may still be sent. +	// TODO: Other timers if needed. +	if c.killTimer != nil { +		c.killTimer.Stop() +	} + +	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.destroy(reason) +		return +	} +	if c.closing { +		return +	} + +	nickname := c.nickname +	if nickname == "" { +		nickname = "*" +	} + +	// 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)", +		nickname, c.hostname /* TODO host IP? */, reason) +	c.closing = true + +	c.unregister(reason) +	c.killTimer = time.AfterFunc(3*time.Second, func() { +		timeouts <- c +	}) +} + +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 ------------------------------------------------------------------ + +// TODO + +// --- IRC command handling ---------------------------------------------------- + +// XXX: ap doesn't really need to be a slice. +func (c *client) makeReply(id int, ap []interface{}) string { +	nickname := c.nickname +	if nickname == "" { +		nickname = "*" +	} + +	s := fmt.Sprintf(":%s %03d %s ", serverName, id, nickname) +	a := fmt.Sprintf(defaultReplies[id], ap...) +	return s + a +} + +// XXX: This way we cannot typecheck the arguments, so we must 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). +	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]...) + +		// 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 isThisMe(target string) bool { +	// Target servers can also be matched by their users +	if ircFnmatch(target, serverName) { +		return true +	} +	_, ok := users[ircStrxfrm(target)] +	return ok +} + +func (c *client) sendISUPPORT() { +	// Only # channels, +e supported, +I supported, unlimited arguments to MODE +	c.sendReply(RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"+ +		" TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+ +		" NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName) +} + +func (c *client) tryFinishRegistration() { +	// TODO: Check if the realname is really required. +	if c.nickname == "" || c.username == "" || c.realname == "" { +		return +	} +	if c.registered || c.capNegotiating { +		return +	} + +	c.registered = true +	c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname) + +	c.sendReply(RPL_YOURHOST, serverName, "TODO version") +	// The purpose of this message eludes me. +	c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006")) +	c.sendReply(RPL_MYINFO, serverName, "TODO version", +		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, ircStrxfrm(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() +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// TODO: Beware of case sensitivity, probably need to index it by ircStrxfrm, +// which should arguably be named ircToLower and ircToUpper or something. +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 operato on +// global state, though. + +func ircHandleCAP(msg *message, c *client) { +	if len(msg.params) < 1 { +		c.sendReply(ERR_NEEDMOREPARAMS, msg.command) +		return +	} + +	// TODO: This really does seem to warrant a method. +	nickname := c.nickname +	if nickname == "" { +		nickname = "*" +	} + +	args := &ircCapArgs{ +		target:     nickname, +		subcommand: msg.params[0], +		fullParams: "", +		params:     []string{}, +	} + +	if len(msg.params) > 1 { +		args.fullParams = msg.params[1] +		// TODO: ignore_empty, likely create SplitSkipEmpty +		args.params = strings.Split(args.fullParams, " ") +	} + +	// FIXME: We should ASCII ToUpper the subcommand. +	if fn, ok := ircCapHandlers[ircStrxfrm(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 +	} + +	nicknameNormalized := ircStrxfrm(nickname) +	if client, ok := users[nicknameNormalized]; 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) +		ircSendToRoommates(c, message) +		c.send(message) +	} + +	// Release the old nickname and allocate a new one. +	if c.nickname != "" { +		delete(users, ircStrxfrm(c.nickname)) +	} + +	c.nickname = nickname +	users[nicknameNormalized] = 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 +	} + +	// TODO +} + +func ircHandleLUSERS(msg *message, c *client) { +	if len(msg.params) > 1 && !isThisMe(msg.params[1]) { +		c.sendReply(ERR_NOSUCHSERVER, msg.params[1]) +		return +	} +	c.sendLUSERS() +} + +func ircHandleMOTD(msg *message, c *client) { +	if len(msg.params) > 0 && !isThisMe(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 && !isThisMe(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 +	// TODO +} + +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 && !isThisMe(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 && !isThisMe(msg.params[0]) { +		c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) +		return +	} + +	postVersion := 0 +	if debugMode { +		postVersion = 1 +	} + +	c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName, +		"TODO program name"+" "+"TODO version") +	c.sendISUPPORT() +} + +/* +func ircChannelMulticast(ch *channel, msg string, except *client) { +	for c, m := 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) { +	// TODO: Port, as well as all the other kike functions. +} +*/ + +func ircHandleMODE(msg *message, c *client) { +	if len(msg.params) < 1 { +		c.sendReply(ERR_NEEDMOREPARAMS, msg.command) +		return +	} + +	// TODO +	target := msg.params[0] +	client := users[ircStrxfrm(target)] +	ch := users[ircStrxfrm(target)] + +	if client != nil { +		// TODO: Think about strcmp. +		//if ircStrcmp(target, c.nickname) != 0 { +		//} +	} else if ch != nil { +		// TODO +	} +} + +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] +	if client, ok := users[ircStrxfrm(target)]; ok { +		// TODO +		_ = client +		_ = text +	} else if ch, ok := channels[ircStrxfrm(target)]; ok { +		// TODO +		_ = ch +	} else { +		c.sendReply(ERR_NOSUCHNICK, target) +	} +} + +// TODO: All the various real command handlers. + +func ircHandleX(msg *message, c *client) { +} + +// --- ? ----------------------------------------------------------------------- + +// Handle the results from initializing the client's connection. +func (c *client) onPrepared(host string, isTLS bool) { +	if isTLS { +		c.tls = tls.Server(c.transport, tlsConf) +		c.conn = c.tls +	} else { +		c.conn = c.transport.(connCloseWrite) +	} + +	c.hostname = host +	c.address = net.JoinHostPort(host, c.port) + +	// TODO: If we've tried to send any data before now, we need to flushOutQ. +	go read(c) +	c.reading = true +} + +// 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.inQ = append(c.inQ, data...) +	for { +		// XXX: This accepts even simple LF newlines, even though they're not +		// really allowed by the protocol. +		advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */) +		if advance == 0 { +			break +		} + +		c.inQ = c.inQ[advance:] +		line := string(token) +		log.Printf("-> %s\n", line) + +		m := reMsg.FindStringSubmatch(line) +		if m == nil { +			log.Println("error: invalid line") +			continue +		} + +		msg := message{m[1], m[2], m[3], m[4], nil} +		for _, x := range reArgs.FindAllString(m[5], -1) { +			msg.params = append(msg.params, x[1:]) +		} + +		broadcast(line, c) +	} + +	if readErr != nil { +		c.reading = false + +		if readErr != io.EOF { +			log.Println(readErr) +			c.destroy("TODO") +		} else if c.closing { +			// Disregarding whether a clean shutdown has happened or not. +			log.Println("client finished shutdown") +			c.destroy("TODO") +		} else { +			log.Println("client EOF") +			c.closeLink("") +		} +	} else if len(c.inQ) > 8192 { +		log.Println("client inQ overrun") +		c.closeLink("inQ 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 outQ if possible and necessary. +func (c *client) flushOutQ() { +	if !c.writing && c.conn != nil { +		go write(c, c.outQ) +		c.writing = true +	} +} + +// Handle the results from trying to write to the client connection. +func (c *client) onWrite(written int, writeErr error) { +	c.outQ = c.outQ[written:] +	c.writing = false + +	if writeErr != nil { +		log.Println(writeErr) +		c.destroy("TODO") +	} else if len(c.outQ) > 0 { +		c.flushOutQ() +	} else if c.closing { +		if c.reading { +			c.conn.CloseWrite() +		} else { +			c.destroy("TODO") +		} +	} +} + +// --- Worker goroutines ------------------------------------------------------- + +func accept(ln net.Listener) { +	for { +		if conn, err := ln.Accept(); err != nil { +			// TODO: Consider specific cases in error handling, some errors +			// are transitional while others are fatal. +			log.Println(err) +			break +		} else { +			conns <- conn +		} +	} +} + +func prepare(client *client) { +	conn := client.transport +	host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) +	if err != nil { +		// In effect, we require TCP/UDP, as they have port numbers. +		log.Fatalln(err) +	} + +	// 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 { +			log.Println(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. +		log.Println(err) +	} else { +		isTLS = detectTLS(sysconn) +	} + +	// FIXME: When the client sends no data, we still initialize its conn. +	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 outQ, 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} +} + +// --- Main -------------------------------------------------------------------- + +func processOneEvent() { +	select { +	case <-sigs: +		if inShutdown { +			forceShutdown("requested by user") +		} else { +			initiateShutdown() +		} + +	case <-shutdownTimer: +		forceShutdown("timeout") + +	case conn := <-conns: +		log.Println("accepted client connection") + +		// In effect, we require TCP/UDP, as they have port numbers. +		address := conn.RemoteAddr().String() +		host, port, err := net.SplitHostPort(address) +		if err != nil { +			log.Fatalln(err) +		} + +		c := &client{ +			transport: conn, +			address:   address, +			hostname:  host, +			port:      port, +		} +		clients[c] = true +		go prepare(c) + +	case ev := <-prepared: +		log.Println("client is ready:", ev.host) +		if _, ok := clients[ev.client]; ok { +			ev.client.onPrepared(ev.host, ev.isTLS) +		} + +	case ev := <-reads: +		log.Println("received data from client") +		if _, ok := clients[ev.client]; ok { +			ev.client.onRead(ev.data, ev.err) +		} + +	case ev := <-writes: +		log.Println("sent data to client") +		if _, ok := clients[ev.client]; ok { +			ev.client.onWrite(ev.written, ev.err) +		} + +	case c := <-timeouts: +		if _, ok := clients[c]; ok { +			log.Println("client timeouted") +			c.destroy("TODO") +		} +	} +} + +func main() { +	flag.BoolVar(&debugMode, "debug", false, "debug mode") +	version := flag.Bool("version", false, "show version and exit") +	flag.Parse() + +	// TODO: Consider using the same version number for all subprojects. +	if *version { +		fmt.Printf("%s %s\n", "hid", "0") +		return +	} + +	// TODO: Configuration--create an INI parser, probably; +	//   lift XDG_CONFIG_HOME from gitlab-notifier. +	if len(flag.Args()) != 3 { +		log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0]) +	} + +	cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0)) +	if err != nil { +		log.Fatalln(err) +	} + +	tlsConf = &tls.Config{ +		Certificates:           []tls.Certificate{cert}, +		ClientAuth:             tls.RequestClientCert, +		SessionTicketsDisabled: true, +	} +	listener, err = net.Listen("tcp", flag.Arg(2)) +	if err != nil { +		log.Fatalln(err) +	} + +	go accept(listener) +	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + +	for !inShutdown || len(clients) > 0 { +		processOneEvent() +	} +} | 
