From 90129ee2bcf67f978b7746e80d2509a8141a3049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Janouch?= Date: Mon, 30 Jul 2018 09:42:01 +0200 Subject: 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. --- xS/main.go | 582 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 560 insertions(+), 22 deletions(-) diff --git a/xS/main.go b/xS/main.go index 4c9176b..ff6a84a 100644 --- a/xS/main.go +++ b/xS/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) { -- cgit v1.2.3