diff options
-rw-r--r-- | xS/main.go | 520 |
1 files changed, 486 insertions, 34 deletions
@@ -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 { |