summaryrefslogtreecommitdiff
path: root/xS
diff options
context:
space:
mode:
Diffstat (limited to 'xS')
-rw-r--r--xS/main.go582
1 files 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) {