From b11bf503e796b0c1b3c0885953e2e107f49f7504 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 6 May 2019 23:17:57 -0400 Subject: [PATCH 1/6] improvements to message replay code --- gencapdefs.py | 6 ++ irc/caps/defs.go | 7 +- irc/channel.go | 160 ++++++++++++++++++++++++------------ irc/client.go | 54 +++++++----- irc/handlers.go | 49 ++++++----- irc/history/history.go | 55 ++++++++++--- irc/history/history_test.go | 42 ++++------ irc/nickname.go | 25 ++++-- irc/responsebuffer.go | 102 ++++++++++++++++++----- irc/utils/text.go | 3 + 10 files changed, 338 insertions(+), 165 deletions(-) diff --git a/gencapdefs.py b/gencapdefs.py index 3dcbdd19..a6baee1b 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -159,6 +159,12 @@ CAPDEFS = [ url="https://wiki.znc.in/Query_buffers", standard="ZNC vendor", ), + CapDef( + identifier="EventPlayback", + name="draft/event-playback", + url="https://github.com/ircv3/ircv3-specifications/pull/362", + standard="Draft IRCv3", + ), ] def validate_defs(): diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 9a3fc94c..9f10f0ae 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -7,7 +7,7 @@ package caps const ( // number of recognized capabilities: - numCapabs = 24 + numCapabs = 25 // length of the uint64 array that represents the bitset: bitsetLen = 1 ) @@ -108,6 +108,10 @@ const ( // ZNCSelfMessage is the ZNC vendor capability named "znc.in/self-message": // https://wiki.znc.in/Query_buffers ZNCSelfMessage Capability = iota + + // EventPlayback is the Draft IRCv3 capability named "draft/event-playback": + // https://github.com/ircv3/ircv3-specifications/pull/362 + EventPlayback Capability = iota ) // `capabilityNames[capab]` is the string name of the capability `capab` @@ -137,5 +141,6 @@ var ( "userhost-in-names", "oragono.io/bnc", "znc.in/self-message", + "draft/event-playback", } ) diff --git a/irc/channel.go b/irc/channel.go index 6bd15f4b..28315735 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -536,6 +536,8 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp client.server.logger.Debug("join", fmt.Sprintf("%s joined channel %s", details.nick, chname)) + var message utils.SplitMessage + givenMode := func() (givenMode modes.Mode) { channel.joinPartMutex.Lock() defer channel.joinPartMutex.Unlock() @@ -559,14 +561,15 @@ 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{ + message = utils.MakeSplitMessage("", true) + histItem := history.Item{ Type: history.Join, Nick: details.nickMask, AccountName: details.accountName, Message: message, - }) + } + histItem.Params[0] = details.realname + channel.history.Add(histItem) return }() @@ -587,9 +590,10 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp continue } if session.capabilities.Has(caps.ExtendedJoin) { - session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) } else { session.Send(nil, details.nickMask, "JOIN", chname) + session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) } if givenMode != 0 { session.Send(nil, client.server.name, "MODE", chname, modestr, details.nick) @@ -598,9 +602,9 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp } if rb.session.capabilities.Has(caps.ExtendedJoin) { - rb.Add(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) } else { - rb.Add(nil, details.nickMask, "JOIN", chname) + rb.AddFromClient(message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) } if rb.session.client == client { @@ -613,10 +617,13 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp rb.Flush(true) replayLimit := channel.server.Config().History.AutoreplayOnJoin - if replayLimit > 0 { + if 0 < replayLimit { + // TODO don't replay the client's own JOIN line? items := channel.history.Latest(replayLimit) - channel.replayHistoryItems(rb, items) - rb.Flush(true) + if 0 < len(items) { + channel.replayHistoryItems(rb, items, true) + rb.Flush(true) + } } } @@ -647,14 +654,16 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) channel.Quit(client) + splitMessage := utils.MakeSplitMessage(message, true) + details := client.Details() for _, member := range channel.Members() { - member.Send(nil, details.nickMask, "PART", chname, message) + member.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) } - rb.Add(nil, details.nickMask, "PART", chname, message) + rb.AddFromClient(splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) for _, session := range client.Sessions() { if session != rb.session { - session.Send(nil, details.nickMask, "PART", chname, message) + session.sendFromClientInternal(false, splitMessage.Time, splitMessage.Msgid, details.nickMask, details.accountName, nil, "PART", chname, message) } } @@ -662,7 +671,7 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) Type: history.Part, Nick: details.nickMask, AccountName: details.accountName, - Message: utils.MakeSplitMessage(message, true), + Message: splitMessage, }) client.server.logger.Debug("part", fmt.Sprintf("%s left channel %s", details.nick, chname)) @@ -748,7 +757,7 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) { items, complete := channel.history.Between(after, before, false, 0) rb := NewResponseBuffer(newClient.Sessions()[0]) - channel.replayHistoryItems(rb, items) + channel.replayHistoryItems(rb, items, false) if !complete && !newClient.resumeDetails.HistoryIncomplete { // warn here if we didn't warn already rb.Add(nil, "HistServ", "NOTICE", channel.Name(), newClient.t("Some additional message history may have been lost")) @@ -759,50 +768,93 @@ func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Tim func stripMaskFromNick(nickMask string) (nick string) { index := strings.Index(nickMask, "!") if index == -1 { - return + return nickMask } return nickMask[0:index] } -func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item) { +// munge the msgid corresponding to a replayable event, +// yielding a consistent msgid for the fake PRIVMSG from HistServ +func mungeMsgidForHistserv(token string) (result string) { + return fmt.Sprintf("_%s", token) +} + +func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) { chname := channel.Name() client := rb.target - serverTime := rb.session.capabilities.Has(caps.ServerTime) + eventPlayback := rb.session.capabilities.Has(caps.EventPlayback) + extendedJoin := rb.session.capabilities.Has(caps.ExtendedJoin) + + if len(items) == 0 { + return + } + batchID := rb.StartNestedHistoryBatch(chname) + defer rb.EndNestedBatch(batchID) for _, item := range items { - var tags map[string]string - if serverTime { - tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)} - } - - // TODO(#437) support history.Tagmsg + nick := stripMaskFromNick(item.Nick) switch item.Type { case history.Privmsg: - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "PRIVMSG", chname, item.Message) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "PRIVMSG", chname, item.Message) case history.Notice: - rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, "NOTICE", chname, item.Message) - case history.Join: - nick := stripMaskFromNick(item.Nick) - var message string - if item.AccountName == "*" { - message = fmt.Sprintf(client.t("%s joined the channel"), nick) - } else { - message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "NOTICE", chname, item.Message) + case history.Tagmsg: + if rb.session.capabilities.Has(caps.MessageTags) { + rb.AddSplitMessageFromClient(item.Nick, item.AccountName, item.Tags, "TAGMSG", chname, item.Message) + } + case history.Join: + if eventPlayback { + if extendedJoin { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname, item.AccountName, item.Params[0]) + } else { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "JOIN", chname) + } + } else { + if autoreplay { + continue // #474 + } + var message string + if item.AccountName == "*" { + message = fmt.Sprintf(client.t("%s joined the channel"), nick) + } else { + message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) + } + rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } - 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.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.Message) - rb.Add(tags, "HistServ", "PRIVMSG", chname, message) + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "PART", chname, item.Message.Message) + } else { + if autoreplay { + continue // #474 + } + message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) + rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "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.Message.Msgid, item.Message.Message) - rb.Add(tags, "HistServ", "PRIVMSG", chname, message) + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message) + } else { + message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message) + rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + } + case history.Quit: + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "QUIT", item.Message.Message) + } else { + if autoreplay { + continue // #474 + } + message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) + rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + } + case history.Nick: + if eventPlayback { + rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0]) + } else { + message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) + rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + } } } } @@ -934,7 +986,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod tagsToUse = clientOnlyTags } if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { - rb.AddFromClient(message.Msgid, nickmask, account, tagsToUse, command, chname) + rb.AddFromClient(message.Time, message.Msgid, nickmask, account, tagsToUse, command, chname) } else { rb.AddSplitMessageFromClient(nickmask, account, tagsToUse, command, chname, message) } @@ -986,7 +1038,7 @@ func (channel *Channel) SendSplitMessage(command string, minPrefixMode modes.Mod Message: message, Nick: nickmask, AccountName: account, - Time: now, + Tags: clientOnlyTags, }) } @@ -1110,27 +1162,29 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb comment = comment[:kicklimit] } + message := utils.MakeSplitMessage(comment, true) clientMask := client.NickMaskString() + clientAccount := client.AccountName() + targetNick := target.Nick() chname := channel.Name() for _, member := range channel.Members() { for _, session := range member.Sessions() { if session != rb.session { - session.Send(nil, clientMask, "KICK", chname, targetNick, comment) + session.sendFromClientInternal(false, message.Time, message.Msgid, clientMask, clientAccount, nil, "KICK", chname, targetNick, comment) } } } rb.Add(nil, clientMask, "KICK", chname, targetNick, comment) - message := utils.SplitMessage{} - message.Message = comment - message.Msgid = targetNick // XXX abuse this field - channel.history.Add(history.Item{ + histItem := history.Item{ Type: history.Kick, Nick: clientMask, AccountName: target.AccountName(), Message: message, - }) + } + histItem.Params[0] = targetNick + channel.history.Add(histItem) channel.Quit(target) } diff --git a/irc/client.go b/irc/client.go index e7cb33a1..73c0bfb6 100644 --- a/irc/client.go +++ b/irc/client.go @@ -487,7 +487,7 @@ func (client *Client) tryResume() (success bool) { } } privmsgMatcher := func(item history.Item) bool { - return item.Type == history.Privmsg || item.Type == history.Notice + return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg } privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0) lastDiscarded := oldClient.history.LastDiscarded() @@ -495,8 +495,7 @@ func (client *Client) tryResume() (success bool) { oldestLostMessage = lastDiscarded } for _, item := range privmsgHistory { - // TODO this is the nickmask, fix that - sender := server.clients.Get(item.Nick) + sender := server.clients.Get(stripMaskFromNick(item.Nick)) if sender != nil { friends.Add(sender) } @@ -561,8 +560,13 @@ func (client *Client) tryResumeChannels() { } func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { + var batchID string nick := client.Nick() - serverTime := rb.session.capabilities.Has(caps.ServerTime) + if 0 < len(items) { + batchID = rb.StartNestedHistoryBatch(nick) + } + + allowTags := rb.session.capabilities.Has(caps.MessageTags) for _, item := range items { var command string switch item.Type { @@ -570,15 +574,23 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I command = "PRIVMSG" case history.Notice: command = "NOTICE" + case history.Tagmsg: + if allowTags { + command = "TAGMSG" + } else { + continue + } default: continue } var tags map[string]string - if serverTime { - tags = map[string]string{"time": item.Time.Format(IRCv3TimestampFormat)} + if allowTags { + tags = item.Tags } rb.AddSplitMessageFromClient(item.Nick, item.AccountName, tags, command, nick, item.Message) } + + rb.EndNestedBatch(batchID) if !complete { rb.Add(nil, "HistServ", "NOTICE", nick, client.t("Some additional message history may have been lost")) } @@ -934,19 +946,21 @@ func (client *Client) destroy(beingResumed bool, session *Session) { return } + details := client.Details() + // see #235: deduplicating the list of PART recipients uses (comparatively speaking) // a lot of RAM, so limit concurrency to avoid thrashing client.server.semaphores.ClientDestroy.Acquire() defer client.server.semaphores.ClientDestroy.Release() if beingResumed { - client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", client.nick)) + client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick)) } else { - client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick)) + client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick)) } if !beingResumed { - client.server.whoWas.Append(client.WhoWas()) + client.server.whoWas.Append(details.WhoWas) } // remove from connection limits @@ -963,6 +977,7 @@ func (client *Client) destroy(beingResumed bool, session *Session) { // clean up monitor state client.server.monitorManager.RemoveAll(client) + splitQuitMessage := utils.MakeSplitMessage(quitMessage, true) // clean up channels friends := make(ClientSet) for _, channel := range client.Channels() { @@ -972,7 +987,7 @@ func (client *Client) destroy(beingResumed bool, session *Session) { Type: history.Quit, Nick: nickMaskString, AccountName: accountName, - Message: utils.MakeSplitMessage(quitMessage, true), + Message: splitQuitMessage, }) } for _, member := range channel.Members() { @@ -1007,14 +1022,14 @@ func (client *Client) destroy(beingResumed bool, session *Session) { if quitMessage == "" { quitMessage = "Exited" } - friend.Send(nil, client.nickMaskString, "QUIT", quitMessage) + friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage) } } if !client.exitedSnomaskSent { if beingResumed { client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick)) } else { - client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick)) + client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick)) } } } @@ -1031,15 +1046,7 @@ func (session *Session) sendSplitMsgFromClientInternal(blocking bool, serverTime } } -// 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 map[string]string, command string, params ...string) error { - return client.sendFromClientInternal(false, time.Time{}, msgid, from.NickMaskString(), from.AccountName(), tags, command, params...) -} - -// 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 +// Sends a line with `nickmask` as the prefix, adding `time` and `account` tags if supported func (client *Client) sendFromClientInternal(blocking bool, serverTime time.Time, msgid string, nickmask, accountName string, tags map[string]string, command string, params ...string) (err error) { for _, session := range client.Sessions() { err_ := session.sendFromClientInternal(blocking, serverTime, msgid, nickmask, accountName, tags, command, params...) @@ -1062,7 +1069,10 @@ func (session *Session) sendFromClientInternal(blocking bool, serverTime time.Ti } // attach server-time if session.capabilities.Has(caps.ServerTime) { - msg.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) + if serverTime.IsZero() { + serverTime = time.Now().UTC() + } + msg.SetTag("time", serverTime.Format(IRCv3TimestampFormat)) } return session.SendRawMessage(msg, blocking) diff --git a/irc/handlers.go b/irc/handlers.go index de67ccfd..5194a201 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -585,36 +585,36 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo // e.g., CHATHISTORY #ircv3 BETWEEN timestamp=YYYY-MM-DDThh:mm:ss.sssZ timestamp=YYYY-MM-DDThh:mm:ss.sssZ + 100 func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) (exiting bool) { config := server.Config() - // batch type is chathistory; send an empty batch if necessary - rb.InitializeBatch("chathistory", true) var items []history.Item success := false var hist *history.Buffer var channel *Channel defer func() { - if success { + // successful responses are sent as a chathistory or history batch + if success && 0 < len(items) { + batchType := "chathistory" + if rb.session.capabilities.Has(caps.EventPlayback) { + batchType = "history" + } + rb.ForceBatchStart(batchType, true) if channel == nil { client.replayPrivmsgHistory(rb, items, true) } else { - channel.replayHistoryItems(rb, items) + channel.replayHistoryItems(rb, items, false) } - } - rb.Send(true) // terminate the chathistory batch - if success && len(items) > 0 { return } - newRb := NewResponseBuffer(rb.session) - newRb.Label = rb.Label // same label, new batch + + // errors are sent either without a batch, or in a draft/labeled-response batch as usual // TODO: send `WARN CHATHISTORY MAX_MESSAGES_EXCEEDED` when appropriate if hist == nil { - newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL") + rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_SUCH_CHANNEL") } else if len(items) == 0 { - newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND") + rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NO_TEXT_TO_SEND") } else if !success { - newRb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS") + rb.Add(nil, server.name, "ERR", "CHATHISTORY", "NEED_MORE_PARAMS") } - newRb.Send(true) }() target := msg.Params[0] @@ -744,7 +744,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r } } else { matches = func(item history.Item) bool { - return before == item.Time.Before(timestamp) + return before == item.Message.Time.Before(timestamp) } } items = hist.Match(matches, !before, limit) @@ -767,7 +767,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r } } else { matches = func(item history.Item) bool { - return item.Time.After(timestamp) + return item.Message.Time.After(timestamp) } } items = hist.Match(matches, false, limit) @@ -790,16 +790,16 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r } } else { initialMatcher = func(item history.Item) (result bool) { - return item.Time.Before(timestamp) + return item.Message.Time.Before(timestamp) } } var halfLimit int halfLimit = (limit + 1) / 2 firstPass := hist.Match(initialMatcher, false, halfLimit) if len(firstPass) > 0 { - timeWindowStart := firstPass[0].Time + timeWindowStart := firstPass[0].Message.Time items = hist.Match(func(item history.Item) bool { - return item.Time.Equal(timeWindowStart) || item.Time.After(timeWindowStart) + return item.Message.Time.Equal(timeWindowStart) || item.Message.Time.After(timeWindowStart) }, true, limit) } success = true @@ -1109,7 +1109,7 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R items := hist.Latest(limit) if channel != nil { - channel.replayHistoryItems(rb, items) + channel.replayHistoryItems(rb, items, false) } else { client.replayPrivmsgHistory(rb, items, true) } @@ -1960,7 +1960,6 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R for i, targetString := range targets { // each target gets distinct msgids splitMsg := utils.MakeSplitMessage(message, !rb.session.capabilities.Has(caps.MaxLine)) - now := time.Now().UTC() // max of four targets per privmsg if i > maxTargets-1 { @@ -2009,17 +2008,17 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R if histType == history.Tagmsg { // don't send TAGMSG at all if they don't have the tags cap if session.capabilities.Has(caps.MessageTags) { - session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) + session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) } } else { - session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) + session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) } } } // an echo-message may need to be included in the response: if rb.session.capabilities.Has(caps.EchoMessage) { if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { - rb.AddFromClient(splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) + rb.AddFromClient(splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) } else { rb.AddSplitMessageFromClient(nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) } @@ -2030,9 +2029,9 @@ func messageHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R continue } if histType == history.Tagmsg && rb.session.capabilities.Has(caps.MessageTags) { - session.sendFromClientInternal(false, now, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) + session.sendFromClientInternal(false, splitMsg.Time, splitMsg.Msgid, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick) } else { - session.sendSplitMsgFromClientInternal(false, now, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) + session.sendSplitMsgFromClientInternal(false, splitMsg.Time, nickMaskString, accountName, clientOnlyTags, msg.Command, tnick, splitMsg) } } if histType != history.Notice && user.Away() { diff --git a/irc/history/history.go b/irc/history/history.go index 520ba0bb..3a3655bb 100644 --- a/irc/history/history.go +++ b/irc/history/history.go @@ -22,25 +22,52 @@ const ( Quit Mode Tagmsg + Nick ) +// a Tagmsg that consists entirely of junk tags is not stored +var junkTags = map[string]bool{ + "+draft/typing": true, + "+typing": true, // future-proofing +} + // Item represents an event (e.g., a PRIVMSG or a JOIN) and its associated data type Item struct { Type ItemType - Time time.Time Nick string // this is the uncasefolded account name, if there's no account it should be set to "*" AccountName string - Message utils.SplitMessage // for non-privmsg items, we may stuff some other data in here + Message utils.SplitMessage + Tags map[string]string + Params [1]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.Message.Msgid == msgid + if item.Message.Msgid == msgid { + return true + } + for _, pair := range item.Message.Wrapped { + if pair.Msgid == msgid { + return true + } + } + return false +} + +func (item *Item) isStorable() bool { + if item.Type == Tagmsg { + for name := range item.Tags { + if !junkTags[name] { + return true + } + } + return false // all tags were blacklisted + } else { + return true + } } type Predicate func(item Item) (matches bool) @@ -94,8 +121,12 @@ func (list *Buffer) Add(item Item) { return } - if item.Time.IsZero() { - item.Time = time.Now().UTC() + if !item.isStorable() { + return + } + + if item.Message.Time.IsZero() { + item.Message.Time = time.Now().UTC() } list.Lock() @@ -114,8 +145,8 @@ func (list *Buffer) Add(item Item) { list.end = (list.end + 1) % len(list.buffer) list.start = list.end // advance start as well, overwriting first entry // record the timestamp of the overwritten item - if list.lastDiscarded.Before(list.buffer[pos].Time) { - list.lastDiscarded = list.buffer[pos].Time + if list.lastDiscarded.Before(list.buffer[pos].Message.Time) { + list.lastDiscarded = list.buffer[pos].Message.Time } } @@ -144,7 +175,7 @@ func (list *Buffer) Between(after, before time.Time, ascending bool, limit int) complete = after.Equal(list.lastDiscarded) || after.After(list.lastDiscarded) satisfies := func(item Item) bool { - return (after.IsZero() || item.Time.After(after)) && (before.IsZero() || item.Time.Before(before)) + return (after.IsZero() || item.Message.Time.After(after)) && (before.IsZero() || item.Message.Time.Before(before)) } return list.matchInternal(satisfies, ascending, limit), complete @@ -264,8 +295,8 @@ func (list *Buffer) Resize(size int) { } // update lastDiscarded for discarded entries for i := list.start; i != start; i = (i + 1) % len(list.buffer) { - if list.lastDiscarded.Before(list.buffer[i].Time) { - list.lastDiscarded = list.buffer[i].Time + if list.lastDiscarded.Before(list.buffer[i].Message.Time) { + list.lastDiscarded = list.buffer[i].Message.Time } } } diff --git a/irc/history/history_test.go b/irc/history/history_test.go index c5ba4594..451f331b 100644 --- a/irc/history/history_test.go +++ b/irc/history/history_test.go @@ -87,6 +87,12 @@ func easyParse(timestamp string) time.Time { return result } +func easyItem(nick string, timestamp string) (result Item) { + result.Message.Time = easyParse(timestamp) + result.Nick = nick + return +} + func assertEqual(supplied, expected interface{}, t *testing.T) { if !reflect.DeepEqual(supplied, expected) { t.Errorf("expected %v but got %v", expected, supplied) @@ -97,30 +103,19 @@ func TestBuffer(t *testing.T) { start := easyParse("2006-01-01 00:00:00Z") buf := NewHistoryBuffer(3) - buf.Add(Item{ - Nick: "testnick0", - Time: easyParse("2006-01-01 15:04:05Z"), - }) + buf.Add(easyItem("testnick0", "2006-01-01 15:04:05Z")) - buf.Add(Item{ - Nick: "testnick1", - Time: easyParse("2006-01-02 15:04:05Z"), - }) + buf.Add(easyItem("testnick1", "2006-01-02 15:04:05Z")) - buf.Add(Item{ - Nick: "testnick2", - Time: easyParse("2006-01-03 15:04:05Z"), - }) + buf.Add(easyItem("testnick2", "2006-01-03 15:04:05Z")) since, complete := buf.Between(start, time.Now(), false, 0) assertEqual(complete, true, t) assertEqual(toNicks(since), []string{"testnick0", "testnick1", "testnick2"}, t) // add another item, evicting the first - buf.Add(Item{ - Nick: "testnick3", - Time: easyParse("2006-01-04 15:04:05Z"), - }) + buf.Add(easyItem("testnick3", "2006-01-04 15:04:05Z")) + since, complete = buf.Between(start, time.Now(), false, 0) assertEqual(complete, false, t) assertEqual(toNicks(since), []string{"testnick1", "testnick2", "testnick3"}, t) @@ -139,18 +134,9 @@ func TestBuffer(t *testing.T) { assertEqual(toNicks(since), []string{"testnick2", "testnick3"}, t) buf.Resize(5) - buf.Add(Item{ - Nick: "testnick4", - Time: easyParse("2006-01-05 15:04:05Z"), - }) - buf.Add(Item{ - Nick: "testnick5", - Time: easyParse("2006-01-06 15:04:05Z"), - }) - buf.Add(Item{ - Nick: "testnick6", - Time: easyParse("2006-01-07 15:04:05Z"), - }) + buf.Add(easyItem("testnick4", "2006-01-05 15:04:05Z")) + buf.Add(easyItem("testnick5", "2006-01-06 15:04:05Z")) + buf.Add(easyItem("testnick6", "2006-01-07 15:04:05Z")) since, complete = buf.Between(easyParse("2006-01-03 00:00:00Z"), time.Now(), false, 0) assertEqual(complete, true, t) assertEqual(toNicks(since), []string{"testnick2", "testnick3", "testnick4", "testnick5", "testnick6"}, t) diff --git a/irc/nickname.go b/irc/nickname.go index 611d88b5..c9098679 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -11,7 +11,9 @@ import ( "strings" "github.com/goshuirc/irc-go/ircfmt" + "github.com/oragono/oragono/irc/history" "github.com/oragono/oragono/irc/sno" + "github.com/oragono/oragono/irc/utils" ) var ( @@ -44,7 +46,7 @@ func performNickChange(server *Server, client *Client, target *Client, session * hadNick := target.HasNick() origNickMask := target.NickMaskString() - whowas := target.WhoWas() + details := target.Details() err = client.server.clients.SetNick(target, session, nickname) if err == errNicknameInUse { rb.Add(nil, server.name, ERR_NICKNAMEINUSE, currentNick, nickname, client.t("Nickname is already in use")) @@ -57,18 +59,31 @@ func performNickChange(server *Server, client *Client, target *Client, session * return false } + message := utils.MakeSplitMessage("", true) + histItem := history.Item{ + Type: history.Nick, + Nick: origNickMask, + AccountName: details.accountName, + Message: message, + } + histItem.Params[0] = nickname + client.server.logger.Debug("nick", fmt.Sprintf("%s changed nickname to %s [%s]", origNickMask, nickname, cfnick)) if hadNick { - target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), whowas.nick, nickname)) - target.server.whoWas.Append(whowas) - rb.Add(nil, origNickMask, "NICK", nickname) + target.server.snomasks.Send(sno.LocalNicks, fmt.Sprintf(ircfmt.Unescape("$%s$r changed nickname to %s"), details.nick, nickname)) + target.server.whoWas.Append(details.WhoWas) + rb.AddFromClient(message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname) for session := range target.Friends() { if session != rb.session { - session.Send(nil, origNickMask, "NICK", nickname) + session.sendFromClientInternal(false, message.Time, message.Msgid, origNickMask, details.accountName, nil, "NICK", nickname) } } } + for _, channel := range client.Channels() { + channel.history.Add(histItem) + } + target.nickTimer.Touch(rb) if target.Registered() { diff --git a/irc/responsebuffer.go b/irc/responsebuffer.go index 16d4613d..3b7b0bcb 100644 --- a/irc/responsebuffer.go +++ b/irc/responsebuffer.go @@ -23,8 +23,19 @@ const ( // buffer will silently create a batch if required and label the outgoing messages as // necessary (or leave it off and simply tag the outgoing message). type ResponseBuffer struct { - Label string - batchID string + Label string // label if this is a labeled response batch + batchID string // ID of the labeled response batch, if one has been initiated + batchType string // type of the labeled response batch (possibly `history` or `chathistory`) + + // stack of batch IDs of nested batches, which are handled separately + // from the underlying labeled-response batch. starting a new nested batch + // unconditionally enqueues its batch start message; subsequent messages + // are tagged with the nested batch ID, until nested batch end. + // (the nested batch start itself may have no batch tag, or the batch tag of the + // underlying labeled-response batch, or the batch tag of the next outermost + // nested batch.) + nestedBatches []string + messages []ircmsg.IrcMessage finalized bool target *Client @@ -40,8 +51,9 @@ func GetLabel(msg ircmsg.IrcMessage) string { // NewResponseBuffer returns a new ResponseBuffer. func NewResponseBuffer(session *Session) *ResponseBuffer { return &ResponseBuffer{ - session: session, - target: session.client, + session: session, + target: session.client, + batchType: defaultBatchType, } } @@ -54,6 +66,9 @@ func (rb *ResponseBuffer) AddMessage(msg ircmsg.IrcMessage) { return } + if 0 < len(rb.nestedBatches) { + msg.SetTag("batch", rb.nestedBatches[len(rb.nestedBatches)-1]) + } rb.messages = append(rb.messages, msg) } @@ -63,9 +78,11 @@ func (rb *ResponseBuffer) Add(tags map[string]string, prefix string, command str } // 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]string, command string, params ...string) { +func (rb *ResponseBuffer) AddFromClient(time time.Time, msgid string, fromNickMask string, fromAccount string, tags map[string]string, command string, params ...string) { msg := ircmsg.MakeMessage(nil, fromNickMask, command, params...) - msg.UpdateTags(tags) + if rb.session.capabilities.Has(caps.MessageTags) { + msg.UpdateTags(tags) + } // attach account-tag if rb.session.capabilities.Has(caps.AccountTag) && fromAccount != "*" { @@ -75,6 +92,10 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA if len(msgid) > 0 && rb.session.capabilities.Has(caps.MessageTags) { msg.SetTag("draft/msgid", msgid) } + // attach server-time + if rb.session.capabilities.Has(caps.ServerTime) && !msg.HasTag("time") { + msg.SetTag("time", time.UTC().Format(IRCv3TimestampFormat)) + } rb.AddMessage(msg) } @@ -82,33 +103,31 @@ func (rb *ResponseBuffer) AddFromClient(msgid string, fromNickMask string, fromA // AddSplitMessageFromClient adds a new split message from a specific client to our queue. func (rb *ResponseBuffer) AddSplitMessageFromClient(fromNickMask string, fromAccount string, tags map[string]string, command string, target string, message utils.SplitMessage) { if rb.session.capabilities.Has(caps.MaxLine) || message.Wrapped == nil { - rb.AddFromClient(message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) + rb.AddFromClient(message.Time, message.Msgid, fromNickMask, fromAccount, tags, command, target, message.Message) } else { for _, messagePair := range message.Wrapped { - rb.AddFromClient(messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message) + rb.AddFromClient(message.Time, messagePair.Msgid, fromNickMask, fromAccount, tags, command, target, messagePair.Message) } } } -// InitializeBatch forcibly starts a batch of batch `batchType`. +// ForceBatchStart forcibly starts a batch of batch `batchType`. // Normally, Send/Flush will decide automatically whether to start a batch // of type draft/labeled-response. This allows changing the batch type // and forcing the creation of a possibly empty batch. -func (rb *ResponseBuffer) InitializeBatch(batchType string, blocking bool) { - rb.sendBatchStart(batchType, blocking) +func (rb *ResponseBuffer) ForceBatchStart(batchType string, blocking bool) { + rb.batchType = batchType + rb.sendBatchStart(blocking) } -func (rb *ResponseBuffer) sendBatchStart(batchType string, blocking bool) { +func (rb *ResponseBuffer) sendBatchStart(blocking bool) { if rb.batchID != "" { // batch already initialized return } - // formerly this combined time.Now.UnixNano() in base 36 with an incrementing counter, - // also in base 36. but let's just use a uuidv4-alike (26 base32 characters): rb.batchID = utils.GenerateSecretToken() - - message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, batchType) + message := ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "+"+rb.batchID, rb.batchType) if rb.Label != "" { message.SetTag(caps.LabelTagName, rb.Label) } @@ -125,6 +144,50 @@ func (rb *ResponseBuffer) sendBatchEnd(blocking bool) { rb.session.SendRawMessage(message, blocking) } +// Starts a nested batch (see the ResponseBuffer struct definition for a description of +// how this works) +func (rb *ResponseBuffer) StartNestedBatch(batchType string, params ...string) (batchID string) { + batchID = utils.GenerateSecretToken() + msgParams := make([]string, len(params)+2) + msgParams[0] = "+" + batchID + msgParams[1] = batchType + copy(msgParams[2:], params) + rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", msgParams...)) + rb.nestedBatches = append(rb.nestedBatches, batchID) + return +} + +// Ends a nested batch +func (rb *ResponseBuffer) EndNestedBatch(batchID string) { + if batchID == "" { + return + } + + if 0 == len(rb.nestedBatches) || rb.nestedBatches[len(rb.nestedBatches)-1] != batchID { + rb.target.server.logger.Error("internal", "inconsistent batch nesting detected") + debug.PrintStack() + return + } + + rb.nestedBatches = rb.nestedBatches[0 : len(rb.nestedBatches)-1] + rb.AddMessage(ircmsg.MakeMessage(nil, rb.target.server.name, "BATCH", "-"+batchID)) +} + +// Convenience to start a nested batch for history lines, at the highest level +// supported by the client (`history`, `chathistory`, or no batch, in descending order). +func (rb *ResponseBuffer) StartNestedHistoryBatch(params ...string) (batchID string) { + var batchType string + if rb.session.capabilities.Has(caps.EventPlayback) { + batchType = "history" + } else if rb.session.capabilities.Has(caps.Batch) { + batchType = "chathistory" + } + if batchType != "" { + batchID = rb.StartNestedBatch(batchType, params...) + } + return +} + // Send sends all messages in the buffer to the client. // Afterwards, the buffer is in an undefined state and MUST NOT be used further. // If `blocking` is true you MUST be sending to the client from its own goroutine. @@ -158,7 +221,7 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { if useLabel && !useBatch && len(rb.messages) == 1 && rb.batchID == "" { rb.messages[0].SetTag(caps.LabelTagName, rb.Label) } else if useBatch { - rb.sendBatchStart(defaultBatchType, blocking) + rb.sendBatchStart(blocking) } // send each message out @@ -168,8 +231,9 @@ func (rb *ResponseBuffer) flushInternal(final bool, blocking bool) error { message.SetTag("time", time.Now().UTC().Format(IRCv3TimestampFormat)) } - // attach batch ID - if rb.batchID != "" { + // attach batch ID, unless this message was part of a nested batch and is + // already tagged + if rb.batchID != "" && !message.HasTag("batch") { message.SetTag("batch", rb.batchID) } diff --git a/irc/utils/text.go b/irc/utils/text.go index e216d55c..92b9af38 100644 --- a/irc/utils/text.go +++ b/irc/utils/text.go @@ -4,6 +4,7 @@ package utils import "bytes" +import "time" // WordWrap wraps the given text into a series of lines that don't exceed lineWidth characters. func WordWrap(text string, lineWidth int) []string { @@ -59,6 +60,7 @@ type MessagePair struct { type SplitMessage struct { MessagePair Wrapped []MessagePair // if this is nil, `Message` didn't need wrapping and can be sent to anyone + Time time.Time } const defaultLineWidth = 400 @@ -66,6 +68,7 @@ const defaultLineWidth = 400 func MakeSplitMessage(original string, origIs512 bool) (result SplitMessage) { result.Message = original result.Msgid = GenerateSecretToken() + result.Time = time.Now().UTC() if !origIs512 && defaultLineWidth < len(original) { wrapped := WordWrap(original, defaultLineWidth) From da656c07c8fb46320715388b6e05782887462098 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 8 May 2019 04:11:54 -0400 Subject: [PATCH 2/6] add NICKSERV SESSIONS command to list sessions --- irc/client.go | 44 +++++++++++++++++++++++++++++++------------- irc/commands.go | 4 ++-- irc/gateways.go | 2 ++ irc/getters.go | 32 ++++++++++++++++++++++++++++++++ irc/nickserv.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ irc/server.go | 15 ++++++++------- 6 files changed, 122 insertions(+), 22 deletions(-) diff --git a/irc/client.go b/irc/client.go index 73c0bfb6..61caa8da 100644 --- a/irc/client.go +++ b/irc/client.go @@ -94,7 +94,14 @@ type Client struct { type Session struct { client *Client - socket *Socket + ctime time.Time + atime time.Time + + socket *Socket + realIP net.IP + proxiedIP net.IP + rawHostname string + idletimer IdleTimer fakelag Fakelag @@ -104,9 +111,6 @@ type Session struct { maxlenRest uint32 capState caps.State capVersion caps.Version - - // TODO track per-connection real IP, proxied IP, and hostname here, - // so we can list attached sessions and their details } // sets the session quit message, if there isn't one already @@ -187,6 +191,8 @@ func RunNewClient(server *Server, conn clientConn) { socket: socket, capVersion: caps.Cap301, capState: caps.NoneState, + ctime: now, + atime: now, } session.SetMaxlenRest() client.sessions = []*Session{session} @@ -197,20 +203,29 @@ func RunNewClient(server *Server, conn clientConn) { client.certfp, _ = socket.CertFP() } + remoteAddr := conn.Conn.RemoteAddr() if conn.IsTor { client.SetMode(modes.TLS, true) - client.realIP = utils.IPv4LoopbackAddress - client.rawHostname = config.Server.TorListeners.Vhost + session.realIP = utils.AddrToIP(remoteAddr) + // cover up details of the tor proxying infrastructure (not a user privacy concern, + // but a hardening measure): + session.proxiedIP = utils.IPv4LoopbackAddress + session.rawHostname = config.Server.TorListeners.Vhost } else { - remoteAddr := conn.Conn.RemoteAddr() - client.realIP = utils.AddrToIP(remoteAddr) - // Set the hostname for this client - // (may be overridden by a later PROXY command from stunnel) - client.rawHostname = utils.LookupHostname(client.realIP.String()) + session.realIP = utils.AddrToIP(remoteAddr) + // set the hostname for this client (may be overridden later by PROXY or WEBIRC) + session.rawHostname = utils.LookupHostname(session.realIP.String()) + if utils.AddrIsLocal(remoteAddr) { + // treat local connections as secure (may be overridden later by WEBIRC) + client.SetMode(modes.TLS, true) + } if config.Server.CheckIdent && !utils.AddrIsUnix(remoteAddr) { client.doIdentLookup(conn.Conn) } } + client.realIP = session.realIP + client.rawHostname = session.rawHostname + client.proxiedIP = session.proxiedIP client.run(session) } @@ -389,10 +404,13 @@ func (session *Session) playReattachMessages() { // // Active updates when the client was last 'active' (i.e. the user should be sitting in front of their client). -func (client *Client) Active() { +func (client *Client) Active(session *Session) { + // TODO normalize all times to utc? + now := time.Now() client.stateMutex.Lock() defer client.stateMutex.Unlock() - client.atime = time.Now() + session.atime = now + client.atime = now } // Ping sends the client a PING message. diff --git a/irc/commands.go b/irc/commands.go index e7399acb..e058a3e7 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -58,8 +58,8 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir session.idletimer.Touch() } - if !cmd.leaveClientIdle { - client.Active() + if client.registered && !cmd.leaveClientIdle { + client.Active(session) } return exiting diff --git a/irc/gateways.go b/irc/gateways.go index a3db9582..495dfee0 100644 --- a/irc/gateways.go +++ b/irc/gateways.go @@ -73,7 +73,9 @@ func (client *Client) ApplyProxiedIP(session *Session, proxiedIP string, tls boo client.stateMutex.Lock() defer client.stateMutex.Unlock() + session.proxiedIP = parsedProxiedIP client.proxiedIP = parsedProxiedIP + session.rawHostname = rawHostname client.rawHostname = rawHostname // nickmask will be updated when the client completes registration // set tls info diff --git a/irc/getters.go b/irc/getters.go index ac3e5124..af77a0a8 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -4,6 +4,7 @@ package irc import ( + "net" "time" "github.com/oragono/oragono/irc/isupport" @@ -70,6 +71,37 @@ func (client *Client) Sessions() (sessions []*Session) { return } +type SessionData struct { + ctime time.Time + atime time.Time + ip net.IP + hostname string +} + +func (client *Client) AllSessionData(currentSession *Session) (data []SessionData, currentIndex int) { + currentIndex = -1 + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + data = make([]SessionData, len(client.sessions)) + for i, session := range client.sessions { + if session == currentSession { + currentIndex = i + } + data[i] = SessionData{ + atime: session.atime, + ctime: session.ctime, + hostname: session.rawHostname, + } + if session.proxiedIP != nil { + data[i].ip = session.proxiedIP + } else { + data[i].ip = session.realIP + } + } + return +} + func (client *Client) AddSession(session *Session) (success bool) { client.stateMutex.Lock() defer client.stateMutex.Unlock() diff --git a/irc/nickserv.go b/irc/nickserv.go index 74941658..9a69c90f 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -7,6 +7,8 @@ import ( "fmt" "github.com/goshuirc/irc-go/ircfmt" + + "github.com/oragono/oragono/irc/modes" ) // "enabled" callbacks for specific nickserv commands @@ -26,6 +28,10 @@ func nsEnforceEnabled(config *Config) bool { return servCmdRequiresNickRes(config) && config.Accounts.NickReservation.AllowCustomEnforcement } +func servCmdRequiresBouncerEnabled(config *Config) bool { + return config.Accounts.Bouncer.Enabled +} + var ( // ZNC's nickserv module will not detect this unless it is: // 1. sent with prefix `nickserv` @@ -142,6 +148,16 @@ an administrator can set use this command to set up user accounts.`, capabs: []string{"accreg"}, minParams: 2, }, + "sessions": { + handler: nsSessionsHandler, + help: `Syntax: $bSESSIONS [nickname]$b + +SESSIONS lists information about the sessions currently attached, via +the server's bouncer functionality, to your nickname. An administrator +can use this command to list another user's sessions.`, + helpShort: `$bSESSIONS$b lists the sessions attached to a nickname.`, + enabled: servCmdRequiresBouncerEnabled, + }, "unregister": { handler: nsUnregisterHandler, help: `Syntax: $bUNREGISTER [code]$b @@ -569,3 +585,34 @@ func nsEnforceHandler(server *Server, client *Client, command string, params []s } } } + +func nsSessionsHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { + target := client + + if 0 < len(params) { + // same permissions check as RPL_WHOISACTUALLY for now: + if !client.HasMode(modes.Operator) { + nsNotice(rb, client.t("Command restricted")) + return + } + target = server.clients.Get(params[0]) + if target == nil { + nsNotice(rb, client.t("No such nick")) + return + } + } + + sessionData, currentIndex := target.AllSessionData(rb.session) + nsNotice(rb, fmt.Sprintf(client.t("Nickname %s has %d attached session(s)"), target.Nick(), len(sessionData))) + for i, session := range sessionData { + if currentIndex == i { + nsNotice(rb, fmt.Sprintf(client.t("Session %d (currently attached session):"), i+1)) + } else { + nsNotice(rb, fmt.Sprintf(client.t("Session %d:"), i+1)) + } + nsNotice(rb, fmt.Sprintf(client.t("IP address: %s"), session.ip.String())) + nsNotice(rb, fmt.Sprintf(client.t("Hostname: %s"), session.hostname)) + nsNotice(rb, fmt.Sprintf(client.t("Created at: %s"), session.ctime.Format(IRCv3TimestampFormat))) + nsNotice(rb, fmt.Sprintf(client.t("Last active: %s"), session.atime.Format(IRCv3TimestampFormat))) + } +} diff --git a/irc/server.go b/irc/server.go index fc628098..fad52417 100644 --- a/irc/server.go +++ b/irc/server.go @@ -428,17 +428,18 @@ func (server *Server) tryRegister(c *Client, session *Session) { } // continue registration - server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", c.nick, c.username, c.realname)) - server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", c.nick, c.username, c.rawHostname, c.IPString(), c.realname)) + d := c.Details() + server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname)) + server.snomasks.Send(sno.LocalConnects, fmt.Sprintf("Client connected [%s] [u:%s] [h:%s] [ip:%s] [r:%s]", d.nick, d.username, c.RawHostname(), c.IPString(), d.realname)) // send welcome text //NOTE(dan): we specifically use the NICK here instead of the nickmask // see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask - c.Send(nil, server.name, RPL_WELCOME, c.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), c.nick)) - c.Send(nil, server.name, RPL_YOURHOST, c.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) - c.Send(nil, server.name, RPL_CREATED, c.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) + c.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick)) + c.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) + c.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) //TODO(dan): Look at adding last optional [] parameter - c.Send(nil, server.name, RPL_MYINFO, c.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) + c.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) rb := NewResponseBuffer(session) c.RplISupport(rb) @@ -447,7 +448,7 @@ func (server *Server) tryRegister(c *Client, session *Session) { modestring := c.ModeString() if modestring != "+" { - c.Send(nil, c.nickMaskString, RPL_UMODEIS, c.nick, c.ModeString()) + c.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, c.ModeString()) } if server.logger.IsLoggingRawIO() { c.Notice(c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) From 60c8f286e8299704aa615038f8668730c2da2d54 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 8 May 2019 18:14:49 -0400 Subject: [PATCH 3/6] fix several session destruction bugs --- irc/client.go | 69 +++++++++++++++++++++++----------------- irc/client_lookup_set.go | 4 +-- irc/getters.go | 3 ++ irc/server.go | 50 ++++++++++++++++------------- 4 files changed, 71 insertions(+), 55 deletions(-) diff --git a/irc/client.go b/irc/client.go index 61caa8da..f81e919b 100644 --- a/irc/client.go +++ b/irc/client.go @@ -323,8 +323,10 @@ func (client *Client) run(session *Session) { session.resetFakelag() isReattach := client.Registered() - // don't reset the nick timer during a reattach - if !isReattach { + if isReattach { + client.playReattachMessages(session) + } else { + // don't reset the nick timer during a reattach client.nickTimer.Initialize(client) } @@ -386,14 +388,14 @@ func (client *Client) run(session *Session) { break } else if session.client != client { // bouncer reattach - session.playReattachMessages() go session.client.run(session) break } } } -func (session *Session) playReattachMessages() { +func (client *Client) playReattachMessages(session *Session) { + client.server.playRegistrationBurst(session) for _, channel := range session.client.Channels() { channel.playJoinForSession(session) } @@ -922,14 +924,11 @@ func (client *Client) destroy(beingResumed bool, session *Session) { // allow destroy() to execute at most once client.stateMutex.Lock() - nickMaskString := client.nickMaskString - accountName := client.accountName - - alreadyDestroyed := len(client.sessions) == 0 + details := client.detailsNoMutex() + wasReattach := session != nil && session.client != client sessionRemoved := false var remainingSessions int if session == nil { - sessionRemoved = !alreadyDestroyed sessionsToDestroy = client.sessions client.sessions = nil remainingSessions = 0 @@ -939,33 +938,46 @@ func (client *Client) destroy(beingResumed bool, session *Session) { sessionsToDestroy = []*Session{session} } } - var quitMessage string - if 0 < len(sessionsToDestroy) { - quitMessage = sessionsToDestroy[0].quitMessage - } client.stateMutex.Unlock() - if alreadyDestroyed || !sessionRemoved { + if len(sessionsToDestroy) == 0 { return } + // destroy all applicable sessions: + var quitMessage string for _, session := range sessionsToDestroy { if session.client != client { // session has been attached to a new client; do not destroy it continue } session.idletimer.Stop() - session.socket.Close() // send quit/error message to client if they haven't been sent already client.Quit("", session) + quitMessage = session.quitMessage + session.socket.Close() + + // remove from connection limits + var source string + if client.isTor { + client.server.torLimiter.RemoveClient() + source = "tor" + } else { + ip := session.realIP + if session.proxiedIP != nil { + ip = session.proxiedIP + } + client.server.connectionLimiter.RemoveClient(ip) + source = ip.String() + } + client.server.logger.Info("localconnect-ip", fmt.Sprintf("disconnecting session of %s from %s", details.nick, source)) } + // ok, now destroy the client, unless it still has sessions: if remainingSessions != 0 { return } - details := client.Details() - // see #235: deduplicating the list of PART recipients uses (comparatively speaking) // a lot of RAM, so limit concurrency to avoid thrashing client.server.semaphores.ClientDestroy.Acquire() @@ -973,38 +985,35 @@ func (client *Client) destroy(beingResumed bool, session *Session) { if beingResumed { client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick)) - } else { + } else if !wasReattach { client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick)) } - if !beingResumed { - client.server.whoWas.Append(details.WhoWas) - } - - // remove from connection limits - if client.isTor { - client.server.torLimiter.RemoveClient() - } else { - client.server.connectionLimiter.RemoveClient(client.IP()) + registered := client.Registered() + if !beingResumed && registered { + client.server.whoWas.Append(client.WhoWas()) } client.server.resumeManager.Delete(client) // alert monitors - client.server.monitorManager.AlertAbout(client, false) + if registered { + client.server.monitorManager.AlertAbout(client, false) + } // clean up monitor state client.server.monitorManager.RemoveAll(client) splitQuitMessage := utils.MakeSplitMessage(quitMessage, true) // clean up channels + // (note that if this is a reattach, client has no channels and therefore no friends) friends := make(ClientSet) for _, channel := range client.Channels() { if !beingResumed { channel.Quit(client) channel.history.Add(history.Item{ Type: history.Quit, - Nick: nickMaskString, - AccountName: accountName, + Nick: details.nickMask, + AccountName: details.accountName, Message: splitQuitMessage, }) } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 1982d968..56bcd995 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -160,9 +160,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick if !currentClient.AddSession(session) { return errNicknameInUse } - // successful reattach. temporarily assign them the nick they'll have going forward - // (the current `client` will be discarded at the end of command execution) - client.updateNick(currentClient.Nick(), newcfnick, newSkeleton) + // successful reattach! return nil } // analogous checks for skeletons diff --git a/irc/getters.go b/irc/getters.go index af77a0a8..915a652c 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -329,7 +329,10 @@ func (client *Client) WhoWas() (result WhoWas) { func (client *Client) Details() (result ClientDetails) { client.stateMutex.RLock() defer client.stateMutex.RUnlock() + return client.detailsNoMutex() +} +func (client *Client) detailsNoMutex() (result ClientDetails) { result.nick = client.nick result.nickCasefolded = client.nickCasefolded result.username = client.username diff --git a/irc/server.go b/irc/server.go index fad52417..a5ea566f 100644 --- a/irc/server.go +++ b/irc/server.go @@ -413,20 +413,30 @@ func (server *Server) tryRegister(c *Client, session *Session) { } } - reattached := session.client != c - - if !reattached { - // registration has succeeded: - c.SetRegistered() - - // count new user in statistics - server.stats.ChangeTotal(1) - - if !resumed { - server.monitorManager.AlertAbout(c, true) - } + if session.client != c { + // reattached, bail out. + // we'll play the reg burst later, on the new goroutine associated with + // (thisSession, otherClient). This is to avoid having to transfer state + // like nickname, hostname, etc. to show the correct values in the reg burst. + return } + // registration has succeeded: + c.SetRegistered() + // count new user in statistics + server.stats.ChangeTotal(1) + + server.playRegistrationBurst(session) + + if resumed { + c.tryResumeChannels() + } else { + server.monitorManager.AlertAbout(c, true) + } +} + +func (server *Server) playRegistrationBurst(session *Session) { + c := session.client // continue registration d := c.Details() server.logger.Info("localconnect", fmt.Sprintf("Client connected [%s] [u:%s] [r:%s]", d.nick, d.username, d.realname)) @@ -435,11 +445,11 @@ func (server *Server) tryRegister(c *Client, session *Session) { // send welcome text //NOTE(dan): we specifically use the NICK here instead of the nickmask // see http://modern.ircdocs.horse/#rplwelcome-001 for details on why we avoid using the nickmask - c.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick)) - c.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) - c.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) + session.Send(nil, server.name, RPL_WELCOME, d.nick, fmt.Sprintf(c.t("Welcome to the Internet Relay Network %s"), d.nick)) + session.Send(nil, server.name, RPL_YOURHOST, d.nick, fmt.Sprintf(c.t("Your host is %[1]s, running version %[2]s"), server.name, Ver)) + session.Send(nil, server.name, RPL_CREATED, d.nick, fmt.Sprintf(c.t("This server was created %s"), server.ctime.Format(time.RFC1123))) //TODO(dan): Look at adding last optional [] parameter - c.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) + session.Send(nil, server.name, RPL_MYINFO, d.nick, server.name, Ver, supportedUserModesString, supportedChannelModesString) rb := NewResponseBuffer(session) c.RplISupport(rb) @@ -448,14 +458,10 @@ func (server *Server) tryRegister(c *Client, session *Session) { modestring := c.ModeString() if modestring != "+" { - c.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, c.ModeString()) + session.Send(nil, d.nickMask, RPL_UMODEIS, d.nick, modestring) } if server.logger.IsLoggingRawIO() { - c.Notice(c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) - } - - if resumed { - c.tryResumeChannels() + session.Send(nil, c.server.name, "NOTICE", d.nick, c.t("This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")) } } From 08ebfb167653b7e98c9a663367594a7eb0211f8f Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 9 May 2019 03:57:02 -0400 Subject: [PATCH 4/6] fix redundant JOIN line --- irc/channel.go | 1 - 1 file changed, 1 deletion(-) diff --git a/irc/channel.go b/irc/channel.go index 28315735..4461ea39 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -592,7 +592,6 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp if session.capabilities.Has(caps.ExtendedJoin) { session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname, details.accountName, details.realname) } else { - session.Send(nil, details.nickMask, "JOIN", chname) session.sendFromClientInternal(false, message.Time, message.Msgid, details.nickMask, details.accountName, nil, "JOIN", chname) } if givenMode != 0 { From 278e4acf573bd8bc71a969004b006ed490ca3f4e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 9 May 2019 05:03:21 -0400 Subject: [PATCH 5/6] fix handling of nil session in reattach code --- irc/accounts.go | 5 ++++- irc/client_lookup_set.go | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/irc/accounts.go b/irc/accounts.go index b68b0f7f..1345ebe5 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -238,7 +238,10 @@ func (am *AccountManager) BouncerAllowed(account string, session *Session) bool if !config.Accounts.Bouncer.Enabled { return false } - return config.Accounts.Bouncer.AllowedByDefault || session.capabilities.Has(caps.Bouncer) + if config.Accounts.Bouncer.AllowedByDefault { + return true + } + return session != nil && session.capabilities.Has(caps.Bouncer) } // Looks up the enforcement method stored in the database for an account diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 56bcd995..65ac7127 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -152,7 +152,7 @@ func (clients *ClientManager) SetNick(client *Client, session *Session, newNick currentClient := clients.byNick[newcfnick] // the client may just be changing case - if currentClient != nil && currentClient != client { + if currentClient != nil && currentClient != client && session != nil { // these conditions forbid reattaching to an existing session: if client.Registered() || !bouncerAllowed || account == "" || account != currentClient.Account() || client.isTor != currentClient.isTor || client.HasMode(modes.TLS) != currentClient.HasMode(modes.TLS) { return errNicknameInUse From 15289bd7db7d9f4cd5a4406b661ad6f5128bd488 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sat, 11 May 2019 21:48:36 -0400 Subject: [PATCH 6/6] review fixes --- gencapdefs.py | 2 +- irc/caps/defs.go | 2 +- irc/history/history.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gencapdefs.py b/gencapdefs.py index a6baee1b..3cacebda 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -163,7 +163,7 @@ CAPDEFS = [ identifier="EventPlayback", name="draft/event-playback", url="https://github.com/ircv3/ircv3-specifications/pull/362", - standard="Draft IRCv3", + standard="Proposed IRCv3", ), ] diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 9f10f0ae..c261a6e4 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -109,7 +109,7 @@ const ( // https://wiki.znc.in/Query_buffers ZNCSelfMessage Capability = iota - // EventPlayback is the Draft IRCv3 capability named "draft/event-playback": + // EventPlayback is the Proposed IRCv3 capability named "draft/event-playback": // https://github.com/ircv3/ircv3-specifications/pull/362 EventPlayback Capability = iota ) diff --git a/irc/history/history.go b/irc/history/history.go index 3a3655bb..790eae12 100644 --- a/irc/history/history.go +++ b/irc/history/history.go @@ -25,8 +25,8 @@ const ( Nick ) -// a Tagmsg that consists entirely of junk tags is not stored -var junkTags = map[string]bool{ +// a Tagmsg that consists entirely of transient tags is not stored +var transientTags = map[string]bool{ "+draft/typing": true, "+typing": true, // future-proofing } @@ -60,7 +60,7 @@ func (item *Item) HasMsgid(msgid string) bool { func (item *Item) isStorable() bool { if item.Type == Tagmsg { for name := range item.Tags { - if !junkTags[name] { + if !transientTags[name] { return true } }