From 2d287752d4b17ebac1db25bc6a013215e8b32162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Sat, 28 Jul 2018 16:21:34 +0200 Subject: [PATCH 01/34] 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. --- xS/hid-gen-replies.sh | 16 + xS/hid-replies | 87 +++ xS/main.go | 1526 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 1629 insertions(+) create mode 100755 xS/hid-gen-replies.sh create mode 100644 xS/hid-replies create mode 100644 xS/main.go diff --git a/xS/hid-gen-replies.sh b/xS/hid-gen-replies.sh new file mode 100755 index 0000000..c32b000 --- /dev/null +++ b/xS/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/xS/hid-replies b/xS/hid-replies new file mode 100644 index 0000000..24affae --- /dev/null +++ b/xS/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/xS/main.go b/xS/main.go new file mode 100644 index 0000000..72a6df0 --- /dev/null +++ b/xS/main.go @@ -0,0 +1,1526 @@ +// +// Copyright (c) 2014 - 2018, Přemysl Janouch
+// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +// hid is a straight-forward port of kike IRCd from C. +package main + +/* + +// 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() + } +} From 208a8fcc7e48c64315977a92011ddd63c4a68eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Sun, 29 Jul 2018 07:50:27 +0200 Subject: [PATCH 02/34] hid: first round of mixed fixes and cleanups --- xS/main.go | 464 +++++++++++++++++++++++++++++------------------------ 1 file changed, 255 insertions(+), 209 deletions(-) diff --git a/xS/main.go b/xS/main.go index 72a6df0..98a8b3b 100644 --- a/xS/main.go +++ b/xS/main.go @@ -16,55 +16,101 @@ // hid is a straight-forward port of kike IRCd from C. package main -/* +import ( + "bufio" + "crypto/sha256" + "crypto/tls" + "encoding/hex" + "flag" + "fmt" + "io" + "log" + "net" + "os" + "os/signal" + "os/user" + "path/filepath" + "regexp" + "strconv" + "strings" + "syscall" + "time" +) -// 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" -} +var debugMode = false -func logErrorf(format string, args ...interface{}) { - fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...) -} +const ( + projectName = "hid" + // TODO: Consider using the same version number for all subprojects. + projectVersion = "0" +) -func logFatalf(format string, args ...interface{}) { - fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...) - os.Exit(1) -} +// --- Utilities --------------------------------------------------------------- -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") +// 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 } -// 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:] +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 } - return path + return "~" + username } -func getXdgHomeDir(name, def string) string { +// Tries to expand the tilde in paths, leaving it as-is on error. +func expandTilde(path string) string { + if path[0] != '~' { + return path + } + + var n int + for n = 0; n < len(path); n++ { + if path[n] == '/' { + break + } + } + return findTildeHome(path[1:n]) + path[n:] +} + +func getXDGHomeDir(name, def string) string { env := os.Getenv(name) if env != "" && env[0] == '/' { return env } - return filepath.Join(getHome(), def) + + 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) } -// Retrieve all XDG base directories for configuration files -func getXdgConfigDirs() (result []string) { - home := getXdgHomeDir("XDG_CONFIG_HOME", ".config") +// Retrieve all XDG base directories for configuration files. +func getXDGConfigDirs() (result []string) { + home := getXDGHomeDir("XDG_CONFIG_HOME", ".config") if home != "" { result = append(result, home) } @@ -80,56 +126,6 @@ func getXdgConfigDirs() (result []string) { 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 @@ -173,6 +169,35 @@ var config = []struct { {"bind", []rune(":6667"), "Address of the IRC server"}, } +/* + +// 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() + + // TODO: We don't want to use JSON. + 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") +} + +*/ + // --- Rate limiter ------------------------------------------------------------ type floodDetector struct { @@ -231,21 +256,40 @@ func ircToLower(c byte) byte { 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 { +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, ircToLower(c)) + 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 = ircStrxfrm(pattern), ircStrxfrm(s) - // FIXME: This should not support [] ranges and handle / specially. + 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 @@ -331,8 +375,8 @@ 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 + 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 @@ -459,10 +503,8 @@ type writeEvent struct { 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 + users map[string]*client // maps nicknames to clients + channels map[string]*channel // maps channel names to data whowas map[string]*whowasInfo // WHOWAS registry @@ -481,38 +523,38 @@ var ( 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 + tlsConf *tls.Config + clients = make(map[*client]bool) + listener net.Listener + quitting bool + quitTimer <-chan time.Time ) // Forcefully tear down all connections. -func forceShutdown(reason string) { - if !inShutdown { - log.Fatalln("forceShutdown called without initiateShutdown") +func forceQuit(reason string) { + if !quitting { + log.Fatalln("forceQuit called without initiateQuit") } log.Printf("forced shutdown (%s)\n", reason) for c := range clients { - c.destroy("TODO") + // initiateQuit has already unregistered the client. + c.kill("Shutting down") } } // Initiate a clean shutdown of the whole daemon. -func initiateShutdown() { +func initiateQuit() { log.Println("shutting down") if err := listener.Close(); err != nil { log.Println(err) } for c := range clients { - c.closeLink("TODO") + c.closeLink("Shutting down") } - shutdownTimer = time.After(5 * time.Second) - inShutdown = true + quitTimer = time.After(5 * time.Second) + quitting = true } // TODO: ircChannelCreate @@ -538,32 +580,31 @@ func ircSendToRoommates(c *client, message string) { // --- Clients (continued) ----------------------------------------------------- -// TODO: Perhaps we should append to *[]byte for performance. -func clientModeToString(m uint, mode *string) { +func clientModeToString(m uint, mode *[]byte) { if 0 != m&ircUserModeInvisible { - *mode += "i" + *mode = append(*mode, 'i') } if 0 != m&ircUserModeRxWallops { - *mode += "w" + *mode = append(*mode, 'w') } if 0 != m&ircUserModeRestricted { - *mode += "r" + *mode = append(*mode, 'r') } if 0 != m&ircUserModeOperator { - *mode += "o" + *mode = append(*mode, 'o') } if 0 != m&ircUserModeRxServerNotices { - *mode += "s" + *mode = append(*mode, 's') } } func (c *client) getMode() string { - mode := "" + var mode []byte if c.awayMessage != "" { - mode += "a" + mode = append(mode, 'a') } clientModeToString(c.mode, &mode) - return mode + return string(mode) } func (c *client) send(line string) { @@ -571,14 +612,13 @@ func (c *client) send(line string) { return } - // TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named. - oldOutQ := len(c.outQ) + 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.outQ = time.Now().UTC(). - AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ") + c.sendQ = time.Now().UTC(). + AppendFormat(c.sendQ, "@time=2006-01-02T15:04:05.000Z ") } bytes := []byte(line) @@ -587,13 +627,13 @@ func (c *client) send(line string) { } // TODO: Kill the connection above some "SendQ" threshold (careful!) - c.outQ = append(c.outQ, bytes...) - c.outQ = append(c.outQ, "\r\n"...) - c.flushOutQ() + 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.outQ) - oldOutQ + c.sentBytes += len(c.sendQ) - oldSendQLen } func (c *client) sendf(format string, a ...interface{}) { @@ -604,7 +644,14 @@ 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) + whowas[ircToCanon(c.nickname)] = newWhowasInfo(c) +} + +func (c *client) nicknameOrStar() string { + if c.nickname == "" { + return "*" + } + return c.nickname } func (c *client) unregister(reason string) { @@ -622,14 +669,13 @@ func (c *client) unregister(reason string) { } c.addToWhowas() - delete(users, ircStrxfrm(c.nickname)) + delete(users, ircToCanon(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) { +func (c *client) kill(reason string) { if reason == "" { reason = "Client exited" } @@ -661,25 +707,20 @@ func (c *client) closeLink(reason string) { // We also want to avoid accidentally writing to the socket before // address resolution has finished. if c.conn == nil { - c.destroy(reason) + c.kill(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.nicknameOrStar(), c.hostname /* TODO host IP? */, reason) c.closing = true c.unregister(reason) @@ -720,12 +761,7 @@ func (c *client) getTLSCertFingerprint() string { // 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) + s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar()) a := fmt.Sprintf(defaultReplies[id], ap...) return s + a } @@ -809,7 +845,7 @@ func isThisMe(target string) bool { if ircFnmatch(target, serverName) { return true } - _, ok := users[ircStrxfrm(target)] + _, ok := users[ircToCanon(target)] return ok } @@ -821,21 +857,20 @@ func (c *client) sendISUPPORT() { } func (c *client) tryFinishRegistration() { - // TODO: Check if the realname is really required. - if c.nickname == "" || c.username == "" || c.realname == "" { + if c.registered || c.capNegotiating { return } - if c.registered || c.capNegotiating { + if c.nickname == "" || c.username == "" { return } c.registered = true c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname) - c.sendReply(RPL_YOURHOST, serverName, "TODO version") + c.sendReply(RPL_YOURHOST, serverName, projectVersion) // 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", + c.sendReply(RPL_MYINFO, serverName, projectVersion, ircSupportedUserModes, ircSupportedChanModes) c.sendISUPPORT() @@ -852,7 +887,7 @@ func (c *client) tryFinishRegistration() { serverName, c.nickname, c.tlsCertFingerprint) } - delete(whowas, ircStrxfrm(c.nickname)) + delete(whowas, ircToCanon(c.nickname)) } // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -957,8 +992,6 @@ func (c *client) handleCAPEND(a *ircCapArgs) { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -// 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, @@ -976,14 +1009,8 @@ func ircHandleCAP(msg *message, c *client) { return } - // TODO: This really does seem to warrant a method. - nickname := c.nickname - if nickname == "" { - nickname = "*" - } - args := &ircCapArgs{ - target: nickname, + target: c.nicknameOrStar(), subcommand: msg.params[0], fullParams: "", params: []string{}, @@ -991,12 +1018,10 @@ func ircHandleCAP(msg *message, c *client) { if len(msg.params) > 1 { args.fullParams = msg.params[1] - // TODO: ignore_empty, likely create SplitSkipEmpty - args.params = strings.Split(args.fullParams, " ") + args.params = splitString(args.fullParams, " ", true) } - // FIXME: We should ASCII ToUpper the subcommand. - if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok { + if fn, ok := ircCapHandlers[ircToCanon(args.subcommand)]; !ok { c.sendReply(ERR_INVALIDCAPCMD, args.subcommand, "Invalid CAP subcommand") } else { @@ -1026,8 +1051,8 @@ func ircHandleNICK(msg *message, c *client) { return } - nicknameNormalized := ircStrxfrm(nickname) - if client, ok := users[nicknameNormalized]; ok && client != c { + nicknameCanon := ircToCanon(nickname) + if client, ok := users[nicknameCanon]; ok && client != c { c.sendReply(ERR_NICKNAMEINUSE, nickname) return } @@ -1043,11 +1068,11 @@ func ircHandleNICK(msg *message, c *client) { // Release the old nickname and allocate a new one. if c.nickname != "" { - delete(users, ircStrxfrm(c.nickname)) + delete(users, ircToCanon(c.nickname)) } c.nickname = nickname - users[nicknameNormalized] = c + users[nicknameCanon] = c c.tryFinishRegistration() } @@ -1064,7 +1089,7 @@ func ircHandleUSER(msg *message, c *client) { username, mode, realname := msg.params[0], msg.params[1], msg.params[3] - // Unfortunately the protocol doesn't give us any means of rejecting it + // Unfortunately, the protocol doesn't give us any means of rejecting it. if !ircIsValidUsername(username) { username = "*" } @@ -1091,7 +1116,30 @@ func ircHandleUSERHOST(msg *message, c *client) { return } - // TODO + 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) { @@ -1162,8 +1210,8 @@ func ircHandleVERSION(msg *message, c *client) { postVersion = 1 } - c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName, - "TODO program name"+" "+"TODO version") + c.sendReply(RPL_VERSION, projectVersion, postVersion, serverName, + projectName+" "+projectVersion) c.sendISUPPORT() } @@ -1199,13 +1247,13 @@ func ircHandleMODE(msg *message, c *client) { // TODO target := msg.params[0] - client := users[ircStrxfrm(target)] - ch := users[ircStrxfrm(target)] + client := users[ircToCanon(target)] + ch := users[ircToCanon(target)] if client != nil { - // TODO: Think about strcmp. - //if ircStrcmp(target, c.nickname) != 0 { - //} + // TODO + if ircEqual(target, c.nickname) { + } } else if ch != nil { // TODO } @@ -1223,11 +1271,11 @@ func ircHandleUserMessage(msg *message, c *client, } target, text := msg.params[0], msg.params[1] - if client, ok := users[ircStrxfrm(target)]; ok { + if client, ok := users[ircToCanon(target)]; ok { // TODO _ = client _ = text - } else if ch, ok := channels[ircStrxfrm(target)]; ok { + } else if ch, ok := channels[ircToCanon(target)]; ok { // TODO _ = ch } else { @@ -1254,7 +1302,7 @@ func (c *client) onPrepared(host string, isTLS bool) { 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. + // TODO: If we've tried to send any data before now, we need to flushSendQ. go read(c) c.reading = true } @@ -1266,16 +1314,16 @@ func (c *client) onRead(data []byte, readErr error) { return } - c.inQ = append(c.inQ, data...) + 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.inQ, false /* atEOF */) + advance, token, _ := bufio.ScanLines(c.recvQ, false /* atEOF */) if advance == 0 { break } - c.inQ = c.inQ[advance:] + c.recvQ = c.recvQ[advance:] line := string(token) log.Printf("-> %s\n", line) @@ -1298,18 +1346,18 @@ func (c *client) onRead(data []byte, readErr error) { if readErr != io.EOF { log.Println(readErr) - c.destroy("TODO") + c.kill(readErr.Error()) } else if c.closing { // Disregarding whether a clean shutdown has happened or not. log.Println("client finished shutdown") - c.destroy("TODO") + c.kill("TODO") } else { log.Println("client EOF") c.closeLink("") } - } else if len(c.inQ) > 8192 { - log.Println("client inQ overrun") - c.closeLink("inQ overrun") + } else if len(c.recvQ) > 8192 { + log.Println("client recvQ overrun") + 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 @@ -1319,29 +1367,29 @@ func (c *client) onRead(data []byte, readErr error) { } } -// Spawn a goroutine to flush the outQ if possible and necessary. -func (c *client) flushOutQ() { +// 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.outQ) + 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.outQ = c.outQ[written:] + c.sendQ = c.sendQ[written:] c.writing = false if writeErr != nil { log.Println(writeErr) - c.destroy("TODO") - } else if len(c.outQ) > 0 { - c.flushOutQ() + c.kill(writeErr.Error()) + } else if len(c.sendQ) > 0 { + c.flushSendQ() } else if c.closing { if c.reading { c.conn.CloseWrite() } else { - c.destroy("TODO") + c.kill("TODO") } } } @@ -1419,7 +1467,7 @@ func read(client *client) { } } -// Flush outQ, which is passed by parameter so that there are no data races. +// 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) @@ -1431,14 +1479,14 @@ func write(client *client, data []byte) { func processOneEvent() { select { case <-sigs: - if inShutdown { - forceShutdown("requested by user") + if quitting { + forceQuit("requested by user") } else { - initiateShutdown() + initiateQuit() } - case <-shutdownTimer: - forceShutdown("timeout") + case <-quitTimer: + forceQuit("timeout") case conn := <-conns: log.Println("accepted client connection") @@ -1480,7 +1528,7 @@ func processOneEvent() { case c := <-timeouts: if _, ok := clients[c]; ok { log.Println("client timeouted") - c.destroy("TODO") + c.kill("TODO") } } } @@ -1490,14 +1538,12 @@ func main() { 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") + fmt.Printf("%s %s\n", projectName, projectVersion) return } - // TODO: Configuration--create an INI parser, probably; - // lift XDG_CONFIG_HOME from gitlab-notifier. + // TODO: Configuration--create an INI parser, probably. if len(flag.Args()) != 3 { log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0]) } @@ -1520,7 +1566,7 @@ func main() { go accept(listener) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) - for !inShutdown || len(clients) > 0 { + for !quitting || len(clients) > 0 { processOneEvent() } } From 3322fe2851adfa91f3713d5c51d60c0ae34d0905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Sun, 29 Jul 2018 15:57:39 +0200 Subject: [PATCH 03/34] hid: port PRIVMSG, NOTICE, NAMES, WHO, WHOIS/WAS, TOPIC, SUMMON, USERS --- xS/main.go | 520 +++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 486 insertions(+), 34 deletions(-) diff --git a/xS/main.go b/xS/main.go index 98a8b3b..75f4231 100644 --- a/xS/main.go +++ b/xS/main.go @@ -201,7 +201,7 @@ func readConfigFile(name string, output interface{}) error { // --- Rate limiter ------------------------------------------------------------ type floodDetector struct { - interval uint // interval for the limit + interval uint // interval for the limit in seconds limit uint // maximum number of events allowed timestamps []int64 // timestamps of last events pos uint // index of the oldest event @@ -217,7 +217,7 @@ func newFloodDetector(interval, limit uint) *floodDetector { } func (fd *floodDetector) check() bool { - now := time.Now().Unix() + now := time.Now().UnixNano() fd.timestamps[fd.pos] = now fd.pos++ @@ -226,7 +226,7 @@ func (fd *floodDetector) check() bool { } var count uint - begin := now - int64(fd.interval) + begin := now - int64(time.Second)*int64(fd.interval) for _, ts := range fd.timestamps { if ts >= begin { count++ @@ -471,12 +471,11 @@ func newWhowasInfo(c *client) *whowasInfo { // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - type ircCommand struct { - name string requiresRegistration bool handler func(*message, *client) nReceived uint // number of commands received - bytesReceived uint // number of bytes received total + bytesReceived int // number of bytes received total } type preparedEvent struct { @@ -563,39 +562,29 @@ 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) ----------------------------------------------------- -func clientModeToString(m uint, mode *[]byte) { +func ircAppendClientModes(m uint, mode []byte) []byte { if 0 != m&ircUserModeInvisible { - *mode = append(*mode, 'i') + mode = append(mode, 'i') } if 0 != m&ircUserModeRxWallops { - *mode = append(*mode, 'w') + mode = append(mode, 'w') } if 0 != m&ircUserModeRestricted { - *mode = append(*mode, 'r') + mode = append(mode, 'r') } if 0 != m&ircUserModeOperator { - *mode = append(*mode, 'o') + mode = append(mode, 'o') } if 0 != m&ircUserModeRxServerNotices { - *mode = append(*mode, 's') + mode = append(mode, 's') } + return mode } func (c *client) getMode() string { @@ -603,8 +592,7 @@ func (c *client) getMode() string { if c.awayMessage != "" { mode = append(mode, 'a') } - clientModeToString(c.mode, &mode) - return string(mode) + return string(ircAppendClientModes(c.mode, mode)) } func (c *client) send(line string) { @@ -1215,15 +1203,15 @@ func ircHandleVERSION(msg *message, c *client) { c.sendISUPPORT() } -/* func ircChannelMulticast(ch *channel, msg string, except *client) { - for c, m := range ch.userModes { + for c := range ch.userModes { if c != except { c.send(msg) } } } +/* func ircModifyMode(mask *uint, mode uint, add bool) bool { orig := *mask if add { @@ -1271,21 +1259,484 @@ func ircHandleUserMessage(msg *message, c *client, } target, text := msg.params[0], msg.params[1] - if client, ok := users[ircToCanon(target)]; ok { - // TODO - _ = client - _ = text - } else if ch, ok := channels[ircToCanon(target)]; ok { - // TODO - _ = ch + 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 */) + // Let's not care too much about success or failure. + c.lastActive = time.Now().UnixNano() +} + +func ircHandleNOTICE(msg *message, c *client) { + ircHandleUserMessage(msg, c, "NOTICE", false /* allowAwayReply */) +} + +func ircHandleLIST(msg *message, c *client) { + if len(msg.params) > 1 && !isThisMe(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 +} + +// TODO: Consider using *client instead of string as the map key. +func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[string]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[ircToCanon(client.nickname)] = true + } + nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes)) + } + c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "") +} + +func ircSendDisassociatedNames(c *client, usedNicks map[string]bool) { + var nicks []string + for canonNickname, client := range users { + if 0 == client.mode&ircUserModeInvisible && !usedNicks[canonNickname] { + 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 && !isThisMe(msg.params[1]) { + c.sendReply(ERR_NOSUCHSERVER, msg.params[1]) + return + } + + if len(msg.params) == 0 { + usedNicks := make(map[string]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, "TODO server_info from configuration") + if 0 != target.mode&ircUserModeOperator { + c.sendReply(RPL_WHOISOPERATOR, nick) + } + c.sendReply(RPL_WHOISIDLE, nick, + (time.Now().UnixNano()-target.lastActive)/int64(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 && !isThisMe(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 && !isThisMe(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, "TODO server_info from configuration") + } + 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/int64(time.Second)) + } +} + +func ircHandleTOPIC(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + + target := msg.params[0] + ch := channels[ircToCanon(target)] + if ch == nil { + c.sendReply(ERR_NOSUCHCHANNEL, target) + return + } + + if len(msg.params) < 2 { + ircSendRPLTOPIC(c, ch) + return + } + + modes, present := ch.userModes[c] + if !present { + c.sendReply(ERR_NOTONCHANNEL, target) + return + } + + if 0 != ch.modes&ircChanModeProtectedTopic && + 0 == modes&ircChanModeOperator { + c.sendReply(ERR_CHANOPRIVSNEEDED, target) + return + } + + ch.topic = msg.params[1] + ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname) + ch.topicTime = time.Now().UnixNano() + + message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s", + c.nickname, c.username, c.hostname, target, ch.topic) + ircChannelMulticast(ch, message, nil) +} + // TODO: All the various real command handlers. func ircHandleX(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } +} + +func ircHandleSUMMON(msg *message, c *client) { + c.sendReply(ERR_SUMMONDISABLED) +} + +func ircHandleUSERS(msg *message, c *client) { + c.sendReply(ERR_USERSDISABLED) +} + +// ----------------------------------------------------------------------------- + +// TODO: Add an index for IRC_ERR_NOSUCHSERVER validation? +// TODO: Add a minimal parameter count? +// TODO: Add a field for oper-only commands? +var ircHandlers = map[string]*ircCommand{ + "CAP": {false, ircHandleCAP, 0, 0}, + "PASS": {false, ircHandlePASS, 0, 0}, + "NICK": {false, ircHandleNICK, 0, 0}, + "USER": {false, ircHandleUSER, 0, 0}, + + "USERHOST": {true, ircHandleUSERHOST, 0, 0}, + "LUSERS": {true, ircHandleLUSERS, 0, 0}, + "MOTD": {true, ircHandleMOTD, 0, 0}, + "PING": {true, ircHandlePING, 0, 0}, + "PONG": {false, ircHandlePONG, 0, 0}, + "QUIT": {false, ircHandleQUIT, 0, 0}, + "TIME": {true, ircHandleTIME, 0, 0}, + "VERSION": {true, ircHandleVERSION, 0, 0}, + "USERS": {true, ircHandleUSERS, 0, 0}, + "SUMMON": {true, ircHandleSUMMON, 0, 0}, + + "MODE": {true, ircHandleMODE, 0, 0}, + "PRIVMSG": {true, ircHandlePRIVMSG, 0, 0}, + "NOTICE": {true, ircHandleNOTICE, 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}, +} + +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) + } + } } // --- ? ----------------------------------------------------------------------- @@ -1338,7 +1789,8 @@ func (c *client) onRead(data []byte, readErr error) { msg.params = append(msg.params, x[1:]) } - broadcast(line, c) + // XXX: And since it accepts LF, we miscalculate receivedBytes within. + ircProcessMessage(c, &msg, line) } if readErr != nil { From 50e7f7dca5b65de404b3e397040172c73c47dc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Sun, 29 Jul 2018 17:49:57 +0200 Subject: [PATCH 04/34] hid: port PART, KICK, INVITE, JOIN, AWAY, ISON, ADMIN, DIE --- xS/main.go | 340 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 331 insertions(+), 9 deletions(-) diff --git a/xS/main.go b/xS/main.go index 75f4231..4c9176b 100644 --- a/xS/main.go +++ b/xS/main.go @@ -556,14 +556,40 @@ func initiateQuit() { quitting = true } -// TODO: ircChannelCreate - -func ircChannelDestroyIfEmpty(ch *channel) { - // TODO +func ircChannelCreate(name string) *channel { + ch := &channel{ + name: name, + created: time.Now().UnixNano(), + userLimit: -1, + } + channels[ircToCanon(name)] = ch + return ch } +func ircChannelDestroyIfEmpty(ch *channel) { + if len(ch.userModes) == 0 { + delete(channels, ircToCanon(ch.name)) + } +} + +// TODO: Improve the name as it takes mode +q into account. func ircSendToRoommates(c *client, message string) { - // TODO + 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) ----------------------------------------------------- @@ -1662,13 +1688,248 @@ func ircHandleTOPIC(msg *message, c *client) { ircChannelMulticast(ch, message, nil) } -// TODO: All the various real command handlers. +func ircTryPart(c *client, target string, reason string) { + if reason == "" { + reason = c.nickname + } -func ircHandleX(msg *message, c *client) { + ch := channels[ircToCanon(target)] + if ch == nil { + c.sendReply(ERR_NOSUCHCHANNEL, target) + return + } + + if _, present := ch.userModes[c]; !present { + c.sendReply(ERR_NOTONCHANNEL, target) + return + } + + message := fmt.Sprintf(":%s@%s@%s PART %s :%s", + c.nickname, c.username, c.hostname, target, 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 _, target := range splitString(msg.params[0], ",", true) { + ircTryPart(c, target, reason) + } +} + +// TODO: Undo the rename from channelName to target, also in ircTryPart. +func ircTryKick(c *client, target, nick, reason string) { + ch := channels[ircToCanon(target)] + if ch == nil { + c.sendReply(ERR_NOSUCHCHANNEL, target) + return + } + + if modes, present := ch.userModes[c]; !present { + c.sendReply(ERR_NOTONCHANNEL, target) + return + } else if 0 == modes&ircChanModeOperator { + c.sendReply(ERR_CHANOPRIVSNEEDED, target) + return + } + + client := users[ircToCanon(nick)] + if _, present := ch.userModes[client]; client == nil || !present { + c.sendReply(ERR_USERNOTINCHANNEL, nick, target) + return + } + + message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s", + c.nickname, c.username, c.hostname, target, 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) { @@ -1679,11 +1940,63 @@ 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 && !isThisMe(msg.params[0]) { + c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) + return + } + c.sendReply(ERR_NOADMININFO, serverName) +} + +// TODO: All the remaining command handlers. + +func ircHandleX(msg *message, c *client) { + if len(msg.params) < 1 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } +} + +func ircHandleDIE(msg *message, c *client) { + if 0 == c.mode&ircUserModeOperator { + c.sendReply(ERR_NOPRIVILEGES) + return + } + if !quitting { + initiateQuit() + } +} + // ----------------------------------------------------------------------------- // TODO: Add an index for IRC_ERR_NOSUCHSERVER validation? // TODO: Add a minimal parameter count? -// TODO: Add a field for oper-only commands? +// TODO: Add a field for oper-only commands? Use flags? var ircHandlers = map[string]*ircCommand{ "CAP": {false, ircHandleCAP, 0, 0}, "PASS": {false, ircHandlePASS, 0, 0}, @@ -1700,16 +2013,25 @@ var ircHandlers = map[string]*ircCommand{ "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}, "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}, + + "DIE": {true, ircHandleDIE, 0, 0}, } func ircProcessMessage(c *client, msg *message, raw string) { @@ -1739,7 +2061,7 @@ func ircProcessMessage(c *client, msg *message, raw string) { } } -// --- ? ----------------------------------------------------------------------- +// --- Network I/O ------------------------------------------------------------- // Handle the results from initializing the client's connection. func (c *client) onPrepared(host string, isTLS bool) { From 90129ee2bcf67f978b7746e80d2509a8141a3049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 30 Jul 2018 09:42:01 +0200 Subject: [PATCH 05/34] hid: port MODE, STATS, LINKS, KILL Now all the commands have been ported but we desperately need to parse a configuration file for additional settings yet. --- xS/main.go | 584 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 561 insertions(+), 23 deletions(-) diff --git a/xS/main.go b/xS/main.go index 4c9176b..ff6a84a 100644 --- a/xS/main.go +++ b/xS/main.go @@ -18,6 +18,7 @@ package main import ( "bufio" + "bytes" "crypto/sha256" "crypto/tls" "encoding/hex" @@ -45,6 +46,11 @@ const ( projectVersion = "0" ) +// TODO: Consider using time.Time directly instead of storing Unix epoch +// timestamps with nanosecond precision. Despite carrying unnecessary timezone +// information, it also carries a monotonic reading of the time, which allows +// for more precise measurement of time differences. + // --- Utilities --------------------------------------------------------------- // Split a string by a set of UTF-8 delimiters, optionally ignoring empty items. @@ -316,8 +322,6 @@ const ( ircMaxMessageLength = 510 ) -// TODO: Port the IRC token validation part as needed. - const reClassSpecial = "\\[\\]\\\\`_^{|}" var ( @@ -330,6 +334,9 @@ var ( reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`) reChannelName = regexp.MustCompile(`^[^\0\7\r\n ,:]+$`) + reKey = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`) + reUserMask = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`) + reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`) ) func ircIsValidNickname(nickname string) bool { @@ -346,6 +353,19 @@ 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 connCloseWrite interface { @@ -356,7 +376,7 @@ type connCloseWrite interface { const ircSupportedUserModes = "aiwros" const ( - ircUserModeInvisible = 1 << iota + ircUserModeInvisible uint = 1 << iota ircUserModeRxWallops ircUserModeRestricted ircUserModeOperator @@ -364,7 +384,7 @@ const ( ) const ( - ircCapMultiPrefix = 1 << iota + ircCapMultiPrefix uint = 1 << iota ircCapInviteNotify ircCapEchoMessage ircCapUserhostInNames @@ -417,7 +437,7 @@ type client struct { const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl" const ( - ircChanModeInviteOnly = 1 << iota + ircChanModeInviteOnly uint = 1 << iota ircChanModeModerated ircChanModeNoOutsideMsgs ircChanModeQuiet @@ -447,11 +467,48 @@ type channel struct { inviteList []string // exceptions from +I } -func newChannel() *channel { - return &channel{userLimit: -1} -} +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') + } -// TODO: Port struct channel methods. + 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 ------------------------------------------------------ @@ -854,6 +911,7 @@ func (c *client) sendLUSERS() { c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */) } +// TODO: Rename back to ircIsThisMe for consistency with kike. func isThisMe(target string) bool { // Target servers can also be matched by their users if ircFnmatch(target, serverName) { @@ -1237,7 +1295,6 @@ func ircChannelMulticast(ch *channel, msg string, except *client) { } } -/* func ircModifyMode(mask *uint, mode uint, add bool) bool { orig := *mask if add { @@ -1249,9 +1306,365 @@ func ircModifyMode(mask *uint, mode uint, add bool) bool { } func ircUpdateUserMode(c *client, newMode uint) { - // TODO: Port, as well as all the other kike functions. + 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 { @@ -1259,17 +1672,32 @@ func ircHandleMODE(msg *message, c *client) { return } - // TODO target := msg.params[0] client := users[ircToCanon(target)] - ch := users[ircToCanon(target)] + ch := channels[ircToCanon(target)] if client != nil { - // TODO 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 { - // TODO + if len(msg.params) < 2 { + _, present := ch.userModes[c] + c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present)) + c.sendReply(RPL_CREATIONTIME, + target, ch.created/int64(time.Second)) + } else { + ircHandleChanModeChange(c, ch, msg.params[1:]) + } + } else { + c.sendReply(ERR_NOSUCHNICK, target) } } @@ -1973,21 +2401,128 @@ func ircHandleADMIN(msg *message, c *client) { c.sendReply(ERR_NOADMININFO, serverName) } -// TODO: All the remaining command handlers. +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] + } -func ircHandleX(msg *message, c *client) { - if len(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().UnixNano()-client.opened)/int64(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 #1817. +var ircHandleStatsCommandsIndirect func(c *client) + +func init() { + ircHandleStatsCommandsIndirect = ircHandleStatsCommands +} + +func ircHandleStatsUptime(c *client) { + uptime := (time.Now().UnixNano() - started) / int64(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 && !isThisMe(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 && !isThisMe(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 */, "TODO server_info from configuration") + } + c.sendReply(RPL_ENDOFLINKS, mask) +} + +func ircHandleKILL(msg *message, c *client) { + if len(msg.params) < 2 { + c.sendReply(ERR_NEEDMOREPARAMS, msg.command) + return + } + if 0 == c.mode&ircUserModeOperator { + c.sendReply(ERR_NOPRIVILEGES) + return + } + + target := users[ircToCanon(msg.params[0])] + if target == nil { + c.sendReply(ERR_NOSUCHNICK, msg.params[0]) + return + } + + c.sendf(":%s!%s@%s KILL %s :%s", + c.nickname, c.username, c.hostname, target.nickname, msg.params[1]) + target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1])) } func ircHandleDIE(msg *message, c *client) { if 0 == c.mode&ircUserModeOperator { c.sendReply(ERR_NOPRIVILEGES) - return - } - if !quitting { + } else if !quitting { initiateQuit() } } @@ -2015,6 +2550,8 @@ var ircHandlers = map[string]*ircCommand{ "SUMMON": {true, ircHandleSUMMON, 0, 0}, "AWAY": {true, ircHandleAWAY, 0, 0}, "ADMIN": {true, ircHandleADMIN, 0, 0}, + "STATS": {true, ircHandleSTATS, 0, 0}, + "LINKS": {true, ircHandleLINKS, 0, 0}, "MODE": {true, ircHandleMODE, 0, 0}, "PRIVMSG": {true, ircHandlePRIVMSG, 0, 0}, @@ -2031,7 +2568,8 @@ var ircHandlers = map[string]*ircCommand{ "WHOWAS": {true, ircHandleWHOWAS, 0, 0}, "ISON": {true, ircHandleISON, 0, 0}, - "DIE": {true, ircHandleDIE, 0, 0}, + "KILL": {true, ircHandleKILL, 0, 0}, + "DIE": {true, ircHandleDIE, 0, 0}, } func ircProcessMessage(c *client, msg *message, raw string) { From 404aa8c9cc7069e284e98b9e1f5964fc2dc6eca0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 30 Jul 2018 10:04:05 +0200 Subject: [PATCH 06/34] hid: use time.Time and time.Duration It improves the code significantly over explicit int64 conversions. Despite carrying unnecessary timezone information, time.Time also carries a monotonic reading of time, which allows for more precise measurement of time differences. --- xS/main.go | 87 ++++++++++++++++++++++++++---------------------------- 1 file changed, 42 insertions(+), 45 deletions(-) diff --git a/xS/main.go b/xS/main.go index ff6a84a..1c72fca 100644 --- a/xS/main.go +++ b/xS/main.go @@ -46,11 +46,6 @@ const ( projectVersion = "0" ) -// TODO: Consider using time.Time directly instead of storing Unix epoch -// timestamps with nanosecond precision. Despite carrying unnecessary timezone -// information, it also carries a monotonic reading of the time, which allows -// for more precise measurement of time differences. - // --- Utilities --------------------------------------------------------------- // Split a string by a set of UTF-8 delimiters, optionally ignoring empty items. @@ -207,23 +202,23 @@ func readConfigFile(name string, output interface{}) error { // --- Rate limiter ------------------------------------------------------------ type floodDetector struct { - interval uint // interval for the limit in seconds - limit uint // maximum number of events allowed - timestamps []int64 // timestamps of last events - pos uint // index of the oldest event + 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, limit uint) *floodDetector { +func newFloodDetector(interval time.Duration, limit uint) *floodDetector { return &floodDetector{ interval: interval, limit: limit, - timestamps: make([]int64, limit+1), + timestamps: make([]time.Time, limit+1), pos: 0, } } func (fd *floodDetector) check() bool { - now := time.Now().UnixNano() + now := time.Now() fd.timestamps[fd.pos] = now fd.pos++ @@ -232,9 +227,9 @@ func (fd *floodDetector) check() bool { } var count uint - begin := now - int64(time.Second)*int64(fd.interval) + begin := now.Add(-fd.interval) for _, ts := range fd.timestamps { - if ts >= begin { + if ts.After(begin) { count++ } } @@ -402,11 +397,11 @@ type client struct { 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 + 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 @@ -427,9 +422,9 @@ type client struct { mode uint // user's mode awayMessage string // away message - lastActive int64 // last PRIVMSG, to get idle time + lastActive time.Time // last PRIVMSG, to get idle time invites map[string]bool // channel invitations by operators - antiflood floodDetector // flood detector + antiflood *floodDetector // flood detector } // --- Channels ---------------------------------------------------------------- @@ -450,15 +445,15 @@ const ( ) 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 + 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 int64 // when the topic was set + 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 @@ -557,7 +552,7 @@ type writeEvent 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 + started time.Time // when has the server been started users map[string]*client // maps nicknames to clients channels map[string]*channel // maps channel names to data @@ -616,7 +611,7 @@ func initiateQuit() { func ircChannelCreate(name string) *channel { ch := &channel{ name: name, - created: time.Now().UnixNano(), + created: time.Now(), userLimit: -1, } channels[ircToCanon(name)] = ch @@ -941,7 +936,7 @@ func (c *client) tryFinishRegistration() { c.sendReply(RPL_YOURHOST, serverName, projectVersion) // The purpose of this message eludes me. - c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006")) + c.sendReply(RPL_CREATED, started.Format("Mon, 02 Jan 2006")) c.sendReply(RPL_MYINFO, serverName, projectVersion, ircSupportedUserModes, ircSupportedChanModes) @@ -1691,8 +1686,7 @@ func ircHandleMODE(msg *message, c *client) { if len(msg.params) < 2 { _, present := ch.userModes[c] c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present)) - c.sendReply(RPL_CREATIONTIME, - target, ch.created/int64(time.Second)) + c.sendReply(RPL_CREATIONTIME, target, ch.created.Unix()) } else { ircHandleChanModeChange(c, ch, msg.params[1:]) } @@ -1752,8 +1746,7 @@ func ircHandleUserMessage(msg *message, c *client, func ircHandlePRIVMSG(msg *message, c *client) { ircHandleUserMessage(msg, c, "PRIVMSG", true /* allowAwayReply */) - // Let's not care too much about success or failure. - c.lastActive = time.Now().UnixNano() + c.lastActive = time.Now() } func ircHandleNOTICE(msg *message, c *client) { @@ -1980,7 +1973,7 @@ func ircSendWHOISReply(c, target *client) { c.sendReply(RPL_WHOISOPERATOR, nick) } c.sendReply(RPL_WHOISIDLE, nick, - (time.Now().UnixNano()-target.lastActive)/int64(time.Second)) + time.Now().Sub(target.lastActive)/time.Second) if target.awayMessage != "" { c.sendReply(RPL_AWAY, nick, target.awayMessage) } @@ -2073,7 +2066,7 @@ func ircSendRPLTOPIC(c *client, ch *channel) { } else { c.sendReply(RPL_TOPIC, ch.name, ch.topic) c.sendReply(RPL_TOPICWHOTIME, - ch.name, ch.topicWho, ch.topicTime/int64(time.Second)) + ch.name, ch.topicWho, ch.topicTime.Unix()) } } @@ -2109,7 +2102,7 @@ func ircHandleTOPIC(msg *message, c *client) { ch.topic = msg.params[1] ch.topicWho = fmt.Sprintf("%s@%s@%s", c.nickname, c.username, c.hostname) - ch.topicTime = time.Now().UnixNano() + ch.topicTime = time.Now() message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s", c.nickname, c.username, c.hostname, target, ch.topic) @@ -2418,7 +2411,7 @@ func ircHandleStatsLinks(c *client, msg *message) { len(client.sendQ), // sendq client.nSentMessages, client.sentBytes/1024, client.nReceivedMessages, client.receivedBytes/1024, - (time.Now().UnixNano()-client.opened)/int64(time.Second)) + time.Now().Sub(client.opened)/time.Second) } } @@ -2440,7 +2433,7 @@ func init() { } func ircHandleStatsUptime(c *client) { - uptime := (time.Now().UnixNano() - started) / int64(time.Second) + uptime := time.Now().Sub(started) / time.Second days := uptime / 60 / 60 / 24 hours := (uptime % (60 * 60 * 24)) / 60 / 60 @@ -2811,10 +2804,13 @@ func processOneEvent() { } c := &client{ - transport: conn, - address: address, - hostname: host, - port: port, + transport: conn, + address: address, + hostname: host, + port: port, + capVersion: 301, + // TODO: Make this configurable and more fine-grained. + antiflood: newFloodDetector(10*time.Second, 20), } clients[c] = true go prepare(c) @@ -2875,6 +2871,7 @@ func main() { log.Fatalln(err) } + started = time.Now() go accept(listener) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) From 051bbedc2fd0c5ccc2144ec262713736701496f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 30 Jul 2018 17:39:32 +0200 Subject: [PATCH 07/34] hid: port IRC 3.2 message tag parsing, unused --- xS/main.go | 84 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/xS/main.go b/xS/main.go index 1c72fca..eec67d9 100644 --- a/xS/main.go +++ b/xS/main.go @@ -296,17 +296,72 @@ func ircFnmatch(pattern string, s string) bool { 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 + 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 + } // Everything as per RFC 2812 @@ -2627,23 +2682,16 @@ func (c *client) onRead(data []byte, readErr error) { break } + // XXX: And since it accepts LF, we miscalculate receivedBytes within. c.recvQ = c.recvQ[advance:] line := string(token) log.Printf("-> %s\n", line) - m := reMsg.FindStringSubmatch(line) - if m == nil { + if msg := ircParseMessage(line); msg == nil { log.Println("error: invalid line") - continue + } else { + ircProcessMessage(c, msg, line) } - - 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:]) - } - - // XXX: And since it accepts LF, we miscalculate receivedBytes within. - ircProcessMessage(c, &msg, line) } if readErr != nil { From 2f841d214ffce4c33dc430a3b7557c6cbb0546ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Tue, 31 Jul 2018 20:53:23 +0200 Subject: [PATCH 08/34] hid: port configuration and initialization All the basic elements should be there now, we just need to port PING timers and fix some remaining issues and we're basically done. --- xS/main.go | 557 +++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 439 insertions(+), 118 deletions(-) diff --git a/xS/main.go b/xS/main.go index eec67d9..2c34f08 100644 --- a/xS/main.go +++ b/xS/main.go @@ -22,9 +22,11 @@ import ( "crypto/sha256" "crypto/tls" "encoding/hex" + "errors" "flag" "fmt" "io" + "io/ioutil" "log" "net" "os" @@ -66,67 +68,6 @@ func splitString(s, delims string, ignoreEmpty bool) (result []string) { return } -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 - } - return "~" + username -} - -// Tries to expand the tilde in paths, leaving it as-is on error. -func expandTilde(path string) string { - if path[0] != '~' { - return path - } - - var n int - for n = 0; n < len(path); n++ { - if path[n] == '/' { - break - } - } - return findTildeHome(path[1:n]) + path[n:] -} - -func getXDGHomeDir(name, def string) string { - env := os.Getenv(name) - if env != "" && env[0] == '/' { - 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) -} - -// 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 -} - // // 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 @@ -157,47 +98,230 @@ func detectTLS(sysconn syscall.RawConn) (isTLS bool) { return isTLS } -// --- Configuration ----------------------------------------------------------- +// --- File system ------------------------------------------------------------- -// 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"}, +// 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) } -/* - -// 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 - } +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 } - defer file.Close() - // TODO: We don't want to use JSON. - decoder := json.NewDecoder(file) - err = decoder.Decode(output) - if err != nil { - return fmt.Errorf("%s: %s", full, err) + file := filepath.Join(path, filename) + if _, err := os.Stat(file); err == nil { + return file } - return nil } - return errors.New("configuration file not found") + 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 + } + if debugMode { + log.Printf("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(filename string, data []byte) error { + if dir := filepath.Dir(filename); dir != "." { + if err := os.MkdirAll(dir, 0755); err != nil { + return err + } + } + return ioutil.WriteFile(filename, 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(filename string, data []byte) error { + temp := filename + ".new" + if err := writeFile(temp, data); err != nil { + return err + } + return os.Rename(temp, filename) +} + +// --- Simple configuration ---------------------------------------------------- + +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" + filename := resolveFilename(basename, resolveRelativeConfigFilename) + if filename == "" { + return &os.PathError{ + Op: "cannot find", + Path: basename, + Err: os.ErrNotExist, + } + } + + f, err := os.Open(filename) + 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", filename, 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, + ``, + ``, + } + + filename, err := simpleConfigWriteDefault( + pathHint, strings.Join(prologLines, "\n"), table) + if err != nil { + log.Fatalln(err) + } + + log.Printf("configuration written to `%s'\n", filename) +} + +// --- Configuration ----------------------------------------------------------- + +var configTable = []simpleConfigItem{ + // TODO: Default to the result from os.Hostname (if successful). + {"server_name", "", "Server name"}, + {"server_info", "My server", "Brief server description"}, + {"motd", "", "MOTD filename"}, + {"catalog", "", "Localisation catalog"}, + + {"bind", ":6667", "Bind addresses of the IRC server"}, + {"tls_cert", "", "Server TLS certificate (PEM)"}, + {"tls_key", "", "Server TLS private key (PEM)"}, + + {"operators", "", "IRCop TLS certificate SHA-256 fingerprints"}, + + {"max_connections", "0", "Global connection limit"}, + {"ping_interval", "180", "Interval between PINGs (sec)"}, +} // --- Rate limiter ------------------------------------------------------------ @@ -372,9 +496,16 @@ const ( ircMaxMessageLength = 510 ) -const reClassSpecial = "\\[\\]\\\\`_^{|}" +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-]*$`) @@ -389,6 +520,19 @@ var ( 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) } @@ -603,7 +747,8 @@ type writeEvent struct { err error // write error } -// TODO: Port server_context. Maybe we want to keep it in a struct? +// TODO: Port server_context. Maybe we want to keep it in a struct? A better +// question might be: can we run multiple instances of it? // XXX: Beware that maps with identifier keys need to be indexed correctly. // We might want to enforce accessor functions for users and channels. var ( @@ -614,6 +759,7 @@ var ( whowas map[string]*whowasInfo // WHOWAS registry + config simpleConfig // server configuration serverName string // our server name pingInterval uint // ping interval in seconds maxConnections int // max connections allowed or 0 @@ -631,7 +777,7 @@ var ( tlsConf *tls.Config clients = make(map[*client]bool) - listener net.Listener + listeners []net.Listener quitting bool quitTimer <-chan time.Time ) @@ -652,8 +798,10 @@ func forceQuit(reason string) { // Initiate a clean shutdown of the whole daemon. func initiateQuit() { log.Println("shutting down") - if err := listener.Close(); err != nil { - log.Println(err) + for _, ln := range listeners { + if err := ln.Close(); err != nil { + log.Println(err) + } } for c := range clients { c.closeLink("Shutting down") @@ -2751,12 +2899,15 @@ func (c *client) onWrite(written int, writeErr error) { func accept(ln net.Listener) { for { + // Error handling here may be tricky, see go #6163, #24808. 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 + if op, ok := err.(net.Error); !ok || !op.Temporary() { + log.Fatalln(err) + } else { + log.Println(err) + } } else { + // TCP_NODELAY is set by default on TCPConns. conns <- conn } } @@ -2798,7 +2949,7 @@ func prepare(client *client) { // This is just for the TLS detection and doesn't need to be fatal. log.Println(err) } else { - isTLS = detectTLS(sysconn) + isTLS = tlsConf != nil && detectTLS(sysconn) } // FIXME: When the client sends no data, we still initialize its conn. @@ -2827,7 +2978,7 @@ func write(client *client, data []byte) { writes <- writeEvent{client, n, err} } -// --- Main -------------------------------------------------------------------- +// --- Event loop -------------------------------------------------------------- func processOneEvent() { select { @@ -2842,6 +2993,12 @@ func processOneEvent() { forceQuit("timeout") case conn := <-conns: + if len(clients) >= maxConnections { + log.Println("connection limit reached, refusing connection") + conn.Close() + break + } + log.Println("accepted client connection") // In effect, we require TCP/UDP, as they have port numbers. @@ -2889,24 +3046,34 @@ func processOneEvent() { } } -func main() { - flag.BoolVar(&debugMode, "debug", false, "debug mode") - version := flag.Bool("version", false, "show version and exit") - flag.Parse() +// --- Application setup ------------------------------------------------------- - if *version { - fmt.Printf("%s %s\n", projectName, projectVersion) - return +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") } - // TODO: Configuration--create an INI parser, probably. - if len(flag.Args()) != 3 { - log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0]) + pathCert := resolveFilename(configCert, resolveRelativeConfigFilename) + if pathCert == "" { + return fmt.Errorf("cannot find file: %s", configCert) } - cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0)) + pathKey := resolveFilename(configKey, resolveRelativeConfigFilename) + if pathKey == "" { + return fmt.Errorf("cannot find file: %s", configKey) + } + + cert, err := tls.LoadX509KeyPair(pathCert, pathKey) if err != nil { - log.Fatalln(err) + return err } tlsConf = &tls.Config{ @@ -2914,15 +3081,169 @@ func main() { ClientAuth: tls.RequestClientCert, SessionTicketsDisabled: true, } - listener, err = net.Listen("tcp", flag.Arg(2)) + return nil +} + +func ircInitializeCatalog() error { + // TODO: Not going to use catgets but a simple text file with basic + // checking whether the index is used by this daemon at all should do. + return nil +} + +func ircInitializeMOTD() error { + configMOTD := config["motd"] + if configMOTD == "" { + return nil + } + + pathMOTD := resolveFilename(configMOTD, resolveRelativeConfigFilename) + if pathMOTD == "" { + return fmt.Errorf("cannot find file: %s", configMOTD) + } + + f, err := os.Open(pathMOTD) if err != nil { - log.Fatalln(err) + 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 configError struct { + name string // configuration key + err error // description of the issue +} + +func (e *configError) Error() string { + return fmt.Sprintf("invalid configuration value for `%s': %s", + e.name, e.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 { + // TODO: I think we could further shorten this with lambdas, doing away + // with the custom error type completely and at the same time getting rid of + // the key stuttering. + if u, err := strconv.ParseUint( + config["ping_interval"], 10, 32); err != nil { + return &configError{"ping_interval", err} + } else if u < 1 { + return &configError{"ping_interval", + errors.New("the value is out of range")} + } else { + pingInterval = uint(u) + } + + if i, err := strconv.ParseInt( + config["max_connections"], 10, 32); err != nil { + return &configError{"max_connections", err} + } else if i < 0 { + return &configError{"max_connections", + errors.New("the value is out of range")} + } else { + maxConnections = int(i) + } + + operators = make(map[string]bool) + for _, fp := range splitString(config["operators"], ",", true) { + if !ircIsValidFingerprint(fp) { + return &configError{"operators", + errors.New("invalid fingerprint valeu")} + } + operators[strings.ToLower(fp)] = true + } + return nil +} + +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) + } + 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 debug mode") + version := flag.Bool("version", false, "show version and exit") + writeDefaultCfg := flag.Bool("writedefaultcfg", false, + "write a default configuration file and exit") + + flag.Parse() + + if *version { + fmt.Printf("%s %s\n", projectName, projectVersion) + return + } + if *writeDefaultCfg { + callSimpleConfigWriteDefault("", configTable) + return + } + if flag.NArg() > 0 { + flag.Usage() + os.Exit(1) + } + + config = make(simpleConfig) + if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) { + log.Println("error loading configuration", err) + os.Exit(1) } started = time.Now() - go accept(listener) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + for _, fn := range []func() error{ + ircInitializeTLS, + ircInitializeServerName, + ircInitializeMOTD, + ircInitializeCatalog, + ircParseConfig, + ircSetupListenFDs, + } { + if err := fn(); err != nil { + log.Fatalln(err) + } + } + for !quitting || len(clients) > 0 { processOneEvent() } From e77495f3161259008dbc37b76facaf37e627a886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Tue, 31 Jul 2018 21:13:30 +0200 Subject: [PATCH 09/34] hid: bringup of what we have this far --- xS/main.go | 60 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/xS/main.go b/xS/main.go index 2c34f08..5e99a85 100644 --- a/xS/main.go +++ b/xS/main.go @@ -421,7 +421,7 @@ func ircFnmatch(pattern string, s string) bool { } var reMsg = regexp.MustCompile( - `^(?:@[^ ]* +)(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`) + `^(@[^ ]* +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`) var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`) type message struct { @@ -514,7 +514,7 @@ var ( // behaviour seems to be unstated in the documentation. reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`) - reChannelName = regexp.MustCompile(`^[^\0\7\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}$`) @@ -754,17 +754,16 @@ type writeEvent struct { var ( started time.Time // when has the server been started - users map[string]*client // maps nicknames to clients - channels map[string]*channel // maps channel names to data + 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 - whowas map[string]*whowasInfo // WHOWAS registry - - config simpleConfig // server configuration - 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 + config simpleConfig // server configuration + 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 = make(map[string]bool) // TLS cert. fingerprints for IRCops ) var ( @@ -814,8 +813,9 @@ func initiateQuit() { func ircChannelCreate(name string) *channel { ch := &channel{ name: name, - created: time.Now(), userLimit: -1, + created: time.Now(), + userModes: make(map[*client]uint), } channels[ircToCanon(name)] = ch return ch @@ -1046,10 +1046,15 @@ func (c *client) sendReplyVector(id int, items []string, args ...interface{}) { // 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 && @@ -1120,10 +1125,10 @@ func isThisMe(target string) bool { } func (c *client) sendISUPPORT() { - // Only # channels, +e supported, +I supported, unlimited arguments to MODE - c.sendReply(RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"+ + // 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) + " NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName)) } func (c *client) tryFinishRegistration() { @@ -2170,8 +2175,7 @@ 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, "TODO server_info from configuration") + c.sendReply(RPL_WHOISSERVER, nick, serverName, config["server_info"]) if 0 != target.mode&ircUserModeOperator { c.sendReply(RPL_WHOISOPERATOR, nick) } @@ -2257,7 +2261,7 @@ func ircHandleWHOWAS(msg *message, c *client) { c.sendReply(RPL_WHOWASUSER, nick, info.username, info.hostname, info.realname) c.sendReply(RPL_WHOISSERVER, nick, - serverName, "TODO server_info from configuration") + serverName, config["server_info"]) } c.sendReply(RPL_ENDOFWHOWAS, nick) } @@ -2689,7 +2693,7 @@ func ircHandleLINKS(msg *message, c *client) { if ircFnmatch(mask, serverName) { c.sendReply(RPL_LINKS, mask, serverName, - 0 /* hop count */, "TODO server_info from configuration") + 0 /* hop count */, config["server_info"]) } c.sendReply(RPL_ENDOFLINKS, mask) } @@ -2799,11 +2803,16 @@ func ircProcessMessage(c *client, msg *message, raw string) { // Handle the results from initializing the client's connection. func (c *client) onPrepared(host string, isTLS bool) { - if isTLS { + if !isTLS { + c.conn = c.transport.(connCloseWrite) + } else if tlsConf != nil { c.tls = tls.Server(c.transport, tlsConf) c.conn = c.tls } else { - c.conn = c.transport.(connCloseWrite) + log.Printf("could not initialize TLS for %s: TLS support disabled\n", + c.address) + c.kill("TLS support disabled") + return } c.hostname = host @@ -2949,7 +2958,7 @@ func prepare(client *client) { // This is just for the TLS detection and doesn't need to be fatal. log.Println(err) } else { - isTLS = tlsConf != nil && detectTLS(sysconn) + isTLS = detectTLS(sysconn) } // FIXME: When the client sends no data, we still initialize its conn. @@ -2993,7 +3002,7 @@ func processOneEvent() { forceQuit("timeout") case conn := <-conns: - if len(clients) >= maxConnections { + if maxConnections > 0 && len(clients) >= maxConnections { log.Println("connection limit reached, refusing connection") conn.Close() break @@ -3014,6 +3023,8 @@ func processOneEvent() { 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), } @@ -3223,6 +3234,7 @@ func main() { } config = make(simpleConfig) + config.loadDefaults(configTable) if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) { log.Println("error loading configuration", err) os.Exit(1) From 3610f98d677e71557fb18e6f75876a3f48977dfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Tue, 31 Jul 2018 23:37:54 +0200 Subject: [PATCH 10/34] hid: another round of general code cleanups --- xS/main.go | 183 +++++++++++++++++++++++++++-------------------------- 1 file changed, 92 insertions(+), 91 deletions(-) diff --git a/xS/main.go b/xS/main.go index 5e99a85..bcf7bc6 100644 --- a/xS/main.go +++ b/xS/main.go @@ -307,7 +307,6 @@ func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) { // --- Configuration ----------------------------------------------------------- var configTable = []simpleConfigItem{ - // TODO: Default to the result from os.Hostname (if successful). {"server_name", "", "Server name"}, {"server_info", "My server", "Brief server description"}, {"motd", "", "MOTD filename"}, @@ -488,6 +487,8 @@ func ircParseMessage(line string) *message { } +// --- IRC token validation ---------------------------------------------------- + // Everything as per RFC 2812 const ( ircMaxNickname = 9 @@ -1028,21 +1029,20 @@ func (c *client) getTLSCertFingerprint() string { // --- IRC command handling ---------------------------------------------------- -// XXX: ap doesn't really need to be a slice. -func (c *client) makeReply(id int, ap []interface{}) string { +func (c *client) makeReply(id int, ap ...interface{}) string { s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar()) a := fmt.Sprintf(defaultReplies[id], ap...) return s + a } -// XXX: This way we cannot typecheck the arguments, so we must be careful. +// XXX: This way we cannot typecheck the arguments, so we should be careful. func (c *client) sendReply(id int, args ...interface{}) { - c.send(c.makeReply(id, args)) + 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) + 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). @@ -1114,8 +1114,7 @@ func (c *client) sendLUSERS() { c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */) } -// TODO: Rename back to ircIsThisMe for consistency with kike. -func isThisMe(target string) bool { +func ircIsThisMe(target string) bool { // Target servers can also be matched by their users if ircFnmatch(target, serverName) { return true @@ -1275,7 +1274,7 @@ var ircCapHandlers = map[string]func(*client, *ircCapArgs){ "END": (*client).handleCAPEND, } -// XXX: Maybe these also deserve to be methods for client? They operato on +// XXX: Maybe these also deserve to be methods for client? They operate on // global state, though. func ircHandleCAP(msg *message, c *client) { @@ -1418,7 +1417,7 @@ func ircHandleUSERHOST(msg *message, c *client) { } func ircHandleLUSERS(msg *message, c *client) { - if len(msg.params) > 1 && !isThisMe(msg.params[1]) { + if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[1]) return } @@ -1426,7 +1425,7 @@ func ircHandleLUSERS(msg *message, c *client) { } func ircHandleMOTD(msg *message, c *client) { - if len(msg.params) > 0 && !isThisMe(msg.params[0]) { + if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[1]) return } @@ -1435,7 +1434,7 @@ func ircHandleMOTD(msg *message, c *client) { 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]) { + 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) @@ -1465,7 +1464,7 @@ func ircHandleQUIT(msg *message, c *client) { } func ircHandleTIME(msg *message, c *client) { - if len(msg.params) > 0 && !isThisMe(msg.params[0]) { + if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) return } @@ -1475,7 +1474,7 @@ func ircHandleTIME(msg *message, c *client) { } func ircHandleVERSION(msg *message, c *client) { - if len(msg.params) > 0 && !isThisMe(msg.params[0]) { + if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) return } @@ -1962,7 +1961,7 @@ func ircHandleNOTICE(msg *message, c *client) { } func ircHandleLIST(msg *message, c *client) { - if len(msg.params) > 1 && !isThisMe(msg.params[1]) { + if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[1]) return } @@ -2013,8 +2012,7 @@ func ircMakeRPLNAMREPLYItem(c, target *client, modes uint) string { return result } -// TODO: Consider using *client instead of string as the map key. -func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[string]bool) { +func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[*client]bool) { kind := '=' if 0 != ch.modes&ircChanModeSecret { kind = '@' @@ -2030,17 +2028,17 @@ func ircSendRPLNAMREPLY(c *client, ch *channel, usedNicks map[string]bool) { continue } if usedNicks != nil { - usedNicks[ircToCanon(client.nickname)] = true + usedNicks[client] = true } nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, modes)) } c.sendReplyVector(RPL_NAMREPLY, nicks, kind, ch.name, "") } -func ircSendDisassociatedNames(c *client, usedNicks map[string]bool) { +func ircSendDisassociatedNames(c *client, usedNicks map[*client]bool) { var nicks []string - for canonNickname, client := range users { - if 0 == client.mode&ircUserModeInvisible && !usedNicks[canonNickname] { + for _, client := range users { + if 0 == client.mode&ircUserModeInvisible && !usedNicks[client] { nicks = append(nicks, ircMakeRPLNAMREPLYItem(c, client, 0)) } } @@ -2050,14 +2048,13 @@ func ircSendDisassociatedNames(c *client, usedNicks map[string]bool) { } func ircHandleNAMES(msg *message, c *client) { - if len(msg.params) > 1 && !isThisMe(msg.params[1]) { + 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[string]bool) - + usedNicks := make(map[*client]bool) for _, ch := range channels { if _, present := ch.userModes[c]; present || 0 == ch.modes&(ircChanModePrivate|ircChanModeSecret) { @@ -2211,7 +2208,7 @@ func ircHandleWHOIS(msg *message, c *client) { c.sendReply(ERR_NEEDMOREPARAMS, msg.command) return } - if len(msg.params) > 1 && !isThisMe(msg.params[0]) { + if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) return } @@ -2248,7 +2245,7 @@ func ircHandleWHOWAS(msg *message, c *client) { c.sendReply(ERR_NEEDMOREPARAMS, msg.command) return } - if len(msg.params) > 2 && !isThisMe(msg.params[2]) { + if len(msg.params) > 2 && !ircIsThisMe(msg.params[2]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[2]) return } @@ -2283,10 +2280,10 @@ func ircHandleTOPIC(msg *message, c *client) { return } - target := msg.params[0] - ch := channels[ircToCanon(target)] + channelName := msg.params[0] + ch := channels[ircToCanon(channelName)] if ch == nil { - c.sendReply(ERR_NOSUCHCHANNEL, target) + c.sendReply(ERR_NOSUCHCHANNEL, channelName) return } @@ -2297,13 +2294,13 @@ func ircHandleTOPIC(msg *message, c *client) { modes, present := ch.userModes[c] if !present { - c.sendReply(ERR_NOTONCHANNEL, target) + c.sendReply(ERR_NOTONCHANNEL, channelName) return } if 0 != ch.modes&ircChanModeProtectedTopic && 0 == modes&ircChanModeOperator { - c.sendReply(ERR_CHANOPRIVSNEEDED, target) + c.sendReply(ERR_CHANOPRIVSNEEDED, channelName) return } @@ -2312,28 +2309,28 @@ func ircHandleTOPIC(msg *message, c *client) { ch.topicTime = time.Now() message := fmt.Sprintf(":%s!%s@%s TOPIC %s :%s", - c.nickname, c.username, c.hostname, target, ch.topic) + c.nickname, c.username, c.hostname, channelName, ch.topic) ircChannelMulticast(ch, message, nil) } -func ircTryPart(c *client, target string, reason string) { +func ircTryPart(c *client, channelName string, reason string) { if reason == "" { reason = c.nickname } - ch := channels[ircToCanon(target)] + ch := channels[ircToCanon(channelName)] if ch == nil { - c.sendReply(ERR_NOSUCHCHANNEL, target) + c.sendReply(ERR_NOSUCHCHANNEL, channelName) return } if _, present := ch.userModes[c]; !present { - c.sendReply(ERR_NOTONCHANNEL, target) + c.sendReply(ERR_NOTONCHANNEL, channelName) return } message := fmt.Sprintf(":%s@%s@%s PART %s :%s", - c.nickname, c.username, c.hostname, target, reason) + c.nickname, c.username, c.hostname, channelName, reason) if 0 == ch.modes&ircChanModeQuiet { ircChannelMulticast(ch, message, nil) } else { @@ -2363,35 +2360,34 @@ func ircHandlePART(msg *message, c *client) { reason = msg.params[1] } - for _, target := range splitString(msg.params[0], ",", true) { - ircTryPart(c, target, reason) + for _, channelName := range splitString(msg.params[0], ",", true) { + ircTryPart(c, channelName, reason) } } -// TODO: Undo the rename from channelName to target, also in ircTryPart. -func ircTryKick(c *client, target, nick, reason string) { - ch := channels[ircToCanon(target)] +func ircTryKick(c *client, channelName, nick, reason string) { + ch := channels[ircToCanon(channelName)] if ch == nil { - c.sendReply(ERR_NOSUCHCHANNEL, target) + c.sendReply(ERR_NOSUCHCHANNEL, channelName) return } if modes, present := ch.userModes[c]; !present { - c.sendReply(ERR_NOTONCHANNEL, target) + c.sendReply(ERR_NOTONCHANNEL, channelName) return } else if 0 == modes&ircChanModeOperator { - c.sendReply(ERR_CHANOPRIVSNEEDED, target) + c.sendReply(ERR_CHANOPRIVSNEEDED, channelName) return } client := users[ircToCanon(nick)] if _, present := ch.userModes[client]; client == nil || !present { - c.sendReply(ERR_USERNOTINCHANNEL, nick, target) + c.sendReply(ERR_USERNOTINCHANNEL, nick, channelName) return } message := fmt.Sprintf(":%s@%s@%s KICK %s %s :%s", - c.nickname, c.username, c.hostname, target, nick, reason) + c.nickname, c.username, c.hostname, channelName, nick, reason) if 0 == ch.modes&ircChanModeQuiet { ircChannelMulticast(ch, message, nil) } else { @@ -2594,7 +2590,7 @@ func ircHandleISON(msg *message, c *client) { } func ircHandleADMIN(msg *message, c *client) { - if len(msg.params) > 0 && !isThisMe(msg.params[0]) { + if len(msg.params) > 0 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) return } @@ -2632,7 +2628,7 @@ func ircHandleStatsCommands(c *client) { } // We need to do it this way because of an initialization loop concerning -// ircHandlers. Workaround proposed by rsc in #1817. +// ircHandlers. Workaround proposed by rsc in go #1817. var ircHandleStatsCommandsIndirect func(c *client) func init() { @@ -2656,7 +2652,7 @@ func ircHandleSTATS(msg *message, c *client) { query = msg.params[0][0] } - if len(msg.params) > 1 && !isThisMe(msg.params[1]) { + if len(msg.params) > 1 && !ircIsThisMe(msg.params[1]) { c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) return } @@ -2677,7 +2673,7 @@ func ircHandleSTATS(msg *message, c *client) { } func ircHandleLINKS(msg *message, c *client) { - if len(msg.params) > 1 && !isThisMe(msg.params[0]) { + if len(msg.params) > 1 && !ircIsThisMe(msg.params[0]) { c.sendReply(ERR_NEEDMOREPARAMS, msg.command) return } @@ -2729,7 +2725,7 @@ func ircHandleDIE(msg *message, c *client) { // ----------------------------------------------------------------------------- -// TODO: Add an index for IRC_ERR_NOSUCHSERVER validation? +// 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{ @@ -2818,7 +2814,7 @@ func (c *client) onPrepared(host string, isTLS bool) { c.hostname = host c.address = net.JoinHostPort(host, c.port) - // TODO: If we've tried to send any data before now, we need to flushSendQ. + // If we tried to send any data before now, we would need to flushSendQ. go read(c) c.reading = true } @@ -2961,7 +2957,6 @@ func prepare(client *client) { isTLS = detectTLS(sysconn) } - // FIXME: When the client sends no data, we still initialize its conn. prepared <- preparedEvent{client, host, isTLS} } @@ -3126,52 +3121,58 @@ func ircInitializeMOTD() error { return scanner.Err() } -type configError struct { - name string // configuration key - err error // description of the issue +type configProcessor struct { + err error // any error that has occurred so far } -func (e *configError) Error() string { - return fmt.Sprintf("invalid configuration value for `%s': %s", - e.name, e.err) +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 { - // TODO: I think we could further shorten this with lambdas, doing away - // with the custom error type completely and at the same time getting rid of - // the key stuttering. - if u, err := strconv.ParseUint( - config["ping_interval"], 10, 32); err != nil { - return &configError{"ping_interval", err} - } else if u < 1 { - return &configError{"ping_interval", - errors.New("the value is out of range")} - } else { - pingInterval = uint(u) - } - - if i, err := strconv.ParseInt( - config["max_connections"], 10, 32); err != nil { - return &configError{"max_connections", err} - } else if i < 0 { - return &configError{"max_connections", - errors.New("the value is out of range")} - } else { - maxConnections = int(i) - } - - operators = make(map[string]bool) - for _, fp := range splitString(config["operators"], ",", true) { - if !ircIsValidFingerprint(fp) { - return &configError{"operators", - errors.New("invalid fingerprint valeu")} + 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 = uint(u) } - operators[strings.ToLower(fp)] = true - } - return nil + 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 { From cbdbfc3d642761741f148c6be83851d839a00a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Wed, 1 Aug 2018 17:49:27 +0200 Subject: [PATCH 11/34] hid: figured out how to port timeouts --- xS/main.go | 84 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/xS/main.go b/xS/main.go index bcf7bc6..c3b2875 100644 --- a/xS/main.go +++ b/xS/main.go @@ -761,7 +761,7 @@ var ( config simpleConfig // server configuration serverName string // our server name - pingInterval uint // ping interval in seconds + pingInterval time.Duration // ping interval maxConnections int // max connections allowed or 0 motd []string // MOTD (none if empty) operators = make(map[string]bool) // TLS cert. fingerprints for IRCops @@ -773,7 +773,7 @@ var ( prepared = make(chan preparedEvent) reads = make(chan readEvent) writes = make(chan writeEvent) - timeouts = make(chan *client) + timers = make(chan func()) tlsConf *tls.Config clients = make(map[*client]bool) @@ -962,12 +962,7 @@ func (c *client) kill(reason string) { _ = 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() - } - + c.cancelTimers() delete(clients, c) } @@ -994,9 +989,7 @@ func (c *client) closeLink(reason string) { c.closing = true c.unregister(reason) - c.killTimer = time.AfterFunc(3*time.Second, func() { - timeouts <- c - }) + c.setKillTimer() } func (c *client) inMaskList(masks []string) bool { @@ -1025,7 +1018,57 @@ func (c *client) getTLSCertFingerprint() string { // --- Timers ------------------------------------------------------------------ -// TODO +// 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 ---------------------------------------------------- @@ -1451,7 +1494,7 @@ func ircHandlePONG(msg *message, c *client) { } // Set a new timer to send another PING - // TODO + c.setPingTimer() } func ircHandleQUIT(msg *message, c *client) { @@ -2817,6 +2860,7 @@ func (c *client) onPrepared(host string, isTLS bool) { // 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. @@ -2996,6 +3040,9 @@ func processOneEvent() { case <-quitTimer: forceQuit("timeout") + case callback := <-timers: + callback() + case conn := <-conns: if maxConnections > 0 && len(clients) >= maxConnections { log.Println("connection limit reached, refusing connection") @@ -3026,6 +3073,9 @@ func processOneEvent() { clients[c] = true go prepare(c) + // The TLS autodetection in prepare needs to have a timeout. + c.setKillTimer() + case ev := <-prepared: log.Println("client is ready:", ev.host) if _, ok := clients[ev.client]; ok { @@ -3043,12 +3093,6 @@ func processOneEvent() { 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.kill("TODO") - } } } @@ -3147,7 +3191,7 @@ func ircParseConfig() error { } else if u < 1 { return "the value is out of range" } else { - pingInterval = uint(u) + pingInterval = time.Second * time.Duration(u) } return "" }) From fea801ac7a0bc81452d4e606fa72a2b6255598de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Wed, 1 Aug 2018 19:35:47 +0200 Subject: [PATCH 12/34] hid: ircSendToRoommates -> ircNotifyRoommates Should be clearer. --- xS/main.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/xS/main.go b/xS/main.go index c3b2875..c078440 100644 --- a/xS/main.go +++ b/xS/main.go @@ -828,8 +828,7 @@ func ircChannelDestroyIfEmpty(ch *channel) { } } -// TODO: Improve the name as it takes mode +q into account. -func ircSendToRoommates(c *client, message string) { +func ircNotifyRoommates(c *client, message string) { targets := make(map[*client]bool) for _, ch := range channels { _, present := ch.userModes[c] @@ -929,7 +928,7 @@ func (c *client) unregister(reason string) { return } - ircSendToRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s", + ircNotifyRoommates(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. @@ -1379,7 +1378,7 @@ func ircHandleNICK(msg *message, c *client) { message := fmt.Sprintf(":%s!%s@%s NICK :%s", c.nickname, c.username, c.hostname, nickname) - ircSendToRoommates(c, message) + ircNotifyRoommates(c, message) c.send(message) } From 7ee7dc5f9b67c97f02b7d90d9446290cb92b7d29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Thu, 2 Aug 2018 12:51:22 +0200 Subject: [PATCH 13/34] hid: port default formatting strings to fmt --- xS/hid-replies | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/xS/hid-replies b/xS/hid-replies index 24affae..c539520 100644 --- a/xS/hid-replies +++ b/xS/hid-replies @@ -3,8 +3,8 @@ 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" +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" @@ -29,10 +29,10 @@ 322 RPL_LIST "%s %d :%s" 323 RPL_LISTEND ":End of LIST" 324 RPL_CHANNELMODEIS "%s +%s" -329 RPL_CREATIONTIME "%s %lld" +329 RPL_CREATIONTIME "%s %d" 331 RPL_NOTOPIC "%s :No topic is set" 332 RPL_TOPIC "%s :%s" -333 RPL_TOPICWHOTIME "%s %s %lld" +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" From 9ee07873ea043ff52e977060751e4801eb047ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Thu, 2 Aug 2018 18:42:32 +0200 Subject: [PATCH 14/34] hid: fix nickname verification in the user MODE message --- xS/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index c078440..39453f9 100644 --- a/xS/main.go +++ b/xS/main.go @@ -1921,7 +1921,7 @@ func ircHandleMODE(msg *message, c *client) { ch := channels[ircToCanon(target)] if client != nil { - if ircEqual(target, c.nickname) { + if !ircEqual(target, c.nickname) { c.sendReply(ERR_USERSDONTMATCH) return } From ab66a607039d24a3aa5033530af137c16934e5de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Fri, 3 Aug 2018 10:55:22 +0200 Subject: [PATCH 15/34] hid: fix listener shutdown --- xS/main.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index 39453f9..5d498aa 100644 --- a/xS/main.go +++ b/xS/main.go @@ -931,7 +931,7 @@ func (c *client) unregister(reason string) { ircNotifyRoommates(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. + // The QUIT message will take care of state on clients. for _, ch := range channels { delete(ch.userModes, c) ircChannelDestroyIfEmpty(ch) @@ -2949,6 +2949,13 @@ func accept(ln net.Listener) { for { // Error handling here may be tricky, see go #6163, #24808. if conn, err := ln.Accept(); err != nil { + // See go #4373, they're being dicks. Another solution would be to + // pass a done channel to this function and close it before closing + // all the listeners, returning from here if it's readable. + if strings.Contains(err.Error(), + "use of closed network connection") { + return + } if op, ok := err.(net.Error); !ok || !op.Temporary() { log.Fatalln(err) } else { From 765b741a678fe274ee496a9b5007835cc1359cdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Fri, 3 Aug 2018 21:12:45 +0200 Subject: [PATCH 16/34] hid: cleanups --- xS/main.go | 53 +++++++++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/xS/main.go b/xS/main.go index 5d498aa..c8b007e 100644 --- a/xS/main.go +++ b/xS/main.go @@ -748,26 +748,29 @@ type writeEvent struct { err error // write error } -// TODO: Port server_context. Maybe we want to keep it in a struct? A better -// question might be: can we run multiple instances of it? -// XXX: Beware that maps with identifier keys need to be indexed correctly. -// We might want to enforce accessor functions for users and channels. +// TODO: Maybe we want to keep it in a struct? +// A better question might be: can we run multiple instances of it? var ( - started time.Time // when has the server been started + // 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 - config simpleConfig // server configuration - serverName string // our server name - pingInterval time.Duration // ping interval - maxConnections int // max connections allowed or 0 - motd []string // MOTD (none if empty) - operators = make(map[string]bool) // TLS cert. fingerprints for IRCops -) + // event loop + + quitting bool + quitTimer <-chan time.Time -var ( sigs = make(chan os.Signal, 1) conns = make(chan net.Conn) prepared = make(chan preparedEvent) @@ -775,11 +778,16 @@ var ( writes = make(chan writeEvent) timers = make(chan func()) - tlsConf *tls.Config - clients = make(map[*client]bool) - listeners []net.Listener - quitting bool - quitTimer <-chan time.Time + // 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. @@ -1077,7 +1085,8 @@ func (c *client) makeReply(id int, ap ...interface{}) string { return s + a } -// XXX: This way we cannot typecheck the arguments, so we should be careful. +// 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...)) } @@ -3152,12 +3161,12 @@ func ircInitializeMOTD() error { return nil } - pathMOTD := resolveFilename(configMOTD, resolveRelativeConfigFilename) - if pathMOTD == "" { + path := resolveFilename(configMOTD, resolveRelativeConfigFilename) + if path == "" { return fmt.Errorf("cannot find file: %s", configMOTD) } - f, err := os.Open(pathMOTD) + f, err := os.Open(path) if err != nil { return fmt.Errorf("failed reading the MOTD file: %s", err) } From ffad1f15a58a6795fa978b97e928f099f44e9778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Fri, 3 Aug 2018 21:12:55 +0200 Subject: [PATCH 17/34] hid: unify exit codes with the flag package --- xS/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index c8b007e..d2b44b7 100644 --- a/xS/main.go +++ b/xS/main.go @@ -3290,7 +3290,7 @@ func main() { } if flag.NArg() > 0 { flag.Usage() - os.Exit(1) + os.Exit(2) } config = make(simpleConfig) From fd1538251a7d3637a4613077c05991330a85b598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Fri, 3 Aug 2018 21:44:58 +0200 Subject: [PATCH 18/34] hid: add support for customized replies --- xS/main.go | 46 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/xS/main.go b/xS/main.go index d2b44b7..0026d55 100644 --- a/xS/main.go +++ b/xS/main.go @@ -1081,8 +1081,10 @@ func (c *client) setPingTimer() { func (c *client) makeReply(id int, ap ...interface{}) string { s := fmt.Sprintf(":%s %03d %s ", serverName, id, c.nicknameOrStar()) - a := fmt.Sprintf(defaultReplies[id], ap...) - return s + a + 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 @@ -3150,9 +3152,43 @@ func ircInitializeTLS() error { } func ircInitializeCatalog() error { - // TODO: Not going to use catgets but a simple text file with basic - // checking whether the index is used by this daemon at all should do. - return nil + 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 { From 3815795d59d22cde5f0c81c1926a21bb15b82a58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Sat, 4 Aug 2018 21:13:28 +0200 Subject: [PATCH 19/34] hid: fix SSL 2.0 autodetection --- xS/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index 0026d55..69978b6 100644 --- a/xS/main.go +++ b/xS/main.go @@ -87,7 +87,7 @@ func detectTLS(sysconn syscall.RawConn) (isTLS bool) { isTLS = buf[0]&0x80 != 0 && buf[2] == 1 fallthrough case n == 2: - isTLS = buf[0] == 22 && buf[1] == 3 + isTLS = isTLS || buf[0] == 22 && buf[1] == 3 case n == 1: isTLS = buf[0] == 22 case err == syscall.EAGAIN: From e9bcd0fa53b7213c13b9b223b66a6e7c55056973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 11:45:06 +0200 Subject: [PATCH 20/34] hid: add the first tests This has actually revealed a problem in the SSL 2.0 detection. --- xS/main_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 xS/main_test.go diff --git a/xS/main_test.go b/xS/main_test.go new file mode 100644 index 0000000..90a55f6 --- /dev/null +++ b/xS/main_test.go @@ -0,0 +1,139 @@ +// +// Copyright (c) 2015 - 2018, Přemysl Janouch
+// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +// + +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") + } + }) +} From 09d7a10b6997c805f87b3f6fae2a8de162b16e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 12:06:42 +0200 Subject: [PATCH 21/34] hid: rename connCloseWrite to connCloseWriter --- xS/main.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/xS/main.go b/xS/main.go index 69978b6..a7bd2e9 100644 --- a/xS/main.go +++ b/xS/main.go @@ -563,7 +563,7 @@ func ircIsValidFingerprint(fp string) bool { // --- Clients (equals users) -------------------------------------------------- -type connCloseWrite interface { +type connCloseWriter interface { net.Conn CloseWrite() error } @@ -587,15 +587,15 @@ const ( ) type client struct { - transport net.Conn // underlying connection - tls *tls.Conn // TLS, if detected - conn connCloseWrite // 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 + 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 @@ -2853,7 +2853,7 @@ func ircProcessMessage(c *client, msg *message, raw string) { // Handle the results from initializing the client's connection. func (c *client) onPrepared(host string, isTLS bool) { if !isTLS { - c.conn = c.transport.(connCloseWrite) + c.conn = c.transport.(connCloseWriter) } else if tlsConf != nil { c.tls = tls.Server(c.transport, tlsConf) c.conn = c.tls From 3fee7e8051dc65438e2e633f76c807d743f1a6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 12:09:18 +0200 Subject: [PATCH 22/34] hid: port IRC tests from liberty, fix tag parsing --- xS/main.go | 2 +- xS/main_test.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index a7bd2e9..c49b2f6 100644 --- a/xS/main.go +++ b/xS/main.go @@ -420,7 +420,7 @@ func ircFnmatch(pattern string, s string) bool { } var reMsg = regexp.MustCompile( - `^(@[^ ]* +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`) + `^(?:@([^ ]*) +)?(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`) var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`) type message struct { diff --git a/xS/main_test.go b/xS/main_test.go index 90a55f6..4d6ccb3 100644 --- a/xS/main_test.go +++ b/xS/main_test.go @@ -137,3 +137,32 @@ func TestDetectTLS(t *testing.T) { } }) } + +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. +} From 5c7ac9a92bd86f76b770d7d3fcc8265a346d00c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 12:31:31 +0200 Subject: [PATCH 23/34] hid: cleanups No functional changes. --- xS/main.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/xS/main.go b/xS/main.go index c49b2f6..826ea46 100644 --- a/xS/main.go +++ b/xS/main.go @@ -192,27 +192,30 @@ func resolveFilename(filename string, relativeCB func(string) string) string { // --- Simple file I/O --------------------------------------------------------- // Overwrites filename contents with data; creates directories as needed. -func writeFile(filename string, data []byte) error { - if dir := filepath.Dir(filename); dir != "." { +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(filename, data, 0644) + 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(filename string, data []byte) error { - temp := filename + ".new" +func writeFileSafe(path string, data []byte) error { + temp := path + ".new" if err := writeFile(temp, data); err != nil { return err } - return os.Rename(temp, filename) + 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 @@ -229,8 +232,8 @@ func (sc simpleConfig) loadDefaults(table []simpleConfigItem) { func (sc simpleConfig) updateFromFile() error { basename := projectName + ".conf" - filename := resolveFilename(basename, resolveRelativeConfigFilename) - if filename == "" { + path := resolveFilename(basename, resolveRelativeConfigFilename) + if path == "" { return &os.PathError{ Op: "cannot find", Path: basename, @@ -238,7 +241,7 @@ func (sc simpleConfig) updateFromFile() error { } } - f, err := os.Open(filename) + f, err := os.Open(path) if err != nil { return err } @@ -253,7 +256,7 @@ func (sc simpleConfig) updateFromFile() error { equals := strings.IndexByte(line, '=') if equals <= 0 { - return fmt.Errorf("%s:%d: malformed line", filename, lineNo) + return fmt.Errorf("%s:%d: malformed line", path, lineNo) } sc[strings.TrimRight(line[:equals], " \t")] = line[equals+1:] @@ -295,13 +298,13 @@ func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) { ``, } - filename, err := simpleConfigWriteDefault( + path, err := simpleConfigWriteDefault( pathHint, strings.Join(prologLines, "\n"), table) if err != nil { log.Fatalln(err) } - log.Printf("configuration written to `%s'\n", filename) + log.Printf("configuration written to `%s'\n", path) } // --- Configuration ----------------------------------------------------------- From e2c8fb6e33f536384985197675c2ef569b9a6fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 14:12:45 +0200 Subject: [PATCH 24/34] hid: port logging facilities Though the regular mode now has timestamps and a new mode for systemd has been added. --- xS/main.go | 99 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/xS/main.go b/xS/main.go index 826ea46..69df705 100644 --- a/xS/main.go +++ b/xS/main.go @@ -28,6 +28,7 @@ import ( "io" "io/ioutil" "log" + "log/syslog" "net" "os" "os/signal" @@ -48,6 +49,100 @@ const ( 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. @@ -3316,6 +3411,7 @@ func main() { 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() @@ -3327,6 +3423,9 @@ func main() { callSimpleConfigWriteDefault("", configTable) return } + if *systemd { + logMessage = logMessageSystemd + } if flag.NArg() > 0 { flag.Usage() os.Exit(2) From e2c34afbc6f1193d2538b7d6e766d42c873347b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 19:50:53 +0200 Subject: [PATCH 25/34] hid: move off of the log package We don't spam with useless messages without -debug any longer. --- xS/main.go | 66 ++++++++++++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 34 deletions(-) diff --git a/xS/main.go b/xS/main.go index 69df705..933d865 100644 --- a/xS/main.go +++ b/xS/main.go @@ -27,7 +27,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "log/syslog" "net" "os" @@ -259,9 +258,7 @@ func findTildeHome(username string) string { } else if v, ok := os.LookupEnv("HOME"); ok { return v } - if debugMode { - log.Printf("failed to expand the home directory for %s", username) - } + printDebug("failed to expand the home directory for %s", username) return "~" + username } @@ -396,10 +393,10 @@ func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) { path, err := simpleConfigWriteDefault( pathHint, strings.Join(prologLines, "\n"), table) if err != nil { - log.Fatalln(err) + exitFatal("%s", err) } - log.Printf("configuration written to `%s'\n", path) + printStatus("configuration written to `%s'", path) } // --- Configuration ----------------------------------------------------------- @@ -891,10 +888,10 @@ var ( // Forcefully tear down all connections. func forceQuit(reason string) { if !quitting { - log.Fatalln("forceQuit called without initiateQuit") + exitFatal("forceQuit called without initiateQuit") } - log.Printf("forced shutdown (%s)\n", reason) + printStatus("forced shutdown (%s)", reason) for c := range clients { // initiateQuit has already unregistered the client. c.kill("Shutting down") @@ -903,10 +900,10 @@ func forceQuit(reason string) { // Initiate a clean shutdown of the whole daemon. func initiateQuit() { - log.Println("shutting down") + printStatus("shutting down") for _, ln := range listeners { if err := ln.Close(); err != nil { - log.Println(err) + printError("%s", err) } } for c := range clients { @@ -1057,7 +1054,8 @@ func (c *client) kill(reason string) { c.unregister(reason) // TODO: Log the address; seems like we always have c.address. - log.Println("client destroyed") + // In fact, do it in most debug logs, could be a method of client. + printDebug("client destroyed") // Try to send a "close notify" alert if the TLS object is ready, // otherwise just tear down the transport. @@ -2956,7 +2954,7 @@ func (c *client) onPrepared(host string, isTLS bool) { c.tls = tls.Server(c.transport, tlsConf) c.conn = c.tls } else { - log.Printf("could not initialize TLS for %s: TLS support disabled\n", + printDebug("could not initialize TLS for %s: TLS support disabled", c.address) c.kill("TLS support disabled") return @@ -2990,10 +2988,10 @@ func (c *client) onRead(data []byte, readErr error) { // XXX: And since it accepts LF, we miscalculate receivedBytes within. c.recvQ = c.recvQ[advance:] line := string(token) - log.Printf("-> %s\n", line) + printDebug("-> %s", line) if msg := ircParseMessage(line); msg == nil { - log.Println("error: invalid line") + printDebug("error: invalid line") } else { ircProcessMessage(c, msg, line) } @@ -3003,18 +3001,18 @@ func (c *client) onRead(data []byte, readErr error) { c.reading = false if readErr != io.EOF { - log.Println(readErr) + printDebug("%s", readErr) c.kill(readErr.Error()) } else if c.closing { // Disregarding whether a clean shutdown has happened or not. - log.Println("client finished shutdown") - c.kill("TODO") + printDebug("client finished shutdown") + c.kill("") } else { - log.Println("client EOF") + printDebug("client EOF") c.closeLink("") } } else if len(c.recvQ) > 8192 { - log.Println("client recvQ overrun") + printDebug("client recvQ overrun") c.closeLink("recvQ overrun") // tls.Conn doesn't have the CloseRead method (and it needs to be able @@ -3039,7 +3037,7 @@ func (c *client) onWrite(written int, writeErr error) { c.writing = false if writeErr != nil { - log.Println(writeErr) + printDebug("%s", writeErr) c.kill(writeErr.Error()) } else if len(c.sendQ) > 0 { c.flushSendQ() @@ -3047,7 +3045,7 @@ func (c *client) onWrite(written int, writeErr error) { if c.reading { c.conn.CloseWrite() } else { - c.kill("TODO") + c.kill("") } } } @@ -3066,9 +3064,9 @@ func accept(ln net.Listener) { return } if op, ok := err.(net.Error); !ok || !op.Temporary() { - log.Fatalln(err) + exitFatal("%s", err) } else { - log.Println(err) + printError("%s", err) } } else { // TCP_NODELAY is set by default on TCPConns. @@ -3082,7 +3080,7 @@ func prepare(client *client) { host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) if err != nil { // In effect, we require TCP/UDP, as they have port numbers. - log.Fatalln(err) + exitFatal("%s", err) } // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not @@ -3091,7 +3089,7 @@ func prepare(client *client) { go func() { defer close(ch) if names, err := net.LookupAddr(host); err != nil { - log.Println(err) + printError("%s", err) } else { ch <- names[0] } @@ -3111,7 +3109,7 @@ func prepare(client *client) { 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) + printError("%s", err) } else { isTLS = detectTLS(sysconn) } @@ -3160,18 +3158,18 @@ func processOneEvent() { case conn := <-conns: if maxConnections > 0 && len(clients) >= maxConnections { - log.Println("connection limit reached, refusing connection") + printDebug("connection limit reached, refusing connection") conn.Close() break } - log.Println("accepted client connection") + printDebug("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) + exitFatal("%s", err) } c := &client{ @@ -3192,19 +3190,19 @@ func processOneEvent() { c.setKillTimer() case ev := <-prepared: - log.Println("client is ready:", ev.host) + printDebug("client is ready: %s", ev.host) if _, ok := clients[ev.client]; ok { ev.client.onPrepared(ev.host, ev.isTLS) } case ev := <-reads: - log.Println("received data from client") + printDebug("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") + printDebug("sent data to client") if _, ok := clients[ev.client]; ok { ev.client.onWrite(ev.written, ev.err) } @@ -3434,7 +3432,7 @@ func main() { config = make(simpleConfig) config.loadDefaults(configTable) if err := config.updateFromFile(); err != nil && !os.IsNotExist(err) { - log.Println("error loading configuration", err) + printError("error loading configuration: %s", err) os.Exit(1) } @@ -3450,7 +3448,7 @@ func main() { ircSetupListenFDs, } { if err := fn(); err != nil { - log.Fatalln(err) + exitFatal("%s", err) } } From c285f3a266f3c276fd3274647f1f324696eb0d19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Mon, 6 Aug 2018 20:31:22 +0200 Subject: [PATCH 26/34] hid: clean up/finalize logging --- xS/main.go | 53 ++++++++++++++++++++++++++--------------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/xS/main.go b/xS/main.go index 933d865..cf54ab0 100644 --- a/xS/main.go +++ b/xS/main.go @@ -952,6 +952,12 @@ func ircNotifyRoommates(c *client, message string) { // --- 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') @@ -998,6 +1004,8 @@ func (c *client) send(line string) { 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"...) @@ -1049,13 +1057,12 @@ func (c *client) unregister(reason string) { // Close the connection and forget about the client. func (c *client) kill(reason string) { if reason == "" { - reason = "Client exited" + c.unregister("Client exited") + } else { + c.unregister(reason) } - c.unregister(reason) - // TODO: Log the address; seems like we always have c.address. - // In fact, do it in most debug logs, could be a method of client. - printDebug("client destroyed") + 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. @@ -2948,14 +2955,14 @@ func ircProcessMessage(c *client, msg *message, raw string) { // 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 { - printDebug("could not initialize TLS for %s: TLS support disabled", - c.address) + c.printDebug("could not initialize TLS: disabled") c.kill("TLS support disabled") return } @@ -2988,10 +2995,10 @@ func (c *client) onRead(data []byte, readErr error) { // XXX: And since it accepts LF, we miscalculate receivedBytes within. c.recvQ = c.recvQ[advance:] line := string(token) - printDebug("-> %s", line) + c.printDebug("-> %s", line) if msg := ircParseMessage(line); msg == nil { - printDebug("error: invalid line") + c.printDebug("error: invalid line") } else { ircProcessMessage(c, msg, line) } @@ -3001,18 +3008,17 @@ func (c *client) onRead(data []byte, readErr error) { c.reading = false if readErr != io.EOF { - printDebug("%s", readErr) + c.printDebug("%s", readErr) c.kill(readErr.Error()) } else if c.closing { // Disregarding whether a clean shutdown has happened or not. - printDebug("client finished shutdown") + c.printDebug("client finished shutdown") c.kill("") } else { - printDebug("client EOF") + c.printDebug("client EOF") c.closeLink("") } } else if len(c.recvQ) > 8192 { - printDebug("client recvQ overrun") c.closeLink("recvQ overrun") // tls.Conn doesn't have the CloseRead method (and it needs to be able @@ -3037,7 +3043,7 @@ func (c *client) onWrite(written int, writeErr error) { c.writing = false if writeErr != nil { - printDebug("%s", writeErr) + c.printDebug("%s", writeErr) c.kill(writeErr.Error()) } else if len(c.sendQ) > 0 { c.flushSendQ() @@ -3076,12 +3082,7 @@ func accept(ln net.Listener) { } 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. - exitFatal("%s", err) - } + conn, host := client.transport, client.hostname // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not // bothering with pointless contexts. @@ -3163,12 +3164,10 @@ func processOneEvent() { break } - printDebug("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 { + // In effect, we require TCP/UDP, as they have port numbers. exitFatal("%s", err) } @@ -3183,26 +3182,25 @@ func processOneEvent() { // 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: - printDebug("client is ready: %s", ev.host) if _, ok := clients[ev.client]; ok { ev.client.onPrepared(ev.host, ev.isTLS) } case ev := <-reads: - printDebug("received data from client") if _, ok := clients[ev.client]; ok { ev.client.onRead(ev.data, ev.err) } case ev := <-writes: - printDebug("sent data to client") if _, ok := clients[ev.client]; ok { ev.client.onWrite(ev.written, ev.err) } @@ -3392,6 +3390,7 @@ func ircSetupListenFDs() error { 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") @@ -3405,7 +3404,7 @@ func ircSetupListenFDs() error { // --- Main -------------------------------------------------------------------- func main() { - flag.BoolVar(&debugMode, "debug", false, "run in debug mode") + 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") From a1994865a939c6e8b83fc724b94613dce9e8c27f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?=
Date: Wed, 27 Feb 2019 02:36:04 +0100 Subject: [PATCH 27/34] hid: mention Go 1.12 alternative to TLS autodetection --- xS/main.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/xS/main.go b/xS/main.go index cf54ab0..8fcb6c2 100644 --- a/xS/main.go +++ b/xS/main.go @@ -172,6 +172,10 @@ func splitString(s, delims string, ignoreEmpty bool) (result []string) { // 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 From 6421892ef37206d4585437d2555a911570e8612b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Sat, 1 Aug 2020 14:01:58 +0200 Subject: [PATCH 28/34] Name change --- xS/main.go | 2 +- xS/main_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/xS/main.go b/xS/main.go index 8fcb6c2..28d6fd6 100644 --- a/xS/main.go +++ b/xS/main.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2014 - 2018, Přemysl Janouch
+// Copyright (c) 2014 - 2018, Přemysl Eric Janouch
// // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. diff --git a/xS/main_test.go b/xS/main_test.go index 4d6ccb3..8241b4e 100644 --- a/xS/main_test.go +++ b/xS/main_test.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2015 - 2018, Přemysl Janouch
+// Copyright (c) 2015 - 2018, Přemysl Eric Janouch
// // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. From 4073b7329fa80e6ce0fd3cd110a70fd450387d2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Fri, 6 Aug 2021 17:29:58 +0200 Subject: [PATCH 29/34] hid: reflect the original project's new name Better keep all schizophreny in my own head, rather than all projects. --- xS/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/xS/main.go b/xS/main.go index 28d6fd6..e9a0095 100644 --- a/xS/main.go +++ b/xS/main.go @@ -13,7 +13,7 @@ // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // -// hid is a straight-forward port of kike IRCd from C. +// hid is a straight-forward port of xD IRCd from C. package main import ( From f26e6361f333c7747d39c4d8f494fa44d5e2fd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Sat, 5 Feb 2022 00:30:08 +0100 Subject: [PATCH 30/34] hid: implement WALLOPS --- xS/main.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/xS/main.go b/xS/main.go index e9a0095..fd8d8fb 100644 --- a/xS/main.go +++ b/xS/main.go @@ -2854,6 +2854,25 @@ func ircHandleLINKS(msg *message, c *client) { 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) @@ -2908,6 +2927,7 @@ var ircHandlers = map[string]*ircCommand{ "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}, From a51c247d69b2b765ad59f9a79093e25f74ebcc83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Tue, 15 Mar 2022 18:58:27 +0100 Subject: [PATCH 31/34] hid: add WebIRC support Such clients can only be identified through STATS L. It's a bit weird to abuse the "port" field this way, but right now, it serves its purpose. --- xS/main.go | 50 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/xS/main.go b/xS/main.go index fd8d8fb..43a04f2 100644 --- a/xS/main.go +++ b/xS/main.go @@ -1,5 +1,5 @@ // -// Copyright (c) 2014 - 2018, Přemysl Eric Janouch
+// Copyright (c) 2014 - 2022, Přemysl Eric Janouch
// // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. @@ -414,6 +414,7 @@ var configTable = []simpleConfigItem{ {"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"}, @@ -1437,6 +1438,44 @@ var ircCapHandlers = map[string]func(*client, *ircCapArgs){ // 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) @@ -2908,10 +2947,11 @@ func ircHandleDIE(msg *message, c *client) { // TODO: Add a minimal parameter count? // TODO: Add a field for oper-only commands? Use flags? var ircHandlers = map[string]*ircCommand{ - "CAP": {false, ircHandleCAP, 0, 0}, - "PASS": {false, ircHandlePASS, 0, 0}, - "NICK": {false, ircHandleNICK, 0, 0}, - "USER": {false, ircHandleUSER, 0, 0}, + "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}, From 9c31fb69dfe8e2daa17990d317e860df7d163dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Wed, 16 Mar 2022 12:55:07 +0100 Subject: [PATCH 32/34] hid: make note of a deprecation --- xS/main.go | 1 + 1 file changed, 1 insertion(+) diff --git a/xS/main.go b/xS/main.go index 43a04f2..cf57b5e 100644 --- a/xS/main.go +++ b/xS/main.go @@ -3133,6 +3133,7 @@ func accept(ln net.Listener) { "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 { From 12382335564ff91d0647408e3a105ba0b30b03e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Tue, 2 Aug 2022 22:10:05 +0200 Subject: [PATCH 33/34] hid: bump the FD limit --- xS/main.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/xS/main.go b/xS/main.go index cf57b5e..40f47bc 100644 --- a/xS/main.go +++ b/xS/main.go @@ -3493,6 +3493,14 @@ func main() { 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) { From 8344b09c4f9989370691c71b1145b09348e0a6d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?=
Date: Mon, 26 Sep 2022 12:23:58 +0200 Subject: [PATCH 34/34] hid: rename to xS before merge into xK --- xS/hid-gen-replies.sh | 16 ---------------- xS/main.go | 19 +++++++++---------- xS/xS-gen-replies.awk | 16 ++++++++++++++++ xS/{hid-replies => xS-replies} | 0 4 files changed, 25 insertions(+), 26 deletions(-) delete mode 100755 xS/hid-gen-replies.sh create mode 100755 xS/xS-gen-replies.awk rename xS/{hid-replies => xS-replies} (100%) diff --git a/xS/hid-gen-replies.sh b/xS/hid-gen-replies.sh deleted file mode 100755 index c32b000..0000000 --- a/xS/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/xS/main.go b/xS/main.go index 40f47bc..21851f1 100644 --- a/xS/main.go +++ b/xS/main.go @@ -13,7 +13,7 @@ // CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // -// hid is a straight-forward port of xD IRCd from C. +// xS is a straight-forward port of xD IRCd from C. package main import ( @@ -43,7 +43,7 @@ import ( var debugMode = false const ( - projectName = "hid" + projectName = "xS" // TODO: Consider using the same version number for all subprojects. projectVersion = "0" ) @@ -162,15 +162,14 @@ func splitString(s, delims string, ignoreEmpty bool) (result []string) { 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) +// 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, @@ -383,7 +382,7 @@ func simpleConfigWriteDefault(pathHint string, prolog string, return writeConfigurationFile(pathHint, data) } -/// Convenience wrapper suitable for most simple applications. +// Convenience wrapper suitable for most simple applications. func callSimpleConfigWriteDefault(pathHint string, table []simpleConfigItem) { prologLines := []string{ `# ` + projectName + ` ` + projectVersion + ` configuration file`, @@ -461,7 +460,7 @@ func (fd *floodDetector) check() bool { // --- IRC protocol ------------------------------------------------------------ -//go:generate sh -c "./hid-gen-replies.sh > hid-replies.go < hid-replies" +//go:generate sh -c "LC_ALL=C awk -f xS-gen-replies.awk > xS-replies.go < xS-replies" func ircToLower(c byte) byte { switch c { @@ -1201,7 +1200,7 @@ 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. +// 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...) diff --git a/xS/xS-gen-replies.awk b/xS/xS-gen-replies.awk new file mode 100755 index 0000000..fce7b50 --- /dev/null +++ b/xS/xS-gen-replies.awk @@ -0,0 +1,16 @@ +#!/usr/bin/awk -f +/^[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/xS/hid-replies b/xS/xS-replies similarity index 100% rename from xS/hid-replies rename to xS/xS-replies