diff options
| author | Přemysl Janouch <p@janouch.name> | 2018-07-30 09:42:01 +0200 | 
|---|---|---|
| committer | Přemysl Janouch <p@janouch.name> | 2018-07-30 09:46:59 +0200 | 
| commit | 40370702d4189a30905e3eba70a2f01d09918cf1 (patch) | |
| tree | b637ac2fbe6e0260df71c50b7e6255844de848ca /hid | |
| parent | 5429c0b09d9821490fac3343dc9091323de3eae2 (diff) | |
| download | haven-40370702d4189a30905e3eba70a2f01d09918cf1.tar.gz haven-40370702d4189a30905e3eba70a2f01d09918cf1.tar.xz haven-40370702d4189a30905e3eba70a2f01d09918cf1.zip | |
hid: port MODE, STATS, LINKS, KILL
Now all the commands have been ported but we desperately need to parse
a configuration file for additional settings yet.
Diffstat (limited to 'hid')
| -rw-r--r-- | hid/main.go | 582 | 
1 files changed, 560 insertions, 22 deletions
| diff --git a/hid/main.go b/hid/main.go index 4c9176b..ff6a84a 100644 --- a/hid/main.go +++ b/hid/main.go @@ -18,6 +18,7 @@ package main  import (  	"bufio" +	"bytes"  	"crypto/sha256"  	"crypto/tls"  	"encoding/hex" @@ -45,6 +46,11 @@ const (  	projectVersion = "0"  ) +// TODO: Consider using time.Time directly instead of storing Unix epoch +// timestamps with nanosecond precision. Despite carrying unnecessary timezone +// information, it also carries a monotonic reading of the time, which allows +// for more precise measurement of time differences. +  // --- Utilities ---------------------------------------------------------------  // Split a string by a set of UTF-8 delimiters, optionally ignoring empty items. @@ -316,8 +322,6 @@ const (  	ircMaxMessageLength = 510  ) -// TODO: Port the IRC token validation part as needed. -  const reClassSpecial = "\\[\\]\\\\`_^{|}"  var ( @@ -330,6 +334,9 @@ var (  	reUsername = regexp.MustCompile(`^[^\0\r\n @]+$`)  	reChannelName = regexp.MustCompile(`^[^\0\7\r\n ,:]+$`) +	reKey         = regexp.MustCompile(`^[^\r\n\f\t\v ]{1,23}$`) +	reUserMask    = regexp.MustCompile(`^[^!@]+![^!@]+@[^@!]+$`) +	reFingerprint = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)  )  func ircIsValidNickname(nickname string) bool { @@ -346,6 +353,19 @@ func ircIsValidChannelName(name string) bool {  	return len(name) <= ircMaxChannelName && reChannelName.MatchString(name)  } +func ircIsValidKey(key string) bool { +	// XXX: Should be 7-bit as well but whatever. +	return reKey.MatchString(key) +} + +func ircIsValidUserMask(mask string) bool { +	return reUserMask.MatchString(mask) +} + +func ircIsValidFingerprint(fp string) bool { +	return reFingerprint.MatchString(fp) +} +  // --- Clients (equals users) --------------------------------------------------  type connCloseWrite interface { @@ -356,7 +376,7 @@ type connCloseWrite interface {  const ircSupportedUserModes = "aiwros"  const ( -	ircUserModeInvisible = 1 << iota +	ircUserModeInvisible uint = 1 << iota  	ircUserModeRxWallops  	ircUserModeRestricted  	ircUserModeOperator @@ -364,7 +384,7 @@ const (  )  const ( -	ircCapMultiPrefix = 1 << iota +	ircCapMultiPrefix uint = 1 << iota  	ircCapInviteNotify  	ircCapEchoMessage  	ircCapUserhostInNames @@ -417,7 +437,7 @@ type client struct {  const ircSupportedChanModes = "ov" + "beI" + "imnqpst" + "kl"  const ( -	ircChanModeInviteOnly = 1 << iota +	ircChanModeInviteOnly uint = 1 << iota  	ircChanModeModerated  	ircChanModeNoOutsideMsgs  	ircChanModeQuiet @@ -447,11 +467,48 @@ type channel struct {  	inviteList    []string // exceptions from +I  } -func newChannel() *channel { -	return &channel{userLimit: -1} -} +func (ch *channel) getMode(discloseSecrets bool) string { +	var buf []byte +	if 0 != ch.modes&ircChanModeInviteOnly { +		buf = append(buf, 'i') +	} +	if 0 != ch.modes&ircChanModeModerated { +		buf = append(buf, 'm') +	} +	if 0 != ch.modes&ircChanModeNoOutsideMsgs { +		buf = append(buf, 'n') +	} +	if 0 != ch.modes&ircChanModeQuiet { +		buf = append(buf, 'q') +	} +	if 0 != ch.modes&ircChanModePrivate { +		buf = append(buf, 'p') +	} +	if 0 != ch.modes&ircChanModeSecret { +		buf = append(buf, 's') +	} +	if 0 != ch.modes&ircChanModeProtectedTopic { +		buf = append(buf, 'r') +	} -// TODO: Port struct channel methods. +	if ch.userLimit != -1 { +		buf = append(buf, 'l') +	} +	if ch.key != "" { +		buf = append(buf, 'k') +	} + +	// XXX: Is it correct to split it? Try it on an existing implementation. +	if discloseSecrets { +		if ch.userLimit != -1 { +			buf = append(buf, fmt.Sprintf(" %d", ch.userLimit)...) +		} +		if ch.key != "" { +			buf = append(buf, fmt.Sprintf(" %s", ch.key)...) +		} +	} +	return string(buf) +}  // --- IRC server context ------------------------------------------------------ @@ -854,6 +911,7 @@ func (c *client) sendLUSERS() {  	c.sendReply(RPL_LUSERME, nUsers+nServices+nUnknown, 0 /* peer servers */)  } +// TODO: Rename back to ircIsThisMe for consistency with kike.  func isThisMe(target string) bool {  	// Target servers can also be matched by their users  	if ircFnmatch(target, serverName) { @@ -1237,7 +1295,6 @@ func ircChannelMulticast(ch *channel, msg string, except *client) {  	}  } -/*  func ircModifyMode(mask *uint, mode uint, add bool) bool {  	orig := *mask  	if add { @@ -1249,9 +1306,365 @@ func ircModifyMode(mask *uint, mode uint, add bool) bool {  }  func ircUpdateUserMode(c *client, newMode uint) { -	// TODO: Port, as well as all the other kike functions. +	oldMode := c.mode +	c.mode = newMode + +	added, removed := newMode & ^oldMode, oldMode & ^newMode + +	var diff []byte +	if added != 0 { +		diff = append(diff, '+') +		diff = ircAppendClientModes(added, diff) +	} +	if removed != 0 { +		diff = append(diff, '-') +		diff = ircAppendClientModes(removed, diff) +	} + +	if len(diff) > 0 { +		c.sendf(":%s MODE %s :%s", c.nickname, c.nickname, string(diff)) +	}  } -*/ + +func ircHandleUserModeChange(c *client, modeString string) { +	newMode := c.mode +	adding := true + +	for _, flag := range modeString { +		switch flag { +		case '+': +			adding = true +		case '-': +			adding = false + +		case 'a': +			// Ignore, the client should use AWAY. +		case 'i': +			ircModifyMode(&newMode, ircUserModeInvisible, adding) +		case 'w': +			ircModifyMode(&newMode, ircUserModeRxWallops, adding) +		case 'r': +			// It's not possible ot un-restrict yourself. +			if adding { +				newMode |= ircUserModeRestricted +			} +		case 'o': +			if !adding { +				newMode &= ^ircUserModeOperator +			} else if operators[c.tlsCertFingerprint] { +				newMode |= ircUserModeOperator +			} else { +				c.sendf(":%s NOTICE %s :Either you're not using an TLS"+ +					" client certificate, or the fingerprint doesn't match", +					serverName, c.nickname) +			} +		case 's': +			ircModifyMode(&newMode, ircUserModeRxServerNotices, adding) +		default: +			c.sendReply(ERR_UMODEUNKNOWNFLAG) +			return +		} +	} +	ircUpdateUserMode(c, newMode) +} + +func ircSendChannelList(c *client, channelName string, list []string, +	reply, endReply int) { +	for _, line := range list { +		c.sendReply(reply, channelName, line) +	} +	c.sendReply(endReply, channelName) +} + +func ircCheckExpandUserMask(mask string) string { +	var result []byte +	result = append(result, mask...) + +	// Make sure it is a complete mask. +	if bytes.IndexByte(result, '!') < 0 { +		result = append(result, "!*"...) +	} +	if bytes.IndexByte(result, '@') < 0 { +		result = append(result, "@*"...) +	} + +	// And validate whatever the result is. +	s := string(result) +	if !ircIsValidUserMask(s) { +		return "" +	} + +	return s +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +// Channel MODE command handling. This is by far the worst command to implement +// from the whole RFC; don't blame me if it doesn't work exactly as expected. + +type modeProcessor struct { +	params []string // mode string parameters + +	c       *client  // who does the changes +	ch      *channel // the channel we're modifying +	present bool     // c present on ch +	modes   uint     // channel user modes + +	adding   bool // currently adding modes +	modeChar byte // currently processed mode char + +	added   []byte  // added modes +	removed []byte  // removed modes +	output  *[]byte // "added" or "removed" + +	addedParams   []string  // params for added modes +	removedParams []string  // params for removed modes +	outputParams  *[]string // "addedParams" or "removedParams" +} + +func (mp *modeProcessor) nextParam() string { +	if len(mp.params) == 0 { +		return "" +	} + +	param := mp.params[0] +	mp.params = mp.params[1:] +	return param +} + +func (mp *modeProcessor) checkOperator() bool { +	if (mp.present && 0 != mp.modes&ircChanModeOperator) || +		0 != mp.c.mode&ircUserModeOperator { +		return true +	} + +	mp.c.sendReply(ERR_CHANOPRIVSNEEDED, mp.ch.name) +	return false +} + +func (mp *modeProcessor) doUser(mode uint) { +	target := mp.nextParam() +	if !mp.checkOperator() || target == "" { +		return +	} + +	if client := users[ircToCanon(target)]; client == nil { +		mp.c.sendReply(ERR_NOSUCHNICK, target) +	} else if modes, present := mp.ch.userModes[client]; !present { +		mp.c.sendReply(ERR_USERNOTINCHANNEL, target, mp.ch.name) +	} else if ircModifyMode(&modes, mode, mp.adding) { +		mp.ch.userModes[client] = modes +		*mp.output = append(*mp.output, mp.modeChar) +		*mp.outputParams = append(*mp.outputParams, client.nickname) +	} +} + +func (mp *modeProcessor) doChan(mode uint) bool { +	if !mp.checkOperator() || !ircModifyMode(&mp.ch.modes, mode, mp.adding) { +		return false +	} +	*mp.output = append(*mp.output, mp.modeChar) +	return true +} + +func (mp *modeProcessor) doChanRemove(modeChar byte, mode uint) { +	if mp.adding && ircModifyMode(&mp.ch.modes, mode, false) { +		mp.removed = append(mp.removed, modeChar) +	} +} + +func (mp *modeProcessor) doList(list *[]string, listMsg, endMsg int) { +	target := mp.nextParam() +	if target == "" { +		if mp.adding { +			ircSendChannelList(mp.c, mp.ch.name, *list, listMsg, endMsg) +		} +		return +	} + +	if !mp.checkOperator() { +		return +	} + +	mask := ircCheckExpandUserMask(target) +	if mask == "" { +		return +	} + +	var i int +	for i = 0; i < len(*list); i++ { +		if ircEqual((*list)[i], mask) { +			break +		} +	} + +	found := i < len(*list) +	if found != mp.adding { +		if mp.adding { +			*list = append(*list, mask) +		} else { +			*list = append((*list)[:i], (*list)[i+1:]...) +		} + +		*mp.output = append(*mp.output, mp.modeChar) +		*mp.outputParams = append(*mp.outputParams, mask) +	} +} + +func (mp *modeProcessor) doKey() { +	target := mp.nextParam() +	if !mp.checkOperator() || target == "" { +		return +	} + +	if !mp.adding { +		if mp.ch.key == "" || !ircEqual(target, mp.ch.key) { +			return +		} + +		mp.removed = append(mp.removed, mp.modeChar) +		mp.removedParams = append(mp.removedParams, mp.ch.key) +		mp.ch.key = "" +	} else if !ircIsValidKey(target) { +		// TODO: We should notify the user somehow. +		return +	} else if mp.ch.key != "" { +		mp.c.sendReply(ERR_KEYSET, mp.ch.name) +	} else { +		mp.ch.key = target +		mp.added = append(mp.added, mp.modeChar) +		mp.addedParams = append(mp.addedParams, mp.ch.key) +	} +} + +func (mp *modeProcessor) doLimit() { +	if !mp.checkOperator() { +		return +	} + +	if !mp.adding { +		if mp.ch.userLimit == -1 { +			return +		} + +		mp.ch.userLimit = -1 +		mp.removed = append(mp.removed, mp.modeChar) +	} else if target := mp.nextParam(); target != "" { +		if x, err := strconv.ParseInt(target, 10, 32); err == nil && x > 0 { +			mp.ch.userLimit = int(x) +			mp.added = append(mp.added, mp.modeChar) +			mp.addedParams = append(mp.addedParams, target) +		} +	} +} + +func (mp *modeProcessor) step(modeChar byte) bool { +	mp.modeChar = modeChar +	switch mp.modeChar { +	case '+': +		mp.adding = true +		mp.output = &mp.added +		mp.outputParams = &mp.addedParams +	case '-': +		mp.adding = false +		mp.output = &mp.removed +		mp.outputParams = &mp.removedParams + +	case 'o': +		mp.doUser(ircChanModeOperator) +	case 'v': +		mp.doUser(ircChanModeVoice) + +	case 'i': +		mp.doChan(ircChanModeInviteOnly) +	case 'm': +		mp.doChan(ircChanModeModerated) +	case 'n': +		mp.doChan(ircChanModeNoOutsideMsgs) +	case 'q': +		mp.doChan(ircChanModeQuiet) +	case 't': +		mp.doChan(ircChanModeProtectedTopic) + +	case 'p': +		if mp.doChan(ircChanModePrivate) { +			mp.doChanRemove('s', ircChanModeSecret) +		} +	case 's': +		if mp.doChan(ircChanModeSecret) { +			mp.doChanRemove('p', ircChanModePrivate) +		} + +	case 'b': +		mp.doList(&mp.ch.banList, RPL_BANLIST, RPL_ENDOFBANLIST) +	case 'e': +		mp.doList(&mp.ch.banList, RPL_EXCEPTLIST, RPL_ENDOFEXCEPTLIST) +	case 'I': +		mp.doList(&mp.ch.banList, RPL_INVITELIST, RPL_ENDOFINVITELIST) + +	case 'k': +		mp.doKey() +	case 'l': +		mp.doLimit() + +	default: +		// It's not safe to continue, results could be undesired. +		mp.c.sendReply(ERR_UNKNOWNMODE, modeChar, mp.ch.name) +		return false +	} +	return true +} + +func ircHandleChanModeChange(c *client, ch *channel, params []string) { +	modes, present := ch.userModes[c] +	mp := &modeProcessor{ +		c:       c, +		ch:      ch, +		present: present, +		modes:   modes, +		params:  params, +	} + +Outer: +	for { +		modeString := mp.nextParam() +		if modeString == "" { +			break +		} + +		mp.step('+') +		for _, modeChar := range []byte(modeString) { +			if !mp.step(modeChar) { +				break Outer +			} +		} +	} + +	// TODO: Limit to three changes with parameter per command. +	if len(mp.added) > 0 || len(mp.removed) > 0 { +		buf := []byte(fmt.Sprintf(":%s!%s@%s MODE %s ", +			mp.c.nickname, mp.c.username, mp.c.hostname, mp.ch.name)) +		if len(mp.added) > 0 { +			buf = append(buf, '+') +			buf = append(buf, mp.added...) +		} +		if len(mp.removed) > 0 { +			buf = append(buf, '-') +			buf = append(buf, mp.removed...) +		} +		for _, param := range mp.addedParams { +			buf = append(buf, ' ') +			buf = append(buf, param...) +		} +		for _, param := range mp.removedParams { +			buf = append(buf, ' ') +			buf = append(buf, param...) +		} +		ircChannelMulticast(mp.ch, string(buf), nil) +	} +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  func ircHandleMODE(msg *message, c *client) {  	if len(msg.params) < 1 { @@ -1259,17 +1672,32 @@ func ircHandleMODE(msg *message, c *client) {  		return  	} -	// TODO  	target := msg.params[0]  	client := users[ircToCanon(target)] -	ch := users[ircToCanon(target)] +	ch := channels[ircToCanon(target)]  	if client != nil { -		// TODO  		if ircEqual(target, c.nickname) { +			c.sendReply(ERR_USERSDONTMATCH) +			return +		} + +		if len(msg.params) < 2 { +			c.sendReply(RPL_UMODEIS, c.getMode()) +		} else { +			ircHandleUserModeChange(c, msg.params[1])  		}  	} else if ch != nil { -		// TODO +		if len(msg.params) < 2 { +			_, present := ch.userModes[c] +			c.sendReply(RPL_CHANNELMODEIS, target, ch.getMode(present)) +			c.sendReply(RPL_CREATIONTIME, +				target, ch.created/int64(time.Second)) +		} else { +			ircHandleChanModeChange(c, ch, msg.params[1:]) +		} +	} else { +		c.sendReply(ERR_NOSUCHNICK, target)  	}  } @@ -1973,21 +2401,128 @@ func ircHandleADMIN(msg *message, c *client) {  	c.sendReply(ERR_NOADMININFO, serverName)  } -// TODO: All the remaining command handlers. +func ircHandleStatsLinks(c *client, msg *message) { +	// There is only an "l" query in RFC 2812 but we cannot link, +	// so instead we provide the "L" query giving information for all users. +	filter := "" +	if len(msg.params) > 1 { +		filter = msg.params[1] +	} -func ircHandleX(msg *message, c *client) { -	if len(msg.params) < 1 { +	for _, client := range users { +		if filter != "" && !ircEqual(client.nickname, filter) { +			continue +		} +		c.sendReply(RPL_STATSLINKINFO, +			client.address,    // linkname +			len(client.sendQ), // sendq +			client.nSentMessages, client.sentBytes/1024, +			client.nReceivedMessages, client.receivedBytes/1024, +			(time.Now().UnixNano()-client.opened)/int64(time.Second)) +	} +} + +func ircHandleStatsCommands(c *client) { +	for name, handler := range ircHandlers { +		if handler.nReceived > 0 { +			c.sendReply(RPL_STATSCOMMANDS, name, +				handler.nReceived, handler.bytesReceived, 0) +		} +	} +} + +// We need to do it this way because of an initialization loop concerning +// ircHandlers. Workaround proposed by rsc in #1817. +var ircHandleStatsCommandsIndirect func(c *client) + +func init() { +	ircHandleStatsCommandsIndirect = ircHandleStatsCommands +} + +func ircHandleStatsUptime(c *client) { +	uptime := (time.Now().UnixNano() - started) / int64(time.Second) + +	days := uptime / 60 / 60 / 24 +	hours := (uptime % (60 * 60 * 24)) / 60 / 60 +	mins := (uptime % (60 * 60)) / 60 +	secs := uptime % 60 + +	c.sendReply(RPL_STATSUPTIME, days, hours, mins, secs) +} + +func ircHandleSTATS(msg *message, c *client) { +	var query byte +	if len(msg.params) > 0 && len(msg.params[0]) > 0 { +		query = msg.params[0][0] +	} + +	if len(msg.params) > 1 && !isThisMe(msg.params[1]) { +		c.sendReply(ERR_NOSUCHSERVER, msg.params[0]) +		return +	} +	if 0 == c.mode&ircUserModeOperator { +		c.sendReply(ERR_NOPRIVILEGES) +		return +	} + +	switch query { +	case 'L': +		ircHandleStatsLinks(c, msg) +	case 'm': +		ircHandleStatsCommandsIndirect(c) +	case 'u': +		ircHandleStatsUptime(c) +	} +	c.sendReply(RPL_ENDOFSTATS, query) +} + +func ircHandleLINKS(msg *message, c *client) { +	if len(msg.params) > 1 && !isThisMe(msg.params[0]) {  		c.sendReply(ERR_NEEDMOREPARAMS, msg.command)  		return  	} + +	mask := "*" +	if len(msg.params) > 0 { +		if len(msg.params) > 1 { +			mask = msg.params[1] +		} else { +			mask = msg.params[0] +		} +	} + +	if ircFnmatch(mask, serverName) { +		c.sendReply(RPL_LINKS, mask, serverName, +			0 /* hop count */, "TODO server_info from configuration") +	} +	c.sendReply(RPL_ENDOFLINKS, mask)  } -func ircHandleDIE(msg *message, c *client) { +func ircHandleKILL(msg *message, c *client) { +	if len(msg.params) < 2 { +		c.sendReply(ERR_NEEDMOREPARAMS, msg.command) +		return +	}  	if 0 == c.mode&ircUserModeOperator {  		c.sendReply(ERR_NOPRIVILEGES)  		return  	} -	if !quitting { + +	target := users[ircToCanon(msg.params[0])] +	if target == nil { +		c.sendReply(ERR_NOSUCHNICK, msg.params[0]) +		return +	} + +	c.sendf(":%s!%s@%s KILL %s :%s", +		c.nickname, c.username, c.hostname, target.nickname, msg.params[1]) +	target.closeLink(fmt.Sprintf("Killed by %s: %s", c.nickname, msg.params[1])) +} + +func ircHandleDIE(msg *message, c *client) { +	if 0 == c.mode&ircUserModeOperator { +		c.sendReply(ERR_NOPRIVILEGES) +	} else if !quitting {  		initiateQuit()  	}  } @@ -2015,6 +2550,8 @@ var ircHandlers = map[string]*ircCommand{  	"SUMMON":   {true, ircHandleSUMMON, 0, 0},  	"AWAY":     {true, ircHandleAWAY, 0, 0},  	"ADMIN":    {true, ircHandleADMIN, 0, 0}, +	"STATS":    {true, ircHandleSTATS, 0, 0}, +	"LINKS":    {true, ircHandleLINKS, 0, 0},  	"MODE":    {true, ircHandleMODE, 0, 0},  	"PRIVMSG": {true, ircHandlePRIVMSG, 0, 0}, @@ -2031,7 +2568,8 @@ var ircHandlers = map[string]*ircCommand{  	"WHOWAS":  {true, ircHandleWHOWAS, 0, 0},  	"ISON":    {true, ircHandleISON, 0, 0}, -	"DIE": {true, ircHandleDIE, 0, 0}, +	"KILL": {true, ircHandleKILL, 0, 0}, +	"DIE":  {true, ircHandleDIE, 0, 0},  }  func ircProcessMessage(c *client, msg *message, raw string) { | 
