aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2018-07-29 15:57:39 +0200
committerPřemysl Janouch <p@janouch.name>2018-07-29 15:57:39 +0200
commitb28c20a250158937e530a8175280e11c6329adf0 (patch)
tree250fb5fabb5763060a37793303cdaff359ae56d5
parent2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80 (diff)
downloadhaven-b28c20a250158937e530a8175280e11c6329adf0.tar.gz
haven-b28c20a250158937e530a8175280e11c6329adf0.tar.xz
haven-b28c20a250158937e530a8175280e11c6329adf0.zip
hid: port PRIVMSG, NOTICE, NAMES, WHO, WHOIS/WAS, TOPIC, SUMMON, USERS
-rw-r--r--hid/main.go520
1 files changed, 486 insertions, 34 deletions
diff --git a/hid/main.go b/hid/main.go
index 98a8b3b..75f4231 100644
--- a/hid/main.go
+++ b/hid/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 {