diff --git a/Gopkg.lock b/Gopkg.lock index 78234ee2..73ab4a22 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -27,7 +27,7 @@ [[projects]] branch = "master" - digest = "1:6bcd7bcd5e14cc9552fbf83b2f77f24935c0c502d009c9825b6c212c3f8eb967" + digest = "1:e6ed6eaa63211bb90847d8c5f11d7412e56c96b5befb7402ee7a7a8ad02700ec" name = "github.com/goshuirc/irc-go" packages = [ "ircfmt", @@ -35,7 +35,7 @@ "ircmsg", ] pruneopts = "UT" - revision = "cf199aea7186fd960d0ed5abbf579bb0f9d890d1" + revision = "ca74bf6a176d2d1dce6f28f99901a2d48d8da2bd" [[projects]] digest = "1:c658e84ad3916da105a761660dcaeb01e63416c8ec7bc62256a9b411a05fcd67" diff --git a/gencapdefs.py b/gencapdefs.py index 9c3fe2e3..90ac244f 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -83,15 +83,15 @@ CAPDEFS = [ ), CapDef( identifier="MaxLine", - name="oragono.io/maxline", - url="https://oragono.io/maxline", + name="oragono.io/maxline-2", + url="https://oragono.io/maxline-2", standard="Oragono-specific", ), CapDef( identifier="MessageTags", - name="draft/message-tags-0.2", - url="https://ircv3.net/specs/core/message-tags-3.3.html", - standard="draft IRCv3", + name="message-tags", + url="https://ircv3.net/specs/extensions/message-tags.html", + standard="IRCv3", ), CapDef( identifier="MultiPrefix", diff --git a/irc/caps/defs.go b/irc/caps/defs.go index adfbbb7d..2d8b783b 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -57,12 +57,12 @@ const ( // https://gist.github.com/DanielOaks/8126122f74b26012a3de37db80e4e0c6 Languages Capability = iota - // MaxLine is the Oragono-specific capability named "oragono.io/maxline": - // https://oragono.io/maxline + // MaxLine is the Oragono-specific capability named "oragono.io/maxline-2": + // https://oragono.io/maxline-2 MaxLine Capability = iota - // MessageTags is the draft IRCv3 capability named "draft/message-tags-0.2": - // https://ircv3.net/specs/core/message-tags-3.3.html + // MessageTags is the IRCv3 capability named "message-tags": + // https://ircv3.net/specs/extensions/message-tags.html MessageTags Capability = iota // MultiPrefix is the IRCv3 capability named "multi-prefix": @@ -112,8 +112,8 @@ var ( "invite-notify", "draft/labeled-response", "draft/languages", - "oragono.io/maxline", - "draft/message-tags-0.2", + "oragono.io/maxline-2", + "message-tags", "multi-prefix", "draft/rename", "draft/resume-0.3", diff --git a/irc/channel.go b/irc/channel.go index 02a01ceb..a1ab386d 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -14,7 +14,6 @@ import ( "sync" - "github.com/goshuirc/irc-go/ircmsg" "github.com/oragono/oragono/irc/caps" "github.com/oragono/oragono/irc/history" "github.com/oragono/oragono/irc/modes" @@ -425,11 +424,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp channel.regenerateMembersCache() + message := utils.SplitMessage{} + message.Msgid = details.realname channel.history.Add(history.Item{ Type: history.Join, Nick: details.nickMask, AccountName: details.accountName, - Msgid: details.realname, + Message: message, }) return @@ -603,16 +604,17 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I serverTime := client.capabilities.Has(caps.ServerTime) for _, item := range items { - var tags Tags + var tags map[string]string if serverTime { - tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat)) + tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)} } + // TODO(#437) support history.Tagmsg switch item.Type { case history.Privmsg: - rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message) case history.Notice: - rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message) case history.Join: nick := stripMaskFromNick(item.Nick) var message string @@ -624,16 +626,16 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I rb.Add(tags, "HistServ", "PRIVMSG", chname, message) case history.Part: nick := stripMaskFromNick(item.Nick) - message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Original) + message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) rb.Add(tags, "HistServ", "PRIVMSG", chname, message) case history.Quit: nick := stripMaskFromNick(item.Nick) - message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Original) + message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) rb.Add(tags, "HistServ", "PRIVMSG", chname, message) case history.Kick: nick := stripMaskFromNick(item.Nick) // XXX Msgid is the kick target - message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Msgid, item.Message.Original) + message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Message.Msgid, item.Message.Message) rb.Add(tags, "HistServ", "PRIVMSG", chname, message) } } @@ -717,13 +719,20 @@ func (channel *Channel) CanSpeak(client *Client) bool { return true } -// TagMsg sends a tag message to everyone in this channel who can accept them. -func (channel *Channel) TagMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, rb *ResponseBuffer) { - channel.sendMessage(msgid, "TAGMSG", []caps.Capability{caps.MessageTags}, minPrefix, clientOnlyTags, client, nil, rb) -} +func (channel *Channel) SendSplitMessage(command string, minPrefix *modes.Mode, clientOnlyTags map[string]string, client *Client, message utils.SplitMessage, rb *ResponseBuffer) { + var histType history.ItemType + switch command { + case "PRIVMSG": + histType = history.Privmsg + case "NOTICE": + histType = history.Notice + case "TAGMSG": + histType = history.Tagmsg + default: + channel.server.logger.Error("internal", "unrecognized Channel.SendSplitMessage command", command) + return + } -// sendMessage sends a given message to everyone on this channel. -func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capability, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *string, rb *ResponseBuffer) { if !channel.CanSpeak(client) { rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) return @@ -736,85 +745,16 @@ func (channel *Channel) sendMessage(msgid, cmd string, requiredCaps []caps.Capab } // send echo-message if client.capabilities.Has(caps.EchoMessage) { - var messageTagsToUse *map[string]ircmsg.TagValue - if client.capabilities.Has(caps.MessageTags) { - messageTagsToUse = clientOnlyTags - } - - nickMaskString := client.NickMaskString() - accountName := client.AccountName() - if message == nil { - rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name) - } else { - rb.AddFromClient(msgid, nickMaskString, accountName, messageTagsToUse, cmd, channel.name, *message) - } - } - for _, member := range channel.Members() { - if minPrefix != nil && !channel.ClientIsAtLeast(member, minPrefixMode) { - // STATUSMSG - continue - } - // echo-message is handled above, so skip sending the msg to the user themselves as well - if member == client { - continue - } - - canReceive := true - for _, capName := range requiredCaps { - if !member.capabilities.Has(capName) { - canReceive = false - } - } - if !canReceive { - continue - } - - var messageTagsToUse *map[string]ircmsg.TagValue - if member.capabilities.Has(caps.MessageTags) { - messageTagsToUse = clientOnlyTags - } - - if message == nil { - member.SendFromClient(msgid, client, messageTagsToUse, cmd, channel.name) - } else { - member.SendFromClient(msgid, client, messageTagsToUse, cmd, channel.name, *message) - } - } -} - -// SplitPrivMsg sends a private message to everyone in this channel. -func (channel *Channel) SplitPrivMsg(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message utils.SplitMessage, rb *ResponseBuffer) { - channel.sendSplitMessage(msgid, "PRIVMSG", history.Privmsg, minPrefix, clientOnlyTags, client, &message, rb) -} - -// SplitNotice sends a private message to everyone in this channel. -func (channel *Channel) SplitNotice(msgid string, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message utils.SplitMessage, rb *ResponseBuffer) { - channel.sendSplitMessage(msgid, "NOTICE", history.Notice, minPrefix, clientOnlyTags, client, &message, rb) -} - -func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.ItemType, minPrefix *modes.Mode, clientOnlyTags *map[string]ircmsg.TagValue, client *Client, message *utils.SplitMessage, rb *ResponseBuffer) { - if !channel.CanSpeak(client) { - rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) - return - } - - // for STATUSMSG - var minPrefixMode modes.Mode - if minPrefix != nil { - minPrefixMode = *minPrefix - } - // send echo-message - if client.capabilities.Has(caps.EchoMessage) { - var tagsToUse *map[string]ircmsg.TagValue + var tagsToUse map[string]string if client.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags } nickMaskString := client.NickMaskString() accountName := client.AccountName() - if message == nil { - rb.AddFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name) + if command == "TAGMSG" && client.capabilities.Has(caps.MessageTags) { + rb.AddFromClient(message.Msgid, nickMaskString, accountName, tagsToUse, command, channel.name) } else { - rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, tagsToUse, cmd, channel.name, *message) + rb.AddSplitMessageFromClient(nickMaskString, accountName, tagsToUse, command, channel.name, message) } } @@ -832,22 +772,23 @@ func (channel *Channel) sendSplitMessage(msgid, cmd string, histType history.Ite if member == client { continue } - var tagsToUse *map[string]ircmsg.TagValue + var tagsToUse map[string]string if member.capabilities.Has(caps.MessageTags) { tagsToUse = clientOnlyTags + } else if command == "TAGMSG" { + continue } - if message == nil { - member.sendFromClientInternal(false, now, msgid, nickmask, account, tagsToUse, cmd, channel.name) + if command == "TAGMSG" { + member.sendFromClientInternal(false, now, message.Msgid, nickmask, account, tagsToUse, command, channel.name) } else { - member.sendSplitMsgFromClientInternal(false, now, msgid, nickmask, account, tagsToUse, cmd, channel.name, *message) + member.sendSplitMsgFromClientInternal(false, now, nickmask, account, tagsToUse, command, channel.name, message) } } channel.history.Add(history.Item{ Type: histType, - Msgid: msgid, - Message: *message, + Message: message, Nick: nickmask, AccountName: account, Time: now, @@ -980,12 +921,14 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb member.Send(nil, clientMask, "KICK", channel.name, targetNick, comment) } + message := utils.SplitMessage{} + message.Message = comment + message.Msgid = targetNick // XXX abuse this field channel.history.Add(history.Item{ Type: history.Kick, Nick: clientMask, - Message: utils.MakeSplitMessage(comment, true), AccountName: target.AccountName(), - Msgid: targetNick, // XXX abuse this field + Message: message, }) channel.Quit(target) diff --git a/irc/client.go b/irc/client.go index 3ae9ac22..4a8bbb71 100644 --- a/irc/client.go +++ b/irc/client.go @@ -62,14 +62,13 @@ type Client struct { hasQuit bool hops int hostname string - idletimer *IdleTimer + idletimer IdleTimer invitedTo map[string]bool isDestroyed bool isTor bool isQuitting bool languages []string loginThrottle connection_limits.GenericThrottle - maxlenTags uint32 maxlenRest uint32 nick string nickCasefolded string @@ -122,8 +121,9 @@ type ClientDetails struct { func RunNewClient(server *Server, conn clientConn) { now := time.Now() config := server.Config() - fullLineLenLimit := config.Limits.LineLen.Tags + config.Limits.LineLen.Rest - socket := NewSocket(conn.Conn, fullLineLenLimit*2, config.Server.MaxSendQBytes) + fullLineLenLimit := ircmsg.MaxlenTagsFromClient + config.Limits.LineLen.Rest + // give them 1k of grace over the limit: + socket := NewSocket(conn.Conn, fullLineLenLimit+1024, config.Server.MaxSendQBytes) client := &Client{ atime: now, capabilities: caps.NewSet(), @@ -260,30 +260,21 @@ func (client *Client) IPString() string { // command goroutine // -func (client *Client) recomputeMaxlens() (int, int) { - maxlenTags := 512 +func (client *Client) recomputeMaxlens() int { maxlenRest := 512 - if client.capabilities.Has(caps.MessageTags) { - maxlenTags = 4096 - } if client.capabilities.Has(caps.MaxLine) { - limits := client.server.Limits() - if limits.LineLen.Tags > maxlenTags { - maxlenTags = limits.LineLen.Tags - } - maxlenRest = limits.LineLen.Rest + maxlenRest = client.server.Limits().LineLen.Rest } - atomic.StoreUint32(&client.maxlenTags, uint32(maxlenTags)) atomic.StoreUint32(&client.maxlenRest, uint32(maxlenRest)) - return maxlenTags, maxlenRest + return maxlenRest } // allow these negotiated length limits to be read without locks; this is a convenience // so that Client.Send doesn't have to acquire any Client locks -func (client *Client) maxlens() (int, int) { - return int(atomic.LoadUint32(&client.maxlenTags)), int(atomic.LoadUint32(&client.maxlenRest)) +func (client *Client) MaxlenRest() int { + return int(atomic.LoadUint32(&client.maxlenRest)) } func (client *Client) run() { @@ -306,8 +297,7 @@ func (client *Client) run() { client.destroy(false) }() - client.idletimer = NewIdleTimer(client) - client.idletimer.Start() + client.idletimer.Initialize(client) client.nickTimer = NewNickTimer(client) @@ -316,7 +306,7 @@ func (client *Client) run() { firstLine := true for { - maxlenTags, maxlenRest := client.recomputeMaxlens() + maxlenRest := client.recomputeMaxlens() line, err = client.socket.Read() if err != nil { @@ -345,9 +335,12 @@ func (client *Client) run() { } } - msg, err = ircmsg.ParseLineMaxLen(line, maxlenTags, maxlenRest) + msg, err = ircmsg.ParseLineStrict(line, true, maxlenRest) if err == ircmsg.ErrorLineIsEmpty { continue + } else if err == ircmsg.ErrorLineTooLong { + client.Send(nil, client.server.name, ERR_INPUTTOOLONG, client.nick, client.t("Input line too long")) + continue } else if err != nil { client.Quit(client.t("Received malformed line")) break @@ -553,11 +546,11 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I default: continue } - var tags Tags + var tags map[string]string if serverTime { - tags = ensureTag(tags, "time", item.Time.Format(IRCv3TimestampFormat)) + tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)} } - rb.AddSplitMessageFromClient(item.Msgid, item.Nick, item.AccountName, tags, command, nick, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) } if !complete { rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost")) @@ -855,17 +848,18 @@ func (client *Client) Quit(message string) { return } - var quitLine string + var finalData []byte // #364: don't send QUIT lines to unregistered clients if registered { quitMsg := ircmsg.MakeMessage(nil, prefix, "QUIT", message) - quitLine, _ = quitMsg.Line() + finalData, _ = quitMsg.LineBytesStrict(false, 512) } errorMsg := ircmsg.MakeMessage(nil, "", "ERROR", message) - errorLine, _ := errorMsg.Line() + errorMsgBytes, _ := errorMsg.LineBytesStrict(false, 512) + finalData = append(finalData, errorMsgBytes...) - client.socket.SetFinalData(quitLine + errorLine) + client.socket.SetFinalData(finalData) } // destroy gets rid of a client, removes them from server lists etc. @@ -976,50 +970,45 @@ func (client *Client) destroy(beingResumed bool) { // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client. // Adds account-tag to the line as well. -func (client *Client) SendSplitMsgFromClient(msgid string, from *Client, tags Tags, command, target string, message utils.SplitMessage) { - client.sendSplitMsgFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, target, message) +func (client *Client) SendSplitMsgFromClient(from *Client, tags map[string]string, command, target string, message utils.SplitMessage) { + client.sendSplitMsgFromClientInternal(false, time.Time{}, from.NickMaskString(), from.AccountName(), tags, command, target, message) } -func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags Tags, command, target string, message utils.SplitMessage) { +func (client *Client) sendSplitMsgFromClientInternal(blocking bool, serverTime time.Time, nickmask, accountName string, tags map[string]string, command, target string, message utils.SplitMessage) { if client.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { - client.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, target, message.Original) + client.sendFromClientInternal(blocking, serverTime, message.Msgid, nickmask, accountName, tags, command, target, message.Message) } else { - for _, str := range message.Wrapped { - client.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, target, str) + for _, messagePair := range message.Wrapped { + client.sendFromClientInternal(blocking, serverTime, messagePair.Msgid, nickmask, accountName, tags, command, target, messagePair.Message) } } } // SendFromClient sends an IRC line coming from a specific client. // Adds account-tag to the line as well. -func (client *Client) SendFromClient(msgid string, from *Client, tags Tags, command string, params ...string) error { +func (client *Client) SendFromClient(msgid string, from *Client, tags map[string]string, command string, params ...string) error { return client.sendFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, params...) } -// helper to add a tag to `tags` (or create a new tag set if the current one is nil) -func ensureTag(tags Tags, tagName, tagValue string) (result Tags) { - if tags == nil { - result = ircmsg.MakeTags(tagName, tagValue) - } else { - result = tags - (*tags)[tagName] = ircmsg.MakeTagValue(tagValue) - } - return -} - -// XXX this is a hack where we allow overriding the client's nickmask -// this is to support CHGHOST, which requires that we send the *original* nickmask with the response -func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags Tags, command string, params ...string) error { +// this is SendFromClient, but directly exposing nickmask and accountName, +// for things like history replay and CHGHOST where they no longer (necessarily) +// correspond to the current state of a client +func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) error { + msg := ircmsg.MakeMessage(tags, nickmask, command, params...) // attach account-tag if client.capabilities.Has(caps.AccountTag) && accountName != "*" { - tags = ensureTag(tags, "account", accountName) + msg.SetTag("account", accountName) } // attach message-id - if len(msgid) > 0 && client.capabilities.Has(caps.MessageTags) { - tags = ensureTag(tags, "draft/msgid", msgid) + if msgid != "" && client.capabilities.Has(caps.MessageTags) { + msg.SetTag("draft/msgid", msgid) + } + // attach server-time + if client.capabilities.Has(caps.ServerTime) { + msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } - return client.sendInternal(blocking, serverTime, tags, nickmask, command, params...) + return client.SendRawMessage(msg, blocking) } var ( @@ -1039,24 +1028,24 @@ var ( func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) error { // use dumb hack to force the last param to be a trailing param if required var usedTrailingHack bool - if commandsThatMustUseTrailing[strings.ToUpper(message.Command)] && len(message.Params) > 0 { + if commandsThatMustUseTrailing[message.Command] && len(message.Params) > 0 { lastParam := message.Params[len(message.Params)-1] // to force trailing, we ensure the final param contains a space - if !strings.Contains(lastParam, " ") { + if strings.IndexByte(lastParam, ' ') == -1 { message.Params[len(message.Params)-1] = lastParam + " " usedTrailingHack = true } } // assemble message - maxlenTags, maxlenRest := client.maxlens() - line, err := message.LineMaxLenBytes(maxlenTags, maxlenRest) + maxlenRest := client.MaxlenRest() + line, err := message.LineBytesStrict(false, maxlenRest) if err != nil { logline := fmt.Sprintf("Error assembling message for sending: %v\n%s", err, debug.Stack()) client.server.logger.Error("internal", logline) message = ircmsg.MakeMessage(nil, client.server.name, ERR_UNKNOWNERROR, "*", "Error assembling message for sending") - line, _ := message.LineBytes() + line, _ := message.LineBytesStrict(false, 0) if blocking { client.socket.BlockingWrite(line) @@ -1068,7 +1057,7 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e // if we used the trailing hack, we need to strip the final space we appended earlier on if usedTrailingHack { - copy(line[len(line)-3:], []byte{'\r', '\n'}) + copy(line[len(line)-3:], "\r\n") line = line[:len(line)-1] } @@ -1084,24 +1073,13 @@ func (client *Client) SendRawMessage(message ircmsg.IrcMessage, blocking bool) e } } -func (client *Client) sendInternal(blocking bool, serverTime time.Time, tags Tags, prefix string, command string, params ...string) error { - // attach server time - if client.capabilities.Has(caps.ServerTime) { - if serverTime.IsZero() { - serverTime = time.Now() - } - tags = ensureTag(tags, "time", serverTime.UTC().Format(IRCv3TimestampFormat)) - } - - // send out the message - message := ircmsg.MakeMessage(tags, prefix, command, params...) - client.SendRawMessage(message, blocking) - return nil -} - // Send sends an IRC line to the client. -func (client *Client) Send(tags Tags, prefix string, command string, params ...string) error { - return client.sendInternal(false, time.Time{}, tags, prefix, command, params...) +func (client *Client) Send(tags map[string]string, prefix string, command string, params ...string) error { + msg := ircmsg.MakeMessage(tags, prefix, command, params...) + if client.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") { + msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) + } + return client.SendRawMessage(msg, false) } // Notice sends the client a notice from the server. diff --git a/irc/config.go b/irc/config.go index 2d1cf2f5..524c73c4 100644 --- a/irc/config.go +++ b/irc/config.go @@ -202,7 +202,6 @@ type OperConfig struct { // LineLenConfig controls line lengths. type LineLenLimits struct { - Tags int Rest int } @@ -553,9 +552,6 @@ func LoadConfig(filename string) (config *Config, err error) { } config.Server.WebIRC = newWebIRC // process limits - if config.Limits.LineLen.Tags < 512 { - config.Limits.LineLen.Tags = 512 - } if config.Limits.LineLen.Rest < 512 { config.Limits.LineLen.Rest = 512 } diff --git a/irc/handlers.go b/irc/handlers.go index 791a75e7..c2b42e8b 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -1874,7 +1874,7 @@ func nickHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp // NOTICE {,} func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - clientOnlyTags := utils.GetClientOnlyTags(msg.Tags) + clientOnlyTags := msg.ClientOnlyTags() targets := strings.Split(msg.Params[0], ",") message := msg.Params[1] @@ -1883,7 +1883,6 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re return false } - // split privmsg splitMsg := utils.MakeSplitMessage(message, !client.capabilities.Has(caps.MaxLine)) for i, targetString := range targets { @@ -1905,8 +1904,7 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // errors silently ignored with NOTICE as per RFC continue } - msgid := server.generateMessageID() - channel.SplitNotice(msgid, lowestPrefix, clientOnlyTags, client, splitMsg, rb) + channel.SendSplitMessage("NOTICE", lowestPrefix, clientOnlyTags, client, splitMsg, rb) } else { target, err := CasefoldName(targetString) if err != nil { @@ -1926,23 +1924,21 @@ func noticeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re if !user.capabilities.Has(caps.MessageTags) { clientOnlyTags = nil } - msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) if allowedPlusR && allowedTor { - user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "NOTICE", user.nick, splitMsg) + user.SendSplitMsgFromClient(client, clientOnlyTags, "NOTICE", user.nick, splitMsg) } nickMaskString := client.NickMaskString() accountName := client.AccountName() if client.capabilities.Has(caps.EchoMessage) { - rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "NOTICE", user.nick, splitMsg) + rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, "NOTICE", user.nick, splitMsg) } user.history.Add(history.Item{ Type: history.Notice, - Msgid: msgid, Message: splitMsg, Nick: nickMaskString, AccountName: accountName, @@ -2096,7 +2092,7 @@ func isRestrictedCTCPMessage(message string) bool { // PRIVMSG {,} func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - clientOnlyTags := utils.GetClientOnlyTags(msg.Tags) + clientOnlyTags := msg.ClientOnlyTags() targets := strings.Split(msg.Params[0], ",") message := msg.Params[1] @@ -2133,8 +2129,7 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) continue } - msgid := server.generateMessageID() - channel.SplitPrivMsg(msgid, lowestPrefix, clientOnlyTags, client, splitMsg, rb) + channel.SendSplitMessage("PRIVMSG", lowestPrefix, clientOnlyTags, client, splitMsg, rb) } else { target, err = CasefoldName(targetString) if service, isService := OragonoServices[target]; isService { @@ -2151,18 +2146,17 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R if !user.capabilities.Has(caps.MessageTags) { clientOnlyTags = nil } - msgid := server.generateMessageID() // restrict messages appropriately when +R is set // intentionally make the sending user think the message went through fine allowedPlusR := !user.HasMode(modes.RegisteredOnly) || client.LoggedIntoAccount() allowedTor := !user.isTor || !isRestrictedCTCPMessage(message) if allowedPlusR && allowedTor { - user.SendSplitMsgFromClient(msgid, client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) + user.SendSplitMsgFromClient(client, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) } nickMaskString := client.NickMaskString() accountName := client.AccountName() if client.capabilities.Has(caps.EchoMessage) { - rb.AddSplitMessageFromClient(msgid, nickMaskString, accountName, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) + rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, "PRIVMSG", user.nick, splitMsg) } if user.HasMode(modes.Away) { //TODO(dan): possibly implement cooldown of away notifications to users @@ -2171,7 +2165,6 @@ func privmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R user.history.Add(history.Item{ Type: history.Privmsg, - Msgid: msgid, Message: splitMsg, Nick: nickMaskString, AccountName: accountName, @@ -2357,7 +2350,7 @@ func setnameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R // TAGMSG {,} func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - clientOnlyTags := utils.GetClientOnlyTags(msg.Tags) + clientOnlyTags := msg.ClientOnlyTags() // no client-only tags, so we can drop it if clientOnlyTags == nil { return false @@ -2366,6 +2359,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re targets := strings.Split(msg.Params[0], ",") cnick := client.Nick() + message := utils.MakeSplitMessage("", true) // assign consistent message ID for i, targetString := range targets { // max of four targets per privmsg if i > maxTargets-1 { @@ -2390,9 +2384,7 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re rb.Add(nil, client.server.name, ERR_CANNOTSENDTOCHAN, channel.name, client.t("Cannot send to channel")) continue } - msgid := server.generateMessageID() - - channel.TagMsg(msgid, lowestPrefix, clientOnlyTags, client, rb) + channel.SendSplitMessage("TAGMSG", lowestPrefix, clientOnlyTags, client, message, rb) } else { target, err = CasefoldName(targetString) user := server.clients.Get(target) @@ -2402,19 +2394,19 @@ func tagmsgHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re } continue } - msgid := server.generateMessageID() // end user can't receive tagmsgs if !user.capabilities.Has(caps.MessageTags) { continue } - user.SendFromClient(msgid, client, clientOnlyTags, "TAGMSG", user.nick) + unick := user.Nick() + user.SendSplitMsgFromClient(client, clientOnlyTags, "TAGMSG", unick, message) if client.capabilities.Has(caps.EchoMessage) { - rb.AddFromClient(msgid, client.NickMaskString(), client.AccountName(), clientOnlyTags, "TAGMSG", user.nick) + rb.AddSplitMessageFromClient(client.NickMaskString(), client.AccountName(), clientOnlyTags, "TAGMSG", unick, message) } if user.HasMode(modes.Away) { //TODO(dan): possibly implement cooldown of away notifications to users - rb.Add(nil, server.name, RPL_AWAY, cnick, user.Nick(), user.AwayMessage()) + rb.Add(nil, server.name, RPL_AWAY, cnick, unick, user.AwayMessage()) } } } diff --git a/irc/history/history.go b/irc/history/history.go index c8c8acdc..520ba0bb 100644 --- a/irc/history/history.go +++ b/irc/history/history.go @@ -21,6 +21,7 @@ const ( Kick Quit Mode + Tagmsg ) // Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data @@ -33,14 +34,13 @@ type Item struct { AccountName string Message utils.SplitMessage // for non-privmsg items, we may stuff some other data in here - Msgid string } // HasMsgid tests whether a message has the message id `msgid`. func (item *Item) HasMsgid(msgid string) bool { // XXX we stuff other data in the Msgid field sometimes, // don't match it by accident - return (item.Type == Privmsg || item.Type == Notice) && item.Msgid == msgid + return (item.Type == Privmsg || item.Type == Notice) && item.Message.Msgid == msgid } type Predicate func(item Item) (matches bool) diff --git a/irc/idletimer.go b/irc/idletimer.go index 25a99b7e..8ca44cac 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -53,14 +53,17 @@ type IdleTimer struct { timer *time.Timer } -// NewIdleTimer sets up a new IdleTimer using constant timeouts. -func NewIdleTimer(client *Client) *IdleTimer { - it := IdleTimer{ - registerTimeout: RegisterTimeout, - client: client, - } +// Initialize sets up an IdleTimer and starts counting idle time; +// if there is no activity from the client, it will eventually be stopped. +func (it *IdleTimer) Initialize(client *Client) { + it.client = client + it.registerTimeout = RegisterTimeout it.idleTimeout, it.quitTimeout = it.recomputeDurations() - return &it + + it.Lock() + defer it.Unlock() + it.state = TimerUnregistered + it.resetTimeout() } // recomputeDurations recomputes the idle and quit durations, given the client's caps. @@ -81,15 +84,6 @@ func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duratio return } -// Start starts counting idle time; if there is no activity from the client, -// it will eventually be stopped. -func (it *IdleTimer) Start() { - it.Lock() - defer it.Unlock() - it.state = TimerUnregistered - it.resetTimeout() -} - func (it *IdleTimer) Touch() { idleTimeout, quitTimeout := it.recomputeDurations() diff --git a/irc/languages/languages.go b/irc/languages/languages.go index 34a33621..f0f76567 100644 --- a/irc/languages/languages.go +++ b/irc/languages/languages.go @@ -175,7 +175,7 @@ func (lm *Manager) Translators() []string { tlist = append(tlist, fmt.Sprintf("%s (%s): %s", info.Name, info.Code, info.Contributors)) } - sort.Sort(tlist) + tlist.Sort() return tlist } @@ -228,7 +228,7 @@ func (lm *Manager) Translate(languages []string, originalString string) string { } func (lm *Manager) CapValue() string { - langCodes := make([]string, len(lm.Languages)+1) + langCodes := make(sort.StringSlice, len(lm.Languages)+1) langCodes[0] = strconv.Itoa(len(lm.Languages)) i := 1 for _, info := range lm.Languages { @@ -239,5 +239,6 @@ func (lm *Manager) CapValue() string { langCodes[i] = codeToken i += 1 } + langCodes.Sort() return strings.Join(langCodes, ",") } diff --git a/irc/numerics.go b/irc/numerics.go index 05c4ff21..4557b61d 100644 --- a/irc/numerics.go +++ b/irc/numerics.go @@ -119,6 +119,7 @@ const ( ERR_NOTOPLEVEL = "413" ERR_WILDTOPLEVEL = "414" ERR_BADMASK = "415" + ERR_INPUTTOOLONG = "417" ERR_UNKNOWNCOMMAND = "421" ERR_NOMOTD = "422" ERR_NOADMININFO = "423" diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 183a7fe7..bb95a198 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -32,7 +32,8 @@ type ResponseBuffer struct { // GetLabel returns the label from the given message. func GetLabel(msg ircmsg.IrcMessage) string { - return msg.Tags[caps.LabelTagName].Value + _, value := msg.GetTag(caps.LabelTagName) + return value } // NewResponseBuffer returns a new ResponseBuffer. @@ -42,8 +43,7 @@ func NewResponseBuffer(target *Client) *ResponseBuffer { } } -// Add adds a standard new message to our queue. -func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, command string, params ...string) { +func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) { if rb.finalized { rb.target.server.logger.Error("internal", "message added to finalized ResponseBuffer, undefined behavior") debug.PrintStack() @@ -52,33 +52,38 @@ func (rb *ResponseBuffer) Add(tags *map[string]ircmsg.TagValue, prefix string, c return } - message := ircmsg.MakeMessage(tags, prefix, command, params...) - rb.messages = append(rb.messages, message) + rb.messages = append(rb.messages, msg) +} + +// Add adds a standard new message to our queue. +func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command string, params ...string) { + rb.AddMessage(ircmsg.MakeMessage(tags, prefix, command, params...)) } // AddFromClient adds a new message from a specific client to our queue. -func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, params ...string) { +func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { + msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) + msg.UpdateTags(tags) + // attach account-tag - if rb.target.capabilities.Has(caps.AccountTag) { - if fromAccount != "*" { - tags = ensureTag(tags, "account", fromAccount) - } + if rb.target.capabilities.Has(caps.AccountTag) && fromAccount != "*" { + msg.SetTag("account", fromAccount) } // attach message-id if len(msgid) > 0 && rb.target.capabilities.Has(caps.MessageTags) { - tags = ensureTag(tags, "draft/msgid", msgid) + msg.SetTag("draft/msgid", msgid) } - rb.Add(tags, fromNickMask, command, params...) + rb.AddMessage(msg) } // AddSplitMessageFromClient adds a new split message from a specific client to our queue. -func (rb *ResponseBuffer) AddSplitMessageFromClient(msgid string, fromNickMask string, fromAccount string, tags *map[string]ircmsg.TagValue, command string, target string, message utils.SplitMessage) { +func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) { if rb.target.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { - rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, message.Original) + rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) } else { - for _, str := range message.Wrapped { - rb.AddFromClient(msgid, fromNickMask, fromAccount, tags, command, target, str) + for _, messagePair := range message.Wrapped { + rb.AddFromClient(messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message) } } } @@ -103,7 +108,7 @@ func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) { message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType) if rb.Label != "" { - message.Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label) + message.SetTag(caps.LabelTagName, rb.Label) } rb.target.SendRawMessage(message, blocking) } @@ -149,7 +154,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { // if label but no batch, add label to first message if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" { - rb.messages[0].Tags[caps.LabelTagName] = ircmsg.MakeTagValue(rb.Label) + rb.messages[0].SetTag(caps.LabelTagName, rb.Label) } else if useBatch { rb.sendBatchStart(defaultBatchType, blocking) } @@ -157,16 +162,13 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { // send each message out for _, message := range rb.messages { // attach server-time if needed - if rb.target.capabilities.Has(caps.ServerTime) { - if !message.Tags["time"].HasValue { - t := time.Now().UTC().Format(IRCv3TimestampFormat) - message.Tags["time"] = ircmsg.MakeTagValue(t) - } + if rb.target.capabilities.Has(caps.ServerTime) && !message.HasTag("time") { + message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } // attach batch ID if rb.batchID != "" { - message.Tags["batch"] = ircmsg.MakeTagValue(rb.batchID) + message.SetTag("batch", rb.batchID) } // send message out diff --git a/irc/server.go b/irc/server.go index 8c2c77ff..4b0d66aa 100644 --- a/irc/server.go +++ b/irc/server.go @@ -387,11 +387,6 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b return &wrapper, nil } -// generateMessageID returns a network-unique message ID. -func (server *Server) generateMessageID() string { - return utils.GenerateSecretToken() -} - // // server functionality // @@ -623,7 +618,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { } else { // enforce configs that can't be changed after launch: currentLimits := server.Limits() - if currentLimits.LineLen.Tags != config.Limits.LineLen.Tags || currentLimits.LineLen.Rest != config.Limits.LineLen.Rest { + if currentLimits.LineLen.Rest != config.Limits.LineLen.Rest { return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted") } else if server.name != config.Server.Name { return fmt.Errorf("Server name cannot be changed after launching the server, rehash aborted") @@ -703,9 +698,9 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { } // MaxLine - if config.Limits.LineLen.Tags != 512 || config.Limits.LineLen.Rest != 512 { + if config.Limits.LineLen.Rest != 512 { SupportedCapabilities.Enable(caps.MaxLine) - value := fmt.Sprintf("%d,%d", config.Limits.LineLen.Tags, config.Limits.LineLen.Rest) + value := fmt.Sprintf("%d", config.Limits.LineLen.Rest) CapValues.Set(caps.MaxLine, value) } diff --git a/irc/socket.go b/irc/socket.go index ae46f72f..97c7b66e 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -20,6 +20,8 @@ import ( var ( handshakeTimeout, _ = time.ParseDuration("5s") errSendQExceeded = errors.New("SendQ exceeded") + + sendQExceededMessage = []byte("\r\nERROR :SendQ Exceeded\r\n") ) // Socket represents an IRC socket. @@ -38,7 +40,7 @@ type Socket struct { totalLength int closed bool sendQExceeded bool - finalData string // what to send when we die + finalData []byte // what to send when we die finalized bool } @@ -196,7 +198,7 @@ func (socket *Socket) wakeWriter() { } // SetFinalData sets the final data to send when the SocketWriter closes. -func (socket *Socket) SetFinalData(data string) { +func (socket *Socket) SetFinalData(data []byte) { socket.Lock() defer socket.Unlock() socket.finalData = data @@ -271,7 +273,7 @@ func (socket *Socket) finalize() { socket.finalized = true finalData := socket.finalData if socket.sendQExceeded { - finalData = "\r\nERROR :SendQ Exceeded\r\n" + finalData = sendQExceededMessage } socket.Unlock() @@ -279,8 +281,8 @@ func (socket *Socket) finalize() { return } - if finalData != "" { - socket.conn.Write([]byte(finalData)) + if len(finalData) != 0 { + socket.conn.Write(finalData) } // close the connection diff --git a/irc/types.go b/irc/types.go index 9ebc23f5..9ef6b51a 100644 --- a/irc/types.go +++ b/irc/types.go @@ -6,7 +6,6 @@ package irc import "github.com/oragono/oragono/irc/modes" -import "github.com/goshuirc/irc-go/ircmsg" // ClientSet is a set of clients. type ClientSet map[*Client]bool @@ -57,5 +56,3 @@ func (members MemberSet) AnyHasMode(mode modes.Mode) bool { // ChannelSet is a set of channels. type ChannelSet map[*Channel]bool - -type Tags *map[string]ircmsg.TagValue diff --git a/irc/utils/message_tags.go b/irc/utils/message_tags.go deleted file mode 100644 index d2df94b5..00000000 --- a/irc/utils/message_tags.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2016-2017 Daniel Oaks -// released under the MIT license - -package utils - -import "github.com/goshuirc/irc-go/ircmsg" - -// GetClientOnlyTags takes a tag map and returns a map containing just the client-only tags from it. -func GetClientOnlyTags(tags map[string]ircmsg.TagValue) *map[string]ircmsg.TagValue { - if len(tags) < 1 { - return nil - } - - clientOnlyTags := make(map[string]ircmsg.TagValue) - - for name, value := range tags { - if len(name) > 1 && name[0] == '+' { - clientOnlyTags[name] = value - } - } - - if len(clientOnlyTags) < 1 { - return nil - } - - return &clientOnlyTags -} diff --git a/irc/utils/text.go b/irc/utils/text.go index 4b292341..e216d55c 100644 --- a/irc/utils/text.go +++ b/irc/utils/text.go @@ -50,17 +50,32 @@ func WordWrap(text string, lineWidth int) []string { return lines } -// SplitMessage represents a message that's been split for sending. -type SplitMessage struct { - Original string - Wrapped []string // if this is nil, Original didn't need wrapping and can be sent to anyone +type MessagePair struct { + Message string + Msgid string } -func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) { - result.Original = original +// SplitMessage represents a message that's been split for sending. +type SplitMessage struct { + MessagePair + Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone +} - if !origIs512 { - result.Wrapped = WordWrap(original, 400) +const defaultLineWidth = 400 + +func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) { + result.Message = original + result.Msgid = GenerateSecretToken() + + if !origIs512 && defaultLineWidth < len(original) { + wrapped := WordWrap(original, defaultLineWidth) + result.Wrapped = make([]MessagePair, len(wrapped)) + for i, wrappedMessage := range wrapped { + result.Wrapped[i] = MessagePair{ + Message: wrappedMessage, + Msgid: GenerateSecretToken(), + } + } } return diff --git a/oragono.yaml b/oragono.yaml index 3ee3773a..2c526b3e 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -487,10 +487,8 @@ limits: # maximum length of IRC lines # this should generally be 1024-2048, and will only apply when negotiated by clients linelen: - # tags section - tags: 2048 - - # rest of the message + # ratified version of the message-tags cap fixes the max tag length at 8191 bytes + # configurable length for the rest of the message: rest: 2048 # fakelag: prevents clients from spamming commands too rapidly diff --git a/vendor b/vendor index 72043bab..8ddbb531 160000 --- a/vendor +++ b/vendor @@ -1 +1 @@ -Subproject commit 72043bab39044196e57bff1fe5e59c9ec81e59f3 +Subproject commit 8ddbb531841add50f8b7aff8fe00bef311448aaa