summaryrefslogtreecommitdiff
path: root/xS/main.go
diff options
context:
space:
mode:
Diffstat (limited to 'xS/main.go')
-rw-r--r--xS/main.go1526
1 files changed, 1526 insertions, 0 deletions
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 <p@janouch.name>
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+//
+
+// hid is a straight-forward port of kike IRCd from C.
+package main
+
+/*
+
+// ANSI terminal formatting, would be better if we had isatty() available
+func tf(text string, ansi string) string {
+ return "\x1b[0;" + ansi + "m" + text + "\x1b[0m"
+}
+
+func logErrorf(format string, args ...interface{}) {
+ fmt.Fprintf(os.Stderr, tf("error: "+format+"\n", "1;31"), args...)
+}
+
+func logFatalf(format string, args ...interface{}) {
+ fmt.Fprintf(os.Stderr, tf("fatal: "+format+"\n", "1;31"), args...)
+ os.Exit(1)
+}
+
+func logFatal(object interface{}) {
+ logFatalf("%s", object)
+}
+
+func getHome() (home string) {
+ if u, _ := user.Current(); u != nil {
+ home = u.HomeDir
+ } else {
+ home = os.Getenv("HOME")
+ }
+ return
+}
+
+// Only handling the simple case as that's what one mostly wants.
+// TODO(p): Handle the generic case as well.
+func expandTilde(path string) string {
+ if strings.HasPrefix(path, "~/") {
+ return getHome() + path[1:]
+ }
+ return path
+}
+
+func getXdgHomeDir(name, def string) string {
+ env := os.Getenv(name)
+ if env != "" && env[0] == '/' {
+ return env
+ }
+ return filepath.Join(getHome(), def)
+}
+
+// Retrieve all XDG base directories for configuration files
+func getXdgConfigDirs() (result []string) {
+ home := getXdgHomeDir("XDG_CONFIG_HOME", ".config")
+ if home != "" {
+ result = append(result, home)
+ }
+ dirs := os.Getenv("XDG_CONFIG_DIRS")
+ if dirs == "" {
+ dirs = "/etc/xdg"
+ }
+ for _, path := range strings.Split(dirs, ":") {
+ if path != "" {
+ result = append(result, path)
+ }
+ }
+ return
+}
+
+// Read a configuration file with the given basename w/o extension
+func readConfigFile(name string, output interface{}) error {
+ var suffix = filepath.Join(projectName, name+".json")
+ for _, path := range getXdgConfigDirs() {
+ full := filepath.Join(path, suffix)
+ file, err := os.Open(full)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ return err
+ }
+ continue
+ }
+ defer file.Close()
+
+ decoder := json.NewDecoder(file)
+ err = decoder.Decode(output)
+ if err != nil {
+ return fmt.Errorf("%s: %s", full, err)
+ }
+ return nil
+ }
+ return errors.New("configuration file not found")
+}
+
+*/
+
+import (
+ "bufio"
+ "crypto/sha256"
+ "crypto/tls"
+ "encoding/hex"
+ "flag"
+ "fmt"
+ "io"
+ "log"
+ "net"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "regexp"
+ "strconv"
+ "strings"
+ "syscall"
+ "time"
+)
+
+var debugMode = false
+
+// --- Utilities ---------------------------------------------------------------
+
+//
+// Trivial SSL/TLS autodetection. The first block of data returned by Recvfrom
+// must be at least three octets long for this to work reliably, but that should
+// not pose a problem in practice. We might try waiting for them.
+//
+// SSL2: 1xxx xxxx | xxxx xxxx | <1>
+// (message length) (client hello)
+// SSL3/TLS: <22> | <3> | xxxx xxxx
+// (handshake)| (protocol version)
+//
+func detectTLS(sysconn syscall.RawConn) (isTLS bool) {
+ sysconn.Read(func(fd uintptr) (done bool) {
+ var buf [3]byte
+ n, _, err := syscall.Recvfrom(int(fd), buf[:], syscall.MSG_PEEK)
+ switch {
+ case n == 3:
+ isTLS = buf[0]&0x80 != 0 && buf[2] == 1
+ fallthrough
+ case n == 2:
+ isTLS = buf[0] == 22 && buf[1] == 3
+ case n == 1:
+ isTLS = buf[0] == 22
+ case err == syscall.EAGAIN:
+ return false
+ }
+ return true
+ })
+ return isTLS
+}
+
+// --- Configuration -----------------------------------------------------------
+
+// XXX: Do we really want to support default nil values?
+var config = []struct {
+ key string // INI key
+ def []rune // default value, may be nil
+ description string // documentation
+}{
+ // XXX: I'm not sure if Go will cooperate here.
+ {"pid_file", nil, "Path or name of the PID file"},
+ {"bind", []rune(":6667"), "Address of the IRC server"},
+}
+
+// --- Rate limiter ------------------------------------------------------------
+
+type floodDetector struct {
+ interval uint // interval for the limit
+ limit uint // maximum number of events allowed
+ timestamps []int64 // timestamps of last events
+ pos uint // index of the oldest event
+}
+
+func newFloodDetector(interval, limit uint) *floodDetector {
+ return &floodDetector{
+ interval: interval,
+ limit: limit,
+ timestamps: make([]int64, limit+1),
+ pos: 0,
+ }
+}
+
+func (fd *floodDetector) check() bool {
+ now := time.Now().Unix()
+ fd.timestamps[fd.pos] = now
+
+ fd.pos++
+ if fd.pos > fd.limit {
+ fd.pos = 0
+ }
+
+ var count uint
+ begin := now - int64(fd.interval)
+ for _, ts := range fd.timestamps {
+ if ts >= begin {
+ count++
+ }
+ }
+ return count <= fd.limit
+}
+
+// --- IRC protocol ------------------------------------------------------------
+
+//go:generate sh -c "./hid-gen-replies.sh > hid-replies.go < hid-replies"
+
+func ircToLower(c byte) byte {
+ switch c {
+ case '[':
+ return '{'
+ case ']':
+ return '}'
+ case '\\':
+ return '|'
+ case '~':
+ return '^'
+ }
+ if c >= 'A' && c <= 'Z' {
+ return c + ('a' - 'A')
+ }
+ return c
+}
+
+// TODO: To support ALL CAPS initialization of maps, perhaps we should use
+// ircToUpper instead.
+// FIXME: This doesn't follow the meaning of strxfrm and perhaps should be
+// renamed to ircNormalize.
+func ircStrxfrm(ident string) string {
+ var canon []byte
+ for _, c := range []byte(ident) {
+ canon = append(canon, ircToLower(c))
+ }
+ return string(canon)
+}
+
+func ircFnmatch(pattern string, s string) bool {
+ pattern, s = ircStrxfrm(pattern), ircStrxfrm(s)
+ // FIXME: This should not support [] ranges and handle / specially.
+ // We could translate the pattern to a regular expression.
+ matched, _ := filepath.Match(pattern, s)
+ return matched
+}
+
+// TODO: We will need to add support for IRCv3 tags.
+var reMsg = regexp.MustCompile(
+ `^(?::([^! ]*)(?:!([^@]*)@([^ ]*))? +)?([^ ]+)(.*)?$`)
+var reArgs = regexp.MustCompile(`:.*| [^: ][^ ]*`)
+
+type message struct {
+ nick string // optional nickname
+ user string // optional username
+ host string // optional hostname or IP address
+ command string // command name
+ params []string // arguments
+}
+
+// Everything as per RFC 2812
+const (
+ ircMaxNickname = 9
+ ircMaxHostname = 63
+ ircMaxChannelName = 50
+ ircMaxMessageLength = 510
+)
+
+// TODO: Port the IRC token validation part as needed.
+
+const reClassSpecial = "\\[\\]\\\\`_^{|}"
+
+var (
+ // Extending ASCII to the whole range of Unicode letters.
+ reNickname = regexp.MustCompile(
+ `^[\pL` + reClassSpecial + `][\pL` + reClassSpecial + `0-9-]*$`)
+
+ // Notably, this won't match invalid UTF-8 characters, although the
+ // behaviour seems to be unstated in the documentation.
+ reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)
+
+ reChannelName = regexp.MustCompile(`^[^\0\7\r\n ,:]+$`)
+)
+
+func ircIsValidNickname(nickname string) bool {
+ return len(nickname) <= ircMaxNickname && reNickname.MatchString(nickname)
+}
+
+func ircIsValidUsername(username string) bool {
+ // XXX: We really should put a limit on this
+ // despite the RFC not mentioning one.
+ return reUsername.MatchString(username)
+}
+
+func ircIsValidChannelName(name string) bool {
+ return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)
+}
+
+// --- Clients (equals users) --------------------------------------------------
+
+type connCloseWrite interface {
+ net.Conn
+ CloseWrite() error
+}
+
+const ircSupportedUserModes = "aiwros"
+
+const (
+ ircUserModeInvisible = 1 << iota
+ ircUserModeRxWallops
+ ircUserModeRestricted
+ ircUserModeOperator
+ ircUserModeRxServerNotices
+)
+
+const (
+ ircCapMultiPrefix = 1 << iota
+ ircCapInviteNotify
+ ircCapEchoMessage
+ ircCapUserhostInNames
+ ircCapServerTime
+)
+
+type client struct {
+ transport net.Conn // underlying connection
+ tls *tls.Conn // TLS, if detected
+ conn connCloseWrite // high-level connection
+ inQ []byte // unprocessed input
+ outQ []byte // unprocessed output
+ reading bool // whether a reading goroutine is running
+ writing bool // whether a writing goroutine is running
+ closing bool // whether we're closing the connection
+ killTimer *time.Timer // hard kill timeout
+
+ opened int64 // when the connection was opened
+ nSentMessages uint // number of sent messages total
+ sentBytes int // number of sent bytes total
+ nReceivedMessages uint // number of received messages total
+ receivedBytes int // number of received bytes total
+
+ hostname string // hostname or IP shown to the network
+ port string // port of the peer as a string
+ address string // full network address
+
+ pingTimer *time.Timer // we should send a PING
+ timeoutTimer *time.Timer // connection seems to be dead
+ registered bool // the user has registered
+ capNegotiating bool // negotiating capabilities
+ capsEnabled uint // enabled capabilities
+ capVersion uint // CAP protocol version
+
+ tlsCertFingerprint string // client certificate fingerprint
+
+ nickname string // IRC nickname (main identifier)
+ username string // IRC username
+ realname string // IRC realname (or e-mail)
+
+ mode uint // user's mode
+ awayMessage string // away message
+ lastActive int64 // last PRIVMSG, to get idle time
+ invites map[string]bool // channel invitations by operators
+ antiflood floodDetector // flood detector
+}
+
+// --- Channels ----------------------------------------------------------------
+
+const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"
+
+const (
+ ircChanModeInviteOnly = 1 << iota
+ ircChanModeModerated
+ ircChanModeNoOutsideMsgs
+ ircChanModeQuiet
+ ircChanModePrivate
+ ircChanModeSecret
+ ircChanModeProtectedTopic
+
+ ircChanModeOperator
+ ircChanModeVoice
+)
+
+type channel struct {
+ name string // channel name
+ modes uint // channel modes
+ key string // channel key
+ userLimit int // user limit or -1
+ created int64 // creation time
+
+ topic string // channel topic
+ topicWho string // who set the topic
+ topicTime int64 // when the topic was set
+
+ userModes map[*client]uint // modes for all channel users
+
+ banList []string // ban list
+ exceptionList []string // exceptions from bans
+ inviteList []string // exceptions from +I
+}
+
+func newChannel() *channel {
+ return &channel{userLimit: -1}
+}
+
+// TODO: Port struct channel methods.
+
+// --- IRC server context ------------------------------------------------------
+
+type whowasInfo struct {
+ nickname, username, realname, hostname string
+}
+
+func newWhowasInfo(c *client) *whowasInfo {
+ return &whowasInfo{
+ nickname: c.nickname,
+ username: c.username,
+ realname: c.realname,
+ hostname: c.hostname,
+ }
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+type ircCommand struct {
+ name string
+ requiresRegistration bool
+ handler func(*message, *client)
+
+ nReceived uint // number of commands received
+ bytesReceived uint // number of bytes received total
+}
+
+type preparedEvent struct {
+ client *client
+ host string // client's hostname or literal IP address
+ isTLS bool // the client seems to use TLS
+}
+
+type readEvent struct {
+ client *client
+ data []byte // new data from the client
+ err error // read error
+}
+
+type writeEvent struct {
+ client *client
+ written int // amount of bytes written
+ err error // write error
+}
+
+// TODO: Port server_context. Maybe we want to keep it in a struct?
+// XXX: Beware that maps with identifier keys need to be indexed correctly.
+// We might want to enforce accessor functions for users and channels.
+var (
+ started int64 // when has the server been started
+
+ users map[string]*client // maps nicknames to clients
+ channels map[string]*channel // maps channel names to data
+ handlers map[string]bool // TODO message handlers
+ capHandlers map[string]bool // TODO CAP message handlers
+
+ whowas map[string]*whowasInfo // WHOWAS registry
+
+ serverName string // our server name
+ pingInterval uint // ping interval in seconds
+ maxConnections int // max connections allowed or 0
+ motd []string // MOTD (none if empty)
+ operators map[string]bool // TLS certificate fingerprints for IRCops
+)
+
+var (
+ sigs = make(chan os.Signal, 1)
+ conns = make(chan net.Conn)
+ prepared = make(chan preparedEvent)
+ reads = make(chan readEvent)
+ writes = make(chan writeEvent)
+ timeouts = make(chan *client)
+
+ tlsConf *tls.Config
+ clients = make(map[*client]bool)
+ listener net.Listener
+ // TODO: quitting, quitTimer as they are named in kike?
+ inShutdown bool
+ shutdownTimer <-chan time.Time
+)
+
+// Forcefully tear down all connections.
+func forceShutdown(reason string) {
+ if !inShutdown {
+ log.Fatalln("forceShutdown called without initiateShutdown")
+ }
+
+ log.Printf("forced shutdown (%s)\n", reason)
+ for c := range clients {
+ c.destroy("TODO")
+ }
+}
+
+// Initiate a clean shutdown of the whole daemon.
+func initiateShutdown() {
+ log.Println("shutting down")
+ if err := listener.Close(); err != nil {
+ log.Println(err)
+ }
+ for c := range clients {
+ c.closeLink("TODO")
+ }
+
+ shutdownTimer = time.After(5 * time.Second)
+ inShutdown = true
+}
+
+// TODO: ircChannelCreate
+
+func ircChannelDestroyIfEmpty(ch *channel) {
+ // TODO
+}
+
+// TODO: ircSendToRoommates
+// Broadcast to all /other/ clients (telnet-friendly, also in accordance to
+// the plan of extending this to an IRCd).
+func broadcast(line string, except *client) {
+ for c := range clients {
+ if c != except {
+ c.send(line)
+ }
+ }
+}
+
+func ircSendToRoommates(c *client, message string) {
+ // TODO
+}
+
+// --- Clients (continued) -----------------------------------------------------
+
+// TODO: Perhaps we should append to *[]byte for performance.
+func clientModeToString(m uint, mode *string) {
+ if 0 != m&ircUserModeInvisible {
+ *mode += "i"
+ }
+ if 0 != m&ircUserModeRxWallops {
+ *mode += "w"
+ }
+ if 0 != m&ircUserModeRestricted {
+ *mode += "r"
+ }
+ if 0 != m&ircUserModeOperator {
+ *mode += "o"
+ }
+ if 0 != m&ircUserModeRxServerNotices {
+ *mode += "s"
+ }
+}
+
+func (c *client) getMode() string {
+ mode := ""
+ if c.awayMessage != "" {
+ mode += "a"
+ }
+ clientModeToString(c.mode, &mode)
+ return mode
+}
+
+func (c *client) send(line string) {
+ if c.conn == nil || c.closing {
+ return
+ }
+
+ // TODO: Rename inQ and outQ to recvQ and sendQ as they are usually named.
+ oldOutQ := len(c.outQ)
+
+ // So far there's only one message tag we use, so we can do it simple;
+ // note that a 1024-character limit applies to messages with tags on.
+ if 0 != c.capsEnabled&ircCapServerTime {
+ c.outQ = time.Now().UTC().
+ AppendFormat(c.outQ, "@time=2006-01-02T15:04:05.000Z ")
+ }
+
+ bytes := []byte(line)
+ if len(bytes) > ircMaxMessageLength {
+ bytes = bytes[:ircMaxMessageLength]
+ }
+
+ // TODO: Kill the connection above some "SendQ" threshold (careful!)
+ c.outQ = append(c.outQ, bytes...)
+ c.outQ = append(c.outQ, "\r\n"...)
+ c.flushOutQ()
+
+ // Technically we haven't sent it yet but that's a minor detail
+ c.nSentMessages++
+ c.sentBytes += len(c.outQ) - oldOutQ
+}
+
+func (c *client) sendf(format string, a ...interface{}) {
+ c.send(fmt.Sprintf(format, a...))
+}
+
+func (c *client) addToWhowas() {
+ // Only keeping one entry for each nickname.
+ // TODO: Make sure this list doesn't get too long, for example by
+ // putting them in a linked list ordered by time.
+ whowas[ircStrxfrm(c.nickname)] = newWhowasInfo(c)
+}
+
+func (c *client) unregister(reason string) {
+ if !c.registered {
+ return
+ }
+
+ ircSendToRoommates(c, fmt.Sprintf(":%s!%s@%s QUIT :%s",
+ c.nickname, c.username, c.hostname, reason))
+
+ // The eventual QUIT message will take care of state at clients.
+ for _, ch := range channels {
+ delete(ch.userModes, c)
+ ircChannelDestroyIfEmpty(ch)
+ }
+
+ c.addToWhowas()
+ delete(users, ircStrxfrm(c.nickname))
+ c.nickname = ""
+ c.registered = false
+}
+
+// TODO: Rename to kill.
+// Close the connection and forget about the client.
+func (c *client) destroy(reason string) {
+ if reason == "" {
+ reason = "Client exited"
+ }
+ c.unregister(reason)
+
+ // TODO: Log the address; seems like we always have c.address.
+ log.Println("client destroyed")
+
+ // Try to send a "close notify" alert if the TLS object is ready,
+ // otherwise just tear down the transport.
+ if c.conn != nil {
+ _ = c.conn.Close()
+ } else {
+ _ = c.transport.Close()
+ }
+
+ // Clean up the goroutine, although a spurious event may still be sent.
+ // TODO: Other timers if needed.
+ if c.killTimer != nil {
+ c.killTimer.Stop()
+ }
+
+ delete(clients, c)
+}
+
+// Tear down the client connection, trying to do so in a graceful manner.
+func (c *client) closeLink(reason string) {
+ // Let's just cut the connection, the client can try again later.
+ // We also want to avoid accidentally writing to the socket before
+ // address resolution has finished.
+ if c.conn == nil {
+ c.destroy(reason)
+ return
+ }
+ if c.closing {
+ return
+ }
+
+ nickname := c.nickname
+ if nickname == "" {
+ nickname = "*"
+ }
+
+ // We push an "ERROR" message to the write buffer and let the writer send
+ // it, with some arbitrary timeout. The "closing" state makes sure
+ // that a/ we ignore any successive messages, and b/ that the connection
+ // is killed after the write buffer is transferred and emptied.
+ // (Since we send this message, we don't need to call CloseWrite here.)
+ c.sendf("ERROR :Closing link: %s[%s] (%s)",
+ nickname, c.hostname /* TODO host IP? */, reason)
+ c.closing = true
+
+ c.unregister(reason)
+ c.killTimer = time.AfterFunc(3*time.Second, func() {
+ timeouts <- c
+ })
+}
+
+func (c *client) inMaskList(masks []string) bool {
+ client := fmt.Sprintf("%s!%s@%s", c.nickname, c.username, c.hostname)
+ for _, mask := range masks {
+ if ircFnmatch(mask, client) {
+ return true
+ }
+ }
+ return false
+}
+
+func (c *client) getTLSCertFingerprint() string {
+ if c.tls == nil {
+ return ""
+ }
+
+ peerCerts := c.tls.ConnectionState().PeerCertificates
+ if len(peerCerts) == 0 {
+ return ""
+ }
+
+ hash := sha256.Sum256(peerCerts[0].Raw)
+ return hex.EncodeToString(hash[:])
+}
+
+// --- Timers ------------------------------------------------------------------
+
+// TODO
+
+// --- IRC command handling ----------------------------------------------------
+
+// XXX: ap doesn't really need to be a slice.
+func (c *client) makeReply(id int, ap []interface{}) string {
+ nickname := c.nickname
+ if nickname == "" {
+ nickname = "*"
+ }
+
+ s := fmt.Sprintf(":%s %03d %s ", serverName, id, nickname)
+ a := fmt.Sprintf(defaultReplies[id], ap...)
+ return s + a
+}
+
+// XXX: This way we cannot typecheck the arguments, so we must be careful.
+func (c *client) sendReply(id int, args ...interface{}) {
+ c.send(c.makeReply(id, args))
+}
+
+/// Send a space-separated list of words across as many replies as needed.
+func (c *client) sendReplyVector(id int, items []string, args ...interface{}) {
+ common := c.makeReply(id, args)
+
+ // We always send at least one message (there might be a client that
+ // expects us to send this message at least once).
+ for len(items) > 0 {
+ // If not even a single item fits in the limit (which may happen,
+ // in theory) it just gets cropped. We could also skip it.
+ reply := append([]byte(common), items[0]...)
+
+ // Append as many items as fits in a single message.
+ for len(items) > 0 &&
+ len(reply)+1+len(items[0]) <= ircMaxMessageLength {
+ reply = append(reply, ' ')
+ reply = append(reply, items[0]...)
+ items = items[1:]
+ }
+
+ c.send(string(reply))
+ }
+}
+
+func (c *client) sendMOTD() {
+ if len(motd) == 0 {
+ c.sendReply(ERR_NOMOTD)
+ return
+ }
+
+ c.sendReply(RPL_MOTDSTART, serverName)
+ for _, line := range motd {
+ c.sendReply(RPL_MOTD, line)
+ }
+ c.sendReply(RPL_ENDOFMOTD)
+}
+
+func (c *client) sendLUSERS() {
+ nUsers, nServices, nOpers, nUnknown := 0, 0, 0, 0
+ for c := range clients {
+ if c.registered {
+ nUsers++
+ } else {
+ nUnknown++
+ }
+ if 0 != c.mode&ircUserModeOperator {
+ nOpers++
+ }
+ }
+
+ nChannels := 0
+ for _, ch := range channels {
+ if 0 != ch.modes&ircChanModeSecret {
+ nChannels++
+ }
+ }
+
+ c.sendReply(RPL_LUSERCLIENT, nUsers, nServices, 1 /* servers total */)
+ if nOpers != 0 {
+ c.sendReply(RPL_LUSEROP, nOpers)
+ }
+ if nUnknown != 0 {
+ c.sendReply(RPL_LUSERUNKNOWN, nUnknown)
+ }
+ if nChannels != 0 {
+ c.sendReply(RPL_LUSERCHANNELS, nChannels)
+ }
+ c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)
+}
+
+func isThisMe(target string) bool {
+ // Target servers can also be matched by their users
+ if ircFnmatch(target, serverName) {
+ return true
+ }
+ _, ok := users[ircStrxfrm(target)]
+ return ok
+}
+
+func (c *client) sendISUPPORT() {
+ // Only # channels, +e supported, +I supported, unlimited arguments to MODE
+ c.sendReply(RPL_ISUPPORT, "CHANTYPES=# EXCEPTS INVEX MODES"+
+ " TARGMAX=WHOIS:,LIST:,NAMES:,PRIVMSG:1,NOTICE:1,KICK:"+
+ " NICKLEN=%d CHANNELLEN=%d", ircMaxNickname, ircMaxChannelName)
+}
+
+func (c *client) tryFinishRegistration() {
+ // TODO: Check if the realname is really required.
+ if c.nickname == "" || c.username == "" || c.realname == "" {
+ return
+ }
+ if c.registered || c.capNegotiating {
+ return
+ }
+
+ c.registered = true
+ c.sendReply(RPL_WELCOME, c.nickname, c.username, c.hostname)
+
+ c.sendReply(RPL_YOURHOST, serverName, "TODO version")
+ // The purpose of this message eludes me.
+ c.sendReply(RPL_CREATED, time.Unix(started, 0).Format("Mon, 02 Jan 2006"))
+ c.sendReply(RPL_MYINFO, serverName, "TODO version",
+ ircSupportedUserModes, ircSupportedChanModes)
+
+ c.sendISUPPORT()
+ c.sendLUSERS()
+ c.sendMOTD()
+
+ if mode := c.getMode(); mode != "" {
+ c.sendf(":%s MODE %s :+%s", c.nickname, c.nickname, mode)
+ }
+
+ c.tlsCertFingerprint = c.getTLSCertFingerprint()
+ if c.tlsCertFingerprint != "" {
+ c.sendf(":%s NOTICE %s :Your TLS client certificate fingerprint is %s",
+ serverName, c.nickname, c.tlsCertFingerprint)
+ }
+
+ delete(whowas, ircStrxfrm(c.nickname))
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// IRCv3 capability negotiation. See http://ircv3.org for details.
+
+type ircCapArgs struct {
+ subcommand string // the subcommand being processed
+ fullParams string // whole parameter string
+ params []string // split parameters
+ target string // target parameter for replies
+}
+
+var ircCapTable = []struct {
+ flag uint // flag
+ name string // name of the capability
+}{
+ {ircCapMultiPrefix, "multi-prefix"},
+ {ircCapInviteNotify, "invite-notify"},
+ {ircCapEchoMessage, "echo-message"},
+ {ircCapUserhostInNames, "userhost-in-names"},
+ {ircCapServerTime, "server-time"},
+}
+
+func (c *client) handleCAPLS(a *ircCapArgs) {
+ if len(a.params) == 1 {
+ if ver, err := strconv.ParseUint(a.params[0], 10, 32); err != nil {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "Ignoring invalid protocol version number")
+ } else {
+ c.capVersion = uint(ver)
+ }
+ }
+
+ c.capNegotiating = true
+ c.sendf(":%s CAP %s LS :multi-prefix invite-notify echo-message"+
+ " userhost-in-names server-time", serverName, a.target)
+}
+
+func (c *client) handleCAPLIST(a *ircCapArgs) {
+ caps := []string{}
+ for _, cap := range ircCapTable {
+ if 0 != c.capsEnabled&cap.flag {
+ caps = append(caps, cap.name)
+ }
+ }
+
+ c.sendf(":%s CAP %s LIST :%s", serverName, a.target,
+ strings.Join(caps, " "))
+}
+
+func ircDecodeCapability(name string) uint {
+ for _, cap := range ircCapTable {
+ if cap.name == name {
+ return cap.flag
+ }
+ }
+ return 0
+}
+
+func (c *client) handleCAPREQ(a *ircCapArgs) {
+ c.capNegotiating = true
+
+ newCaps := c.capsEnabled
+ ok := true
+ for _, param := range a.params {
+ removing := false
+ name := param
+ if name[:1] == "-" {
+ removing = true
+ name = name[1:]
+ }
+
+ if cap := ircDecodeCapability(name); cap == 0 {
+ ok = false
+ } else if removing {
+ newCaps &= ^cap
+ } else {
+ newCaps |= cap
+ }
+ }
+
+ if ok {
+ c.capsEnabled = newCaps
+ c.sendf(":%s CAP %s ACK :%s", serverName, a.target, a.fullParams)
+ } else {
+ c.sendf(":%s CAP %s NAK :%s", serverName, a.target, a.fullParams)
+ }
+}
+
+func (c *client) handleCAPACK(a *ircCapArgs) {
+ if len(a.params) > 0 {
+ c.sendReply(ERR_INVALIDCAPCMD, a.subcommand,
+ "No acknowledgable capabilities supported")
+ }
+}
+
+func (c *client) handleCAPEND(a *ircCapArgs) {
+ c.capNegotiating = false
+ c.tryFinishRegistration()
+}
+
+// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// TODO: Beware of case sensitivity, probably need to index it by ircStrxfrm,
+// which should arguably be named ircToLower and ircToUpper or something.
+var ircCapHandlers = map[string]func(*client, *ircCapArgs){
+ "LS": (*client).handleCAPLS,
+ "LIST": (*client).handleCAPLIST,
+ "REQ": (*client).handleCAPREQ,
+ "ACK": (*client).handleCAPACK,
+ "END": (*client).handleCAPEND,
+}
+
+// XXX: Maybe these also deserve to be methods for client? They operato on
+// global state, though.
+
+func ircHandleCAP(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ // TODO: This really does seem to warrant a method.
+ nickname := c.nickname
+ if nickname == "" {
+ nickname = "*"
+ }
+
+ args := &ircCapArgs{
+ target: nickname,
+ subcommand: msg.params[0],
+ fullParams: "",
+ params: []string{},
+ }
+
+ if len(msg.params) > 1 {
+ args.fullParams = msg.params[1]
+ // TODO: ignore_empty, likely create SplitSkipEmpty
+ args.params = strings.Split(args.fullParams, " ")
+ }
+
+ // FIXME: We should ASCII ToUpper the subcommand.
+ if fn, ok := ircCapHandlers[ircStrxfrm(args.subcommand)]; !ok {
+ c.sendReply(ERR_INVALIDCAPCMD, args.subcommand,
+ "Invalid CAP subcommand")
+ } else {
+ fn(c, args)
+ }
+}
+
+func ircHandlePASS(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ }
+
+ // We have TLS client certificates for this purpose; ignoring.
+}
+
+func ircHandleNICK(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NONICKNAMEGIVEN)
+ return
+ }
+
+ nickname := msg.params[0]
+ if !ircIsValidNickname(nickname) {
+ c.sendReply(ERR_ERRONEOUSNICKNAME, nickname)
+ return
+ }
+
+ nicknameNormalized := ircStrxfrm(nickname)
+ if client, ok := users[nicknameNormalized]; ok && client != c {
+ c.sendReply(ERR_NICKNAMEINUSE, nickname)
+ return
+ }
+
+ if c.registered {
+ c.addToWhowas()
+
+ message := fmt.Sprintf(":%s!%s@%s NICK :%s",
+ c.nickname, c.username, c.hostname, nickname)
+ ircSendToRoommates(c, message)
+ c.send(message)
+ }
+
+ // Release the old nickname and allocate a new one.
+ if c.nickname != "" {
+ delete(users, ircStrxfrm(c.nickname))
+ }
+
+ c.nickname = nickname
+ users[nicknameNormalized] = c
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSER(msg *message, c *client) {
+ if c.registered {
+ c.sendReply(ERR_ALREADYREGISTERED)
+ return
+ }
+ if len(msg.params) < 4 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ username, mode, realname := msg.params[0], msg.params[1], msg.params[3]
+
+ // Unfortunately the protocol doesn't give us any means of rejecting it
+ if !ircIsValidUsername(username) {
+ username = "*"
+ }
+
+ c.username = username
+ c.realname = realname
+
+ c.mode = 0
+ if m, err := strconv.ParseUint(mode, 10, 32); err != nil {
+ if 0 != m&4 {
+ c.mode |= ircUserModeRxWallops
+ }
+ if 0 != m&8 {
+ c.mode |= ircUserModeInvisible
+ }
+ }
+
+ c.tryFinishRegistration()
+}
+
+func ircHandleUSERHOST(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ // TODO
+}
+
+func ircHandleLUSERS(msg *message, c *client) {
+ if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendLUSERS()
+}
+
+func ircHandleMOTD(msg *message, c *client) {
+ if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ return
+ }
+ c.sendMOTD()
+}
+
+func ircHandlePING(msg *message, c *client) {
+ // XXX: The RFC is pretty incomprehensible about the exact usage.
+ if len(msg.params) > 1 && !isThisMe(msg.params[1]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[1])
+ } else if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ } else {
+ c.sendf(":%s PONG :%s", serverName, msg.params[0])
+ }
+}
+
+func ircHandlePONG(msg *message, c *client) {
+ // We are the only server, so we don't have to care too much.
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NOORIGIN)
+ return
+ }
+
+ // Set a new timer to send another PING
+ // TODO
+}
+
+func ircHandleQUIT(msg *message, c *client) {
+ reason := c.nickname
+ if len(msg.params) > 0 {
+ reason = msg.params[0]
+ }
+
+ c.closeLink("Quit: " + reason)
+}
+
+func ircHandleTIME(msg *message, c *client) {
+ if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ c.sendReply(RPL_TIME, serverName,
+ time.Now().Format("Mon Jan _2 2006 15:04:05"))
+}
+
+func ircHandleVERSION(msg *message, c *client) {
+ if len(msg.params) > 0 && !isThisMe(msg.params[0]) {
+ c.sendReply(ERR_NOSUCHSERVER, msg.params[0])
+ return
+ }
+
+ postVersion := 0
+ if debugMode {
+ postVersion = 1
+ }
+
+ c.sendReply(RPL_VERSION, "TODO version", postVersion, serverName,
+ "TODO program name"+" "+"TODO version")
+ c.sendISUPPORT()
+}
+
+/*
+func ircChannelMulticast(ch *channel, msg string, except *client) {
+ for c, m := range ch.userModes {
+ if c != except {
+ c.send(msg)
+ }
+ }
+}
+
+func ircModifyMode(mask *uint, mode uint, add bool) bool {
+ orig := *mask
+ if add {
+ *mask |= mode
+ } else {
+ *mask &= ^mode
+ }
+ return *mask != orig
+}
+
+func ircUpdateUserMode(c *client, newMode uint) {
+ // TODO: Port, as well as all the other kike functions.
+}
+*/
+
+func ircHandleMODE(msg *message, c *client) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NEEDMOREPARAMS, msg.command)
+ return
+ }
+
+ // TODO
+ target := msg.params[0]
+ client := users[ircStrxfrm(target)]
+ ch := users[ircStrxfrm(target)]
+
+ if client != nil {
+ // TODO: Think about strcmp.
+ //if ircStrcmp(target, c.nickname) != 0 {
+ //}
+ } else if ch != nil {
+ // TODO
+ }
+}
+
+func ircHandleUserMessage(msg *message, c *client,
+ command string, allowAwayReply bool) {
+ if len(msg.params) < 1 {
+ c.sendReply(ERR_NORECIPIENT, msg.command)
+ return
+ }
+ if len(msg.params) < 2 || msg.params[1] == "" {
+ c.sendReply(ERR_NOTEXTTOSEND)
+ return
+ }
+
+ target, text := msg.params[0], msg.params[1]
+ if client, ok := users[ircStrxfrm(target)]; ok {
+ // TODO
+ _ = client
+ _ = text
+ } else if ch, ok := channels[ircStrxfrm(target)]; ok {
+ // TODO
+ _ = ch
+ } else {
+ c.sendReply(ERR_NOSUCHNICK, target)
+ }
+}
+
+// TODO: All the various real command handlers.
+
+func ircHandleX(msg *message, c *client) {
+}
+
+// --- ? -----------------------------------------------------------------------
+
+// Handle the results from initializing the client's connection.
+func (c *client) onPrepared(host string, isTLS bool) {
+ if isTLS {
+ c.tls = tls.Server(c.transport, tlsConf)
+ c.conn = c.tls
+ } else {
+ c.conn = c.transport.(connCloseWrite)
+ }
+
+ c.hostname = host
+ c.address = net.JoinHostPort(host, c.port)
+
+ // TODO: If we've tried to send any data before now, we need to flushOutQ.
+ go read(c)
+ c.reading = true
+}
+
+// Handle the results from trying to read from the client connection.
+func (c *client) onRead(data []byte, readErr error) {
+ if !c.reading {
+ // Abusing the flag to emulate CloseRead and skip over data, see below.
+ return
+ }
+
+ c.inQ = append(c.inQ, data...)
+ for {
+ // XXX: This accepts even simple LF newlines, even though they're not
+ // really allowed by the protocol.
+ advance, token, _ := bufio.ScanLines(c.inQ, false /* atEOF */)
+ if advance == 0 {
+ break
+ }
+
+ c.inQ = c.inQ[advance:]
+ line := string(token)
+ log.Printf("-> %s\n", line)
+
+ m := reMsg.FindStringSubmatch(line)
+ if m == nil {
+ log.Println("error: invalid line")
+ continue
+ }
+
+ msg := message{m[1], m[2], m[3], m[4], nil}
+ for _, x := range reArgs.FindAllString(m[5], -1) {
+ msg.params = append(msg.params, x[1:])
+ }
+
+ broadcast(line, c)
+ }
+
+ if readErr != nil {
+ c.reading = false
+
+ if readErr != io.EOF {
+ log.Println(readErr)
+ c.destroy("TODO")
+ } else if c.closing {
+ // Disregarding whether a clean shutdown has happened or not.
+ log.Println("client finished shutdown")
+ c.destroy("TODO")
+ } else {
+ log.Println("client EOF")
+ c.closeLink("")
+ }
+ } else if len(c.inQ) > 8192 {
+ log.Println("client inQ overrun")
+ c.closeLink("inQ overrun")
+
+ // tls.Conn doesn't have the CloseRead method (and it needs to be able
+ // to read from the TCP connection even for writes, so there isn't much
+ // sense in expecting the implementation to do anything useful),
+ // otherwise we'd use it to block incoming packet data.
+ c.reading = false
+ }
+}
+
+// Spawn a goroutine to flush the outQ if possible and necessary.
+func (c *client) flushOutQ() {
+ if !c.writing && c.conn != nil {
+ go write(c, c.outQ)
+ c.writing = true
+ }
+}
+
+// Handle the results from trying to write to the client connection.
+func (c *client) onWrite(written int, writeErr error) {
+ c.outQ = c.outQ[written:]
+ c.writing = false
+
+ if writeErr != nil {
+ log.Println(writeErr)
+ c.destroy("TODO")
+ } else if len(c.outQ) > 0 {
+ c.flushOutQ()
+ } else if c.closing {
+ if c.reading {
+ c.conn.CloseWrite()
+ } else {
+ c.destroy("TODO")
+ }
+ }
+}
+
+// --- Worker goroutines -------------------------------------------------------
+
+func accept(ln net.Listener) {
+ for {
+ if conn, err := ln.Accept(); err != nil {
+ // TODO: Consider specific cases in error handling, some errors
+ // are transitional while others are fatal.
+ log.Println(err)
+ break
+ } else {
+ conns <- conn
+ }
+ }
+}
+
+func prepare(client *client) {
+ conn := client.transport
+ host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
+ if err != nil {
+ // In effect, we require TCP/UDP, as they have port numbers.
+ log.Fatalln(err)
+ }
+
+ // The Cgo resolver doesn't pthread_cancel getnameinfo threads, so not
+ // bothering with pointless contexts.
+ ch := make(chan string, 1)
+ go func() {
+ defer close(ch)
+ if names, err := net.LookupAddr(host); err != nil {
+ log.Println(err)
+ } else {
+ ch <- names[0]
+ }
+ }()
+
+ // While we can't cancel it, we still want to set a timeout on it.
+ select {
+ case <-time.After(5 * time.Second):
+ case resolved, ok := <-ch:
+ if ok {
+ host = resolved
+ }
+ }
+
+ // Note that in this demo application the autodetection prevents non-TLS
+ // clients from receiving any messages until they send something.
+ isTLS := false
+ if sysconn, err := conn.(syscall.Conn).SyscallConn(); err != nil {
+ // This is just for the TLS detection and doesn't need to be fatal.
+ log.Println(err)
+ } else {
+ isTLS = detectTLS(sysconn)
+ }
+
+ // FIXME: When the client sends no data, we still initialize its conn.
+ prepared <- preparedEvent{client, host, isTLS}
+}
+
+func read(client *client) {
+ // A new buffer is allocated each time we receive some bytes, because of
+ // thread-safety. Therefore the buffer shouldn't be too large, or we'd
+ // need to copy it each time into a precisely sized new buffer.
+ var err error
+ for err == nil {
+ var (
+ buf [512]byte
+ n int
+ )
+ n, err = client.conn.Read(buf[:])
+ reads <- readEvent{client, buf[:n], err}
+ }
+}
+
+// Flush outQ, which is passed by parameter so that there are no data races.
+func write(client *client, data []byte) {
+ // We just write as much as we can, the main goroutine does the looping.
+ n, err := client.conn.Write(data)
+ writes <- writeEvent{client, n, err}
+}
+
+// --- Main --------------------------------------------------------------------
+
+func processOneEvent() {
+ select {
+ case <-sigs:
+ if inShutdown {
+ forceShutdown("requested by user")
+ } else {
+ initiateShutdown()
+ }
+
+ case <-shutdownTimer:
+ forceShutdown("timeout")
+
+ case conn := <-conns:
+ log.Println("accepted client connection")
+
+ // In effect, we require TCP/UDP, as they have port numbers.
+ address := conn.RemoteAddr().String()
+ host, port, err := net.SplitHostPort(address)
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ c := &client{
+ transport: conn,
+ address: address,
+ hostname: host,
+ port: port,
+ }
+ clients[c] = true
+ go prepare(c)
+
+ case ev := <-prepared:
+ log.Println("client is ready:", ev.host)
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onPrepared(ev.host, ev.isTLS)
+ }
+
+ case ev := <-reads:
+ log.Println("received data from client")
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onRead(ev.data, ev.err)
+ }
+
+ case ev := <-writes:
+ log.Println("sent data to client")
+ if _, ok := clients[ev.client]; ok {
+ ev.client.onWrite(ev.written, ev.err)
+ }
+
+ case c := <-timeouts:
+ if _, ok := clients[c]; ok {
+ log.Println("client timeouted")
+ c.destroy("TODO")
+ }
+ }
+}
+
+func main() {
+ flag.BoolVar(&debugMode, "debug", false, "debug mode")
+ version := flag.Bool("version", false, "show version and exit")
+ flag.Parse()
+
+ // TODO: Consider using the same version number for all subprojects.
+ if *version {
+ fmt.Printf("%s %s\n", "hid", "0")
+ return
+ }
+
+ // TODO: Configuration--create an INI parser, probably;
+ // lift XDG_CONFIG_HOME from gitlab-notifier.
+ if len(flag.Args()) != 3 {
+ log.Fatalf("usage: %s KEY CERT ADDRESS\n", os.Args[0])
+ }
+
+ cert, err := tls.LoadX509KeyPair(flag.Arg(1), flag.Arg(0))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ tlsConf = &tls.Config{
+ Certificates: []tls.Certificate{cert},
+ ClientAuth: tls.RequestClientCert,
+ SessionTicketsDisabled: true,
+ }
+ listener, err = net.Listen("tcp", flag.Arg(2))
+ if err != nil {
+ log.Fatalln(err)
+ }
+
+ go accept(listener)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ for !inShutdown || len(clients) > 0 {
+ processOneEvent()
+ }
+}