diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-07-29 15:57:39 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-07-29 15:57:39 +0200 | 
| commit | b28c20a250158937e530a8175280e11c6329adf0 (patch) | |
| tree | 250fb5fabb5763060a37793303cdaff359ae56d5 | |
| parent | 2dfb4e45d1c8f073ec0a5a9a4761acbebeb3fc80 (diff) | |
| download | haven-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.go | 520 | 
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 { | 
