// // 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() } }