diff --git a/gencapdefs.py b/gencapdefs.py index 10c9481b..f3825c51 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -113,7 +113,7 @@ CAPDEFS = [ ), CapDef( identifier="Resume", - name="draft/resume-0.3", + name="draft/resume-0.4", url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md", standard="proposed IRCv3", ), diff --git a/irc/accounts.go b/irc/accounts.go index 8815d886..d60ac246 100644 --- a/irc/accounts.go +++ b/irc/accounts.go @@ -931,7 +931,7 @@ func (am *AccountManager) Unregister(account string) error { if config.Accounts.RequireSasl.Enabled { client.Quit(client.t("You are no longer authorized to be on this server"), nil) // destroy acquires a semaphore so we can't call it while holding a lock - go client.destroy(false, nil) + go client.destroy(nil) } else { am.logoutOfAccount(client) } diff --git a/irc/caps/defs.go b/irc/caps/defs.go index e4fcfcac..0ac48337 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -77,7 +77,7 @@ const ( // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md Rename Capability = iota - // Resume is the proposed IRCv3 capability named "draft/resume-0.3": + // Resume is the proposed IRCv3 capability named "draft/resume-0.4": // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md Resume Capability = iota @@ -141,7 +141,7 @@ var ( "message-tags", "multi-prefix", "draft/rename", - "draft/resume-0.3", + "draft/resume-0.4", "sasl", "server-time", "draft/setname", diff --git a/irc/channel.go b/irc/channel.go index 3854ce4a..e4b71055 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -20,6 +20,10 @@ import ( "github.com/oragono/oragono/irc/utils" ) +const ( + histServMask = "HistServ!HistServ@localhost" +) + // Channel represents a channel that clients can join. type Channel struct { flags modes.ModeSet @@ -664,10 +668,11 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp func (channel *Channel) playJoinForSession(session *Session) { client := session.client sessionRb := NewResponseBuffer(session) + details := client.Details() if session.capabilities.Has(caps.ExtendedJoin) { - sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name(), client.AccountName(), client.Realname()) + sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name(), details.accountName, details.realname) } else { - sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name()) + sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name()) } channel.SendTopic(client, sessionRb, false) channel.Names(client, sessionRb) @@ -711,46 +716,29 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer) // 1. Replace the old client with the new in the channel's data structures // 2. Send JOIN and MODE lines to channel participants (including the new client) // 3. Replay missed message history to the client -func (channel *Channel) Resume(newClient, oldClient *Client, timestamp time.Time) { +func (channel *Channel) Resume(session *Session, timestamp time.Time) { now := time.Now().UTC() - channel.resumeAndAnnounce(newClient, oldClient) + channel.resumeAndAnnounce(session) if !timestamp.IsZero() { - channel.replayHistoryForResume(newClient, timestamp, now) + channel.replayHistoryForResume(session, timestamp, now) } } -func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { - var oldModeSet *modes.ModeSet - - func() { - channel.joinPartMutex.Lock() - defer channel.joinPartMutex.Unlock() - - defer channel.regenerateMembersCache() - - channel.stateMutex.Lock() - defer channel.stateMutex.Unlock() - - newClient.channels[channel] = true - oldModeSet = channel.members[oldClient] - if oldModeSet == nil { - oldModeSet = modes.NewModeSet() - } - channel.members.Remove(oldClient) - channel.members[newClient] = oldModeSet - }() - - // construct fake modestring if necessary - oldModes := oldModeSet.String() +func (channel *Channel) resumeAndAnnounce(session *Session) { + channel.stateMutex.RLock() + modeSet := channel.members[session.client] + channel.stateMutex.RUnlock() + if modeSet == nil { + return + } + oldModes := modeSet.String() if 0 < len(oldModes) { oldModes = "+" + oldModes } // send join for old clients - nick := newClient.Nick() - nickMask := newClient.NickMaskString() - accountName := newClient.AccountName() - realName := newClient.Realname() + chname := channel.Name() + details := session.client.Details() for _, member := range channel.Members() { for _, session := range member.Sessions() { if session.capabilities.Has(caps.Resume) { @@ -758,39 +746,36 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { } if session.capabilities.Has(caps.ExtendedJoin) { - session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName) + session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, details.realname) } else { - session.Send(nil, nickMask, "JOIN", channel.name) + session.Send(nil, details.nickMask, "JOIN", chname) } if 0 < len(oldModes) { - session.Send(nil, channel.server.name, "MODE", channel.name, oldModes, nick) + session.Send(nil, channel.server.name, "MODE", chname, oldModes, details.nick) } } } - rb := NewResponseBuffer(newClient.Sessions()[0]) + rb := NewResponseBuffer(session) // use blocking i/o to synchronize with the later history replay if rb.session.capabilities.Has(caps.ExtendedJoin) { - rb.Add(nil, nickMask, "JOIN", channel.name, accountName, realName) + rb.Add(nil, details.nickMask, "JOIN", channel.name, details.accountName, details.realname) } else { - rb.Add(nil, nickMask, "JOIN", channel.name) - } - channel.SendTopic(newClient, rb, false) - channel.Names(newClient, rb) - if 0 < len(oldModes) { - rb.Add(nil, newClient.server.name, "MODE", channel.name, oldModes, nick) + rb.Add(nil, details.nickMask, "JOIN", channel.name) } + channel.SendTopic(session.client, rb, false) + channel.Names(session.client, rb) rb.Send(true) } -func (channel *Channel) replayHistoryForResume(newClient *Client, after time.Time, before time.Time) { +func (channel *Channel) replayHistoryForResume(session *Session, after time.Time, before time.Time) { items, complete := channel.history.Between(after, before, false, 0) - rb := NewResponseBuffer(newClient.Sessions()[0]) + rb := NewResponseBuffer(session) channel.replayHistoryItems(rb, items, false) - if !complete && !newClient.resumeDetails.HistoryIncomplete { + if !complete && !session.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")) + rb.Add(nil, histServMask, "NOTICE", channel.Name(), session.client.t("Some additional message history may have been lost")) } rb.Send(true) } @@ -844,7 +829,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I } else { message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) } - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) } case history.Part: if eventPlayback { @@ -854,14 +839,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I continue // #474 } message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) } case history.Kick: 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, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) } case history.Quit: if eventPlayback { @@ -871,14 +856,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I continue // #474 } message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", 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, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), histServMask, "*", nil, "PRIVMSG", chname, message) } } } @@ -923,7 +908,7 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe return } - topicLimit := client.server.Limits().TopicLen + topicLimit := client.server.Config().Limits.TopicLen if len(topic) > topicLimit { topic = topic[:topicLimit] } @@ -1152,7 +1137,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb return } - kicklimit := client.server.Limits().KickLen + kicklimit := client.server.Config().Limits.KickLen if len(comment) > kicklimit { comment = comment[:kicklimit] } diff --git a/irc/channelmanager.go b/irc/channelmanager.go index f520619e..ad07d1c6 100644 --- a/irc/channelmanager.go +++ b/irc/channelmanager.go @@ -61,7 +61,7 @@ func (cm *ChannelManager) Get(name string) (channel *Channel) { func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error { server := client.server casefoldedName, err := CasefoldChannel(name) - if err != nil || len(casefoldedName) > server.Limits().ChannelLen { + if err != nil || len(casefoldedName) > server.Config().Limits.ChannelLen { return errNoSuchChannel } diff --git a/irc/client.go b/irc/client.go index 527cdd0b..b75b822e 100644 --- a/irc/client.go +++ b/irc/client.go @@ -36,11 +36,8 @@ const ( // the resume process: when handling the RESUME command itself, // when completing the registration, and when rejoining channels. type ResumeDetails struct { - OldClient *Client PresentedToken string Timestamp time.Time - ResumedAt time.Time - Channels []string HistoryIncomplete bool } @@ -52,6 +49,7 @@ type Client struct { atime time.Time away bool awayMessage string + brbTimer BrbTimer certfp string channels ChannelSet ctime time.Time @@ -75,7 +73,6 @@ type Client struct { realname string realIP net.IP registered bool - resumeDetails *ResumeDetails resumeID string saslInProgress bool saslMechanism string @@ -87,7 +84,7 @@ type Client struct { stateMutex sync.RWMutex // tier 1 username string vhost string - history *history.Buffer + history history.Buffer } // Session is an individual client connection to the server (TCP connection @@ -114,6 +111,10 @@ type Session struct { capState caps.State capVersion caps.Version + registrationMessages int + + resumeID string + resumeDetails *ResumeDetails zncPlaybackTimes *zncPlaybackTimes } @@ -209,8 +210,9 @@ func (server *Server) RunClient(conn clientConn) { nick: "*", // * is used until actual nick is given nickCasefolded: "*", nickMaskString: "*", // * is used until actual nick is given - history: history.NewHistoryBuffer(config.History.ClientLength), } + client.history.Initialize(config.History.ClientLength) + client.brbTimer.Initialize(client) session := &Session{ client: client, socket: socket, @@ -334,14 +336,14 @@ func (client *Client) run(session *Session) { if r := recover(); r != nil { client.server.logger.Error("internal", fmt.Sprintf("Client caused panic: %v\n%s", r, debug.Stack())) - if client.server.RecoverFromErrors() { + if client.server.Config().Debug.recoverFromErrors { client.server.logger.Error("internal", "Disconnecting client and attempting to recover") } else { panic(r) } } // ensure client connection gets closed - client.destroy(false, session) + client.destroy(session) }() session.idletimer.Initialize(session) @@ -349,7 +351,13 @@ func (client *Client) run(session *Session) { isReattach := client.Registered() if isReattach { - client.playReattachMessages(session) + if session.resumeDetails != nil { + session.playResume() + session.resumeDetails = nil + client.brbTimer.Disable() + } else { + client.playReattachMessages(session) + } } else { // don't reset the nick timer during a reattach client.nickTimer.Initialize(client) @@ -367,6 +375,9 @@ func (client *Client) run(session *Session) { quitMessage = "readQ exceeded" } client.Quit(quitMessage, session) + // since the client did not actually send us a QUIT, + // give them a chance to resume or reattach if applicable: + client.brbTimer.Enable() break } @@ -387,6 +398,17 @@ func (client *Client) run(session *Session) { } } + if client.registered { + session.fakelag.Touch() + } else { + // DoS hardening, #505 + session.registrationMessages++ + if client.server.Config().Limits.RegistrationMessages < session.registrationMessages { + client.Send(nil, client.server.name, ERR_UNKNOWNERROR, "*", client.t("You have sent too many registration messages")) + break + } + } + msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest) if err == ircmsg.ErrorLineIsEmpty { continue @@ -445,83 +467,66 @@ func (session *Session) Ping() { } // tryResume tries to resume if the client asked us to. -func (client *Client) tryResume() (success bool) { - server := client.server - config := server.Config() +func (session *Session) tryResume() (success bool) { + var oldResumeID string defer func() { - if !success { - client.resumeDetails = nil + if success { + // "On a successful request, the server [...] terminates the old client's connection" + oldSession := session.client.GetSessionByResumeID(oldResumeID) + if oldSession != nil { + session.client.destroy(oldSession) + } + } else { + session.resumeDetails = nil } }() - timestamp := client.resumeDetails.Timestamp - var timestampString string - if !timestamp.IsZero() { - timestampString = timestamp.UTC().Format(IRCv3TimestampFormat) - } + client := session.client + server := client.server + config := server.Config() - oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken) + oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken) if oldClient == nil { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, token is not valid")) + session.Send(nil, server.name, "FAIL", "RESUME", "INVALID_TOKEN", client.t("Cannot resume connection, token is not valid")) return } - oldNick := oldClient.Nick() - oldNickmask := oldClient.NickMaskString() resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS)) if !resumeAllowed { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS")) + session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection, old and new clients must have TLS")) return } if oldClient.isTor != client.isTor { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection from Tor to non-Tor or vice versa")) + session.Send(nil, server.name, "FAIL", "RESUME", "INSECURE_SESSION", client.t("Cannot resume connection from Tor to non-Tor or vice versa")) return } - if 1 < len(oldClient.Sessions()) { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions")) - return - } - - err := server.clients.Resume(client, oldClient) + err := server.clients.Resume(oldClient, session) if err != nil { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection")) + session.Send(nil, server.name, "FAIL", "RESUME", "CANNOT_RESUME", client.t("Cannot resume connection")) return } success = true + client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick())) - // this is a bit racey - client.resumeDetails.ResumedAt = time.Now().UTC() + return +} - client.nickTimer.Touch(nil) - - // resume successful, proceed to copy client state (nickname, flags, etc.) - // after this, the server thinks that `newClient` owns the nickname - - client.resumeDetails.OldClient = oldClient - - // transfer monitor stuff - server.monitorManager.Resume(client, oldClient) - - // record the names, not the pointers, of the channels, - // to avoid dumb annoying race conditions - channels := oldClient.Channels() - client.resumeDetails.Channels = make([]string, len(channels)) - for i, channel := range channels { - client.resumeDetails.Channels[i] = channel.Name() - } - - username := client.Username() - hostname := client.Hostname() +// playResume is called from the session's fresh goroutine after a resume; +// it sends notifications to friends, then plays the registration burst and replays +// stored history to the session +func (session *Session) playResume() { + client := session.client + server := client.server friends := make(ClientSet) oldestLostMessage := time.Now().UTC() // work out how much time, if any, is not covered by history buffers - for _, channel := range channels { + for _, channel := range client.Channels() { for _, member := range channel.Members() { friends.Add(member) lastDiscarded := channel.history.LastDiscarded() @@ -533,8 +538,8 @@ func (client *Client) tryResume() (success bool) { privmsgMatcher := func(item history.Item) bool { return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg } - privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0) - lastDiscarded := oldClient.history.LastDiscarded() + privmsgHistory := client.history.Match(privmsgMatcher, false, 0) + lastDiscarded := client.history.LastDiscarded() if lastDiscarded.Before(oldestLostMessage) { oldestLostMessage = lastDiscarded } @@ -545,60 +550,61 @@ func (client *Client) tryResume() (success bool) { } } + timestamp := session.resumeDetails.Timestamp gap := lastDiscarded.Sub(timestamp) - client.resumeDetails.HistoryIncomplete = gap > 0 + session.resumeDetails.HistoryIncomplete = gap > 0 gapSeconds := int(gap.Seconds()) + 1 // round up to avoid confusion + details := client.Details() + oldNickmask := details.nickMask + client.SetRawHostname(session.rawHostname) + hostname := client.Hostname() // may be a vhost + timestampString := session.resumeDetails.Timestamp.Format(IRCv3TimestampFormat) + // send quit/resume messages to friends for friend := range friends { - for _, session := range friend.Sessions() { - if session.capabilities.Has(caps.Resume) { + if friend == client { + continue + } + for _, fSession := range friend.Sessions() { + if fSession.capabilities.Has(caps.Resume) { if timestamp.IsZero() { - session.Send(nil, oldNickmask, "RESUMED", username, hostname) + fSession.Send(nil, oldNickmask, "RESUMED", hostname) } else { - session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString) + fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString) } } else { - if client.resumeDetails.HistoryIncomplete { - session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) + if session.resumeDetails.HistoryIncomplete { + fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) } else { - session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) + fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected"))) } } } } - if client.resumeDetails.HistoryIncomplete { - client.Send(nil, client.server.name, "RESUME", "WARN", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds)) + if session.resumeDetails.HistoryIncomplete { + session.Send(nil, client.server.name, "WARN", "RESUME", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds)) } - client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick) + session.Send(nil, client.server.name, "RESUME", "SUCCESS", details.nick) - // after we send the rest of the registration burst, we'll try rejoining channels - return -} + server.playRegistrationBurst(session) -func (client *Client) tryResumeChannels() { - details := client.resumeDetails - - for _, name := range details.Channels { - channel := client.server.channels.Get(name) - if channel == nil { - continue - } - channel.Resume(client, details.OldClient, details.Timestamp) + for _, channel := range client.Channels() { + channel.Resume(session, timestamp) } // replay direct PRIVSMG history - if !details.Timestamp.IsZero() { + if !timestamp.IsZero() { now := time.Now().UTC() - items, complete := client.history.Between(details.Timestamp, now, false, 0) + items, complete := client.history.Between(timestamp, now, false, 0) rb := NewResponseBuffer(client.Sessions()[0]) client.replayPrivmsgHistory(rb, items, complete) rb.Send(true) } - details.OldClient.destroy(true, nil) + session.resumeDetails = nil } func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { @@ -646,41 +652,6 @@ func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.I } } -// copy applicable state from oldClient to client as part of a resume -func (client *Client) copyResumeData(oldClient *Client) { - oldClient.stateMutex.RLock() - history := oldClient.history - nick := oldClient.nick - nickCasefolded := oldClient.nickCasefolded - vhost := oldClient.vhost - account := oldClient.account - accountName := oldClient.accountName - skeleton := oldClient.skeleton - oldClient.stateMutex.RUnlock() - - // copy all flags, *except* TLS (in the case that the admins enabled - // resume over plaintext) - hasTLS := client.flags.HasMode(modes.TLS) - temp := modes.NewModeSet() - temp.Copy(&oldClient.flags) - temp.SetMode(modes.TLS, hasTLS) - client.flags.Copy(temp) - - client.stateMutex.Lock() - defer client.stateMutex.Unlock() - - // reuse the old client's history buffer - client.history = history - // copy other data - client.nick = nick - client.nickCasefolded = nickCasefolded - client.vhost = vhost - client.account = account - client.accountName = accountName - client.skeleton = skeleton - client.updateNickMaskNoMutex() -} - // IdleTime returns how long this client's been idle. func (client *Client) IdleTime() time.Duration { client.stateMutex.RLock() @@ -958,12 +929,13 @@ func (client *Client) Quit(message string, session *Session) { // if `session` is nil, destroys the client unconditionally, removing all sessions; // otherwise, destroys one specific session, only destroying the client if it // has no more sessions. -func (client *Client) destroy(beingResumed bool, session *Session) { +func (client *Client) destroy(session *Session) { var sessionsToDestroy []*Session // allow destroy() to execute at most once client.stateMutex.Lock() details := client.detailsNoMutex() + brbState := client.brbTimer.state wasReattach := session != nil && session.client != client sessionRemoved := false var remainingSessions int @@ -979,10 +951,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) { } client.stateMutex.Unlock() - if len(sessionsToDestroy) == 0 { - return - } - // destroy all applicable sessions: var quitMessage string for _, session := range sessionsToDestroy { @@ -1012,8 +980,8 @@ func (client *Client) destroy(beingResumed bool, session *Session) { 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 { + // do not destroy the client if it has either remaining sessions, or is BRB'ed + if remainingSessions != 0 || brbState == BrbEnabled || brbState == BrbSticky { return } @@ -1022,14 +990,12 @@ func (client *Client) destroy(beingResumed bool, session *Session) { client.server.semaphores.ClientDestroy.Acquire() defer client.server.semaphores.ClientDestroy.Release() - if beingResumed { - client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick)) - } else if !wasReattach { + if !wasReattach { client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick)) } registered := client.Registered() - if !beingResumed && registered { + if registered { client.server.whoWas.Append(client.WhoWas()) } @@ -1047,15 +1013,13 @@ func (client *Client) destroy(beingResumed bool, session *Session) { // (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: details.nickMask, - AccountName: details.accountName, - Message: splitQuitMessage, - }) - } + channel.Quit(client) + channel.history.Add(history.Item{ + Type: history.Quit, + Nick: details.nickMask, + AccountName: details.accountName, + Message: splitQuitMessage, + }) for _, member := range channel.Members() { friends.Add(member) } @@ -1063,40 +1027,34 @@ func (client *Client) destroy(beingResumed bool, session *Session) { friends.Remove(client) // clean up server - if !beingResumed { - client.server.clients.Remove(client) - } + client.server.clients.Remove(client) // clean up self client.nickTimer.Stop() + client.brbTimer.Disable() client.server.accounts.Logout(client) // send quit messages to friends - if !beingResumed { - if registered { - client.server.stats.ChangeTotal(-1) - } - if client.HasMode(modes.Invisible) { - client.server.stats.ChangeInvisible(-1) - } - if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) { - client.server.stats.ChangeOperators(-1) - } - - for friend := range friends { - if quitMessage == "" { - quitMessage = "Exited" - } - friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage) - } + if registered { + client.server.stats.ChangeTotal(-1) } - 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 if registered { - client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick)) + if client.HasMode(modes.Invisible) { + client.server.stats.ChangeInvisible(-1) + } + if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) { + client.server.stats.ChangeOperators(-1) + } + + for friend := range friends { + if quitMessage == "" { + quitMessage = "Exited" } + friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage) + } + + if !client.exitedSnomaskSent && registered { + client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick)) } } diff --git a/irc/client_lookup_set.go b/irc/client_lookup_set.go index 1108f0d1..007d9526 100644 --- a/irc/client_lookup_set.go +++ b/irc/client_lookup_set.go @@ -108,26 +108,21 @@ func (clients *ClientManager) Remove(client *Client) error { return clients.removeInternal(client) } -// Resume atomically replaces `oldClient` with `newClient`, updating -// newClient's data to match. It is the caller's responsibility first -// to verify that the resume is allowed, and then later to call oldClient.destroy(). -func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) { +// Handles a RESUME by attaching a session to a designated client. It is the +// caller's responsibility to verify that the resume is allowed (checking tokens, +// TLS status, etc.) before calling this. +func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) { clients.Lock() defer clients.Unlock() - // atomically grant the new client the old nick - err = clients.removeInternal(oldClient) - if err != nil { - // oldClient no longer owns its nick, fail out - return err + cfnick := oldClient.NickCasefolded() + if _, ok := clients.byNick[cfnick]; !ok { + return errNickMissing } - // nick has been reclaimed, grant it to the new client - clients.removeInternal(newClient) - oldcfnick, oldskeleton := oldClient.uniqueIdentifiers() - clients.byNick[oldcfnick] = newClient - clients.bySkeleton[oldskeleton] = newClient - newClient.copyResumeData(oldClient) + if !oldClient.AddSession(session) { + return errNickMissing + } return nil } @@ -256,27 +251,6 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) { return set } -// Find returns the first client that matches the given userhost mask. -func (clients *ClientManager) Find(userhost string) *Client { - userhost, err := Casefold(ExpandUserHost(userhost)) - if err != nil { - return nil - } - matcher := ircmatch.MakeMatch(userhost) - var matchedClient *Client - - clients.RLock() - defer clients.RUnlock() - for _, client := range clients.byNick { - if matcher.Match(client.NickMaskCasefolded()) { - matchedClient = client - break - } - } - - return matchedClient -} - // // usermask to regexp // diff --git a/irc/commands.go b/irc/commands.go index 84b24ecb..38d50c66 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -39,10 +39,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir return false } - if client.registered { - session.fakelag.Touch() - } - rb := NewResponseBuffer(session) rb.Label = GetLabel(msg) exiting := cmd.handler(server, client, msg, rb) @@ -88,6 +84,10 @@ func init() { handler: awayHandler, minParams: 0, }, + "BRB": { + handler: brbHandler, + minParams: 0, + }, "CAP": { handler: capHandler, usablePreReg: true, diff --git a/irc/config.go b/irc/config.go index 965aa9d5..a3ed6267 100644 --- a/irc/config.go +++ b/irc/config.go @@ -214,16 +214,17 @@ type LineLenLimits struct { // Various server-enforced limits on data size. type Limits struct { - AwayLen int `yaml:"awaylen"` - ChanListModes int `yaml:"chan-list-modes"` - ChannelLen int `yaml:"channellen"` - IdentLen int `yaml:"identlen"` - KickLen int `yaml:"kicklen"` - LineLen LineLenLimits `yaml:"linelen"` - MonitorEntries int `yaml:"monitor-entries"` - NickLen int `yaml:"nicklen"` - TopicLen int `yaml:"topiclen"` - WhowasEntries int `yaml:"whowas-entries"` + AwayLen int `yaml:"awaylen"` + ChanListModes int `yaml:"chan-list-modes"` + ChannelLen int `yaml:"channellen"` + IdentLen int `yaml:"identlen"` + KickLen int `yaml:"kicklen"` + LineLen LineLenLimits `yaml:"linelen"` + MonitorEntries int `yaml:"monitor-entries"` + NickLen int `yaml:"nicklen"` + TopicLen int `yaml:"topiclen"` + WhowasEntries int `yaml:"whowas-entries"` + RegistrationMessages int `yaml:"registration-messages"` } // STSConfig controls the STS configuration/ @@ -334,7 +335,8 @@ type Config struct { Logging []logger.LoggingConfig Debug struct { - RecoverFromErrors *bool `yaml:"recover-from-errors"` + RecoverFromErrors *bool `yaml:"recover-from-errors"` + recoverFromErrors bool PprofListener *string `yaml:"pprof-listener"` } @@ -532,6 +534,9 @@ func LoadConfig(filename string) (config *Config, err error) { if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 { return nil, ErrLimitsAreInsane } + if config.Limits.RegistrationMessages == 0 { + config.Limits.RegistrationMessages = 1024 + } if config.Server.STS.Enabled { config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString) if err != nil { @@ -665,9 +670,10 @@ func LoadConfig(filename string) (config *Config, err error) { } // RecoverFromErrors defaults to true - if config.Debug.RecoverFromErrors == nil { - config.Debug.RecoverFromErrors = new(bool) - *config.Debug.RecoverFromErrors = true + if config.Debug.RecoverFromErrors != nil { + config.Debug.recoverFromErrors = *config.Debug.RecoverFromErrors + } else { + config.Debug.recoverFromErrors = true } // casefold/validate server name diff --git a/irc/getters.go b/irc/getters.go index 13018bba..4d3ad7a8 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -21,22 +21,6 @@ func (server *Server) SetConfig(config *Config) { atomic.StorePointer(&server.config, unsafe.Pointer(config)) } -func (server *Server) Limits() Limits { - return server.Config().Limits -} - -func (server *Server) Password() []byte { - return server.Config().Server.passwordBytes -} - -func (server *Server) RecoverFromErrors() bool { - return *server.Config().Debug.RecoverFromErrors -} - -func (server *Server) DefaultChannelModes() modes.Modes { - return server.Config().Channels.defaultModes -} - func (server *Server) ChannelRegistrationEnabled() bool { return server.Config().Channels.Registration.Enabled } @@ -65,6 +49,18 @@ func (client *Client) Sessions() (sessions []*Session) { return } +func (client *Client) GetSessionByResumeID(resumeID string) (result *Session) { + client.stateMutex.RLock() + defer client.stateMutex.RUnlock() + + for _, session := range client.sessions { + if session.resumeID == resumeID { + return session + } + } + return +} + type SessionData struct { ctime time.Time atime time.Time @@ -100,9 +96,17 @@ func (client *Client) AddSession(session *Session) (success bool) { client.stateMutex.Lock() defer client.stateMutex.Unlock() - if len(client.sessions) == 0 { + // client may be dying and ineligible to receive another session + switch client.brbTimer.state { + case BrbDisabled: + if len(client.sessions) == 0 { + return false + } + case BrbDead: return false + // default: BrbEnabled or BrbSticky, proceed } + // success, attach the new session to the client session.client = client client.sessions = append(client.sessions, session) return true @@ -125,6 +129,12 @@ func (client *Client) removeSession(session *Session) (success bool, length int) return } +func (session *Session) SetResumeID(resumeID string) { + session.client.stateMutex.Lock() + session.resumeID = resumeID + session.client.stateMutex.Unlock() +} + func (client *Client) Nick() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() @@ -161,12 +171,6 @@ func (client *Client) Hostname() string { return client.hostname } -func (client *Client) Realname() string { - client.stateMutex.RLock() - defer client.stateMutex.RUnlock() - return client.realname -} - func (client *Client) Away() (result bool) { client.stateMutex.Lock() result = client.away @@ -233,6 +237,14 @@ func (client *Client) RawHostname() (result string) { return } +func (client *Client) SetRawHostname(rawHostname string) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + + client.rawHostname = rawHostname + client.updateNickMaskNoMutex() +} + func (client *Client) AwayMessage() (result string) { client.stateMutex.RLock() result = client.awayMessage diff --git a/irc/handlers.go b/irc/handlers.go index dbd964f1..d9f18863 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -475,7 +475,7 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp if len(msg.Params) > 0 { isAway = true awayMessage = msg.Params[0] - awayLen := server.Limits().AwayLen + awayLen := server.Config().Limits.AwayLen if len(awayMessage) > awayLen { awayMessage = awayMessage[:awayLen] } @@ -502,6 +502,31 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp return false } +// BRB [message] +func brbHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { + success, duration := client.brbTimer.Enable() + if !success { + rb.Add(nil, server.name, "FAIL", "BRB", "CANNOT_BRB", client.t("Your client does not support BRB")) + return false + } else { + rb.Add(nil, server.name, "BRB", strconv.Itoa(int(duration.Seconds()))) + } + + var message string + if 0 < len(msg.Params) { + message = msg.Params[0] + } else { + message = client.t("I'll be right back") + } + + if len(client.Sessions()) == 1 { + // true BRB + client.SetAway(true, message) + } + + return true +} + // CAP [] func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { subCommand := strings.ToUpper(msg.Params[0]) @@ -568,9 +593,10 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo // if this is the first time the client is requesting a resume token, // send it to them if toAdd.Has(caps.Resume) { - token := server.resumeManager.GenerateToken(client) + token, id := server.resumeManager.GenerateToken(client) if token != "" { rb.Add(nil, server.name, "RESUME", "TOKEN", token) + rb.session.SetResumeID(id) } } @@ -645,7 +671,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r myAccount := client.Account() targetAccount := targetClient.Account() if myAccount != "" && targetAccount != "" && myAccount == targetAccount { - hist = targetClient.history + hist = &targetClient.history } } } @@ -1031,7 +1057,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res killClient = true } else { // if mcl == client, we kill them below - mcl.destroy(false, nil) + mcl.destroy(nil) } } @@ -1094,13 +1120,13 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R hist = &channel.history } else { if strings.ToLower(target) == "me" { - hist = client.history + hist = &client.history } else { targetClient := server.clients.Get(target) if targetClient != nil { myAccount, targetAccount := client.Account(), targetClient.Account() if myAccount != "" && targetAccount != "" && myAccount == targetAccount { - hist = targetClient.history + hist = &targetClient.history } } } @@ -1338,7 +1364,7 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp target.exitedSnomaskSent = true target.Quit(quitMsg, nil) - target.destroy(false, nil) + target.destroy(nil) return false } @@ -1468,7 +1494,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res killClient = true } else { // if mcl == client, we kill them below - mcl.destroy(false, nil) + mcl.destroy(nil) } } @@ -1808,7 +1834,7 @@ func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb var online []string var offline []string - limits := server.Limits() + limits := server.Config().Limits targets := strings.Split(msg.Params[1], ",") for _, target := range targets { @@ -2196,7 +2222,7 @@ func passHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp } // if no password exists, skip checking - serverPassword := server.Password() + serverPassword := server.Config().Server.passwordBytes if serverPassword == nil { return false } @@ -2299,12 +2325,13 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // send RENAME messages clientPrefix := client.NickMaskString() for _, mcl := range channel.Members() { + mDetails := mcl.Details() for _, mSession := range mcl.Sessions() { targetRb := rb targetPrefix := clientPrefix if mSession != rb.session { targetRb = NewResponseBuffer(mSession) - targetPrefix = mcl.NickMaskString() + targetPrefix = mDetails.nickMask } if mSession.capabilities.Has(caps.Rename) { if reason != "" { @@ -2319,7 +2346,7 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed"))) } if mSession.capabilities.Has(caps.ExtendedJoin) { - targetRb.Add(nil, targetPrefix, "JOIN", newName, mcl.AccountName(), mcl.Realname()) + targetRb.Add(nil, targetPrefix, "JOIN", newName, mDetails.accountName, mDetails.realname) } else { targetRb.Add(nil, targetPrefix, "JOIN", newName) } @@ -2337,28 +2364,25 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re // RESUME [timestamp] func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - token := msg.Params[0] + details := ResumeDetails{ + PresentedToken: msg.Params[0], + } if client.registered { - rb.Add(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, connection registration has already been completed")) + rb.Add(nil, server.name, "FAIL", "RESUME", "REGISTRATION_IS_COMPLETED", client.t("Cannot resume connection, connection registration has already been completed")) return false } - var timestamp time.Time if 1 < len(msg.Params) { ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1]) if err == nil { - timestamp = ts + details.Timestamp = ts } else { - rb.Add(nil, server.name, "RESUME", "WARN", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")) + rb.Add(nil, server.name, "WARN", "RESUME", "HISTORY_LOST", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")) } } - client.resumeDetails = &ResumeDetails{ - Timestamp: timestamp, - PresentedToken: token, - } - + rb.session.resumeDetails = &details return false } diff --git a/irc/help.go b/irc/help.go index 07e48bc4..ee64c0f1 100644 --- a/irc/help.go +++ b/irc/help.go @@ -120,6 +120,14 @@ http://ircv3.net/specs/extensions/sasl-3.1.html`, If [message] is sent, marks you away. If [message] is not sent, marks you no longer away.`, + }, + "brb": { + text: `BRB [message] + +Disconnects you from the server, while instructing the server to keep you +present for a short time window. During this window, you can either resume +or reattach to your nickname. If [message] is sent, it is used as your away +message (and as your quit message if you don't return in time).`, }, "cap": { text: `CAP [:] diff --git a/irc/idletimer.go b/irc/idletimer.go index 637d6919..1cda700a 100644 --- a/irc/idletimer.go +++ b/irc/idletimer.go @@ -126,7 +126,7 @@ func (it *IdleTimer) processTimeout() { it.session.Ping() } else { it.session.client.Quit(it.quitMessage(previousState), it.session) - it.session.client.destroy(false, it.session) + it.session.client.destroy(it.session) } } @@ -157,7 +157,11 @@ func (it *IdleTimer) resetTimeout() { case TimerDead: return } - it.timer = time.AfterFunc(nextTimeout, it.processTimeout) + if it.timer != nil { + it.timer.Reset(nextTimeout) + } else { + it.timer = time.AfterFunc(nextTimeout, it.processTimeout) + } } func (it *IdleTimer) quitMessage(state TimerState) string { @@ -300,3 +304,134 @@ func (nt *NickTimer) processTimeout() { nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.Timeout())) nt.client.server.RandomlyRename(nt.client) } + +// BrbTimer is a timer on the client as a whole (not an individual session) for implementing +// the BRB command and related functionality (where a client can remain online without +// having any connected sessions). + +type BrbState uint + +const ( + // BrbDisabled is the default state; the client will be disconnected if it has no sessions + BrbDisabled BrbState = iota + // BrbEnabled allows the client to remain online without sessions; if a timeout is + // reached, it will be removed + BrbEnabled + // BrbDead is the state of a client after its timeout has expired; it will be removed + // and therefore new sessions cannot be attached to it + BrbDead + // BrbSticky allows a client to remain online without sessions, with no timeout. + // This is not used yet. + BrbSticky +) + +type BrbTimer struct { + // XXX we use client.stateMutex for synchronization, so we can atomically test + // conditions that use both brbTimer.state and client.sessions. This code + // is tightly coupled with the rest of Client. + client *Client + + state BrbState + duration time.Duration + timer *time.Timer +} + +func (bt *BrbTimer) Initialize(client *Client) { + bt.client = client +} + +// attempts to enable BRB for a client, returns whether it succeeded +func (bt *BrbTimer) Enable() (success bool, duration time.Duration) { + // BRB only makes sense if a new connection can attach to the session; + // this can happen either via RESUME or via bouncer reattach + if bt.client.Account() == "" && bt.client.ResumeID() == "" { + return + } + + // TODO make this configurable + duration = ResumeableTotalTimeout + + bt.client.stateMutex.Lock() + defer bt.client.stateMutex.Unlock() + + switch bt.state { + case BrbDisabled, BrbEnabled: + bt.state = BrbEnabled + bt.duration = duration + bt.resetTimeout() + success = true + case BrbSticky: + success = true + default: + // BrbDead + success = false + } + return +} + +// turns off BRB for a client and stops the timer; used on resume and during +// client teardown +func (bt *BrbTimer) Disable() { + bt.client.stateMutex.Lock() + defer bt.client.stateMutex.Unlock() + + if bt.state == BrbEnabled { + bt.state = BrbDisabled + } + bt.resetTimeout() +} + +func (bt *BrbTimer) resetTimeout() { + if bt.timer != nil { + bt.timer.Stop() + } + if bt.state != BrbEnabled { + return + } + if bt.timer == nil { + bt.timer = time.AfterFunc(bt.duration, bt.processTimeout) + } else { + bt.timer.Reset(bt.duration) + } +} + +func (bt *BrbTimer) processTimeout() { + dead := false + defer func() { + if dead { + bt.client.Quit(bt.client.AwayMessage(), nil) + bt.client.destroy(nil) + } + }() + + bt.client.stateMutex.Lock() + defer bt.client.stateMutex.Unlock() + + switch bt.state { + case BrbDisabled, BrbEnabled: + if len(bt.client.sessions) == 0 { + // client never returned, quit them + bt.state = BrbDead + dead = true + } else { + // client resumed, reattached, or has another active session + bt.state = BrbDisabled + } + case BrbDead: + dead = true // shouldn't be possible but whatever + } + bt.resetTimeout() +} + +// sets a client to be "sticky", i.e., indefinitely exempt from removal for +// lack of sessions +func (bt *BrbTimer) SetSticky() (success bool) { + bt.client.stateMutex.Lock() + defer bt.client.stateMutex.Unlock() + if bt.state != BrbDead { + success = true + bt.state = BrbSticky + } + bt.resetTimeout() + return +} diff --git a/irc/modes.go b/irc/modes.go index 5919fd20..33db0546 100644 --- a/irc/modes.go +++ b/irc/modes.go @@ -166,7 +166,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c switch change.Op { case modes.Add: - if channel.lists[change.Mode].Length() >= client.server.Limits().ChanListModes { + if channel.lists[change.Mode].Length() >= client.server.Config().Limits.ChanListModes { if !listFullWarned[change.Mode] { rb.Add(nil, client.server.name, ERR_BANLISTFULL, client.Nick(), channel.Name(), change.Mode.String(), client.t("Channel list is full")) listFullWarned[change.Mode] = true diff --git a/irc/monitor.go b/irc/monitor.go index 801d2202..b053b21b 100644 --- a/irc/monitor.go +++ b/irc/monitor.go @@ -77,24 +77,6 @@ func (manager *MonitorManager) Remove(client *Client, nick string) error { return nil } -func (manager *MonitorManager) Resume(newClient, oldClient *Client) error { - manager.Lock() - defer manager.Unlock() - - // newClient is now watching everyone oldClient was watching - oldTargets := manager.watching[oldClient] - delete(manager.watching, oldClient) - manager.watching[newClient] = oldTargets - - // update watchedby as well - for watchedNick := range oldTargets { - delete(manager.watchedby[watchedNick], oldClient) - manager.watchedby[watchedNick][newClient] = true - } - - return nil -} - // RemoveAll unregisters `client` from receiving notifications about *all* nicks. func (manager *MonitorManager) RemoveAll(client *Client) { manager.Lock() diff --git a/irc/nickname.go b/irc/nickname.go index 34917fb4..a1919e59 100644 --- a/irc/nickname.go +++ b/irc/nickname.go @@ -35,7 +35,7 @@ func performNickChange(server *Server, client *Client, target *Client, session * return false } - if err != nil || len(nickname) > server.Limits().NickLen || restrictedNicknames[cfnick] { + if err != nil || len(nickname) > server.Config().Limits.NickLen || restrictedNicknames[cfnick] { rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, nickname, client.t("Erroneous nickname")) return false } diff --git a/irc/nickserv.go b/irc/nickserv.go index 6e8da5c8..aca40d12 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -475,7 +475,7 @@ func nsGhostHandler(server *Server, client *Client, command string, params []str } ghost.Quit(fmt.Sprintf(ghost.t("GHOSTed by %s"), client.Nick()), nil) - ghost.destroy(false, nil) + ghost.destroy(nil) } func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { diff --git a/irc/resume.go b/irc/resume.go index 1556bb02..64c1764c 100644 --- a/irc/resume.go +++ b/irc/resume.go @@ -9,7 +9,7 @@ import ( "github.com/oragono/oragono/irc/utils" ) -// implements draft/resume-0.3, in particular the issuing, management, and verification +// implements draft/resume, in particular the issuing, management, and verification // of resume tokens with two components: a unique ID and a secret key type resumeTokenPair struct { @@ -31,8 +31,8 @@ func (rm *ResumeManager) Initialize(server *Server) { // GenerateToken generates a resume token for a client. If the client has // already been assigned one, it returns "". -func (rm *ResumeManager) GenerateToken(client *Client) (token string) { - id := utils.GenerateSecretToken() +func (rm *ResumeManager) GenerateToken(client *Client) (token string, id string) { + id = utils.GenerateSecretToken() secret := utils.GenerateSecretToken() rm.Lock() @@ -48,13 +48,13 @@ func (rm *ResumeManager) GenerateToken(client *Client) (token string) { secret: secret, } - return id + secret + return id + secret, id } // VerifyToken looks up the client corresponding to a resume token, returning // nil if there is no such client or the token is invalid. If successful, // the token is consumed and cannot be used to resume again. -func (rm *ResumeManager) VerifyToken(token string) (client *Client) { +func (rm *ResumeManager) VerifyToken(newClient *Client, token string) (oldClient *Client, id string) { if len(token) != 2*utils.SecretTokenLength { return } @@ -62,18 +62,32 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) { rm.Lock() defer rm.Unlock() - id := token[:utils.SecretTokenLength] + id = token[:utils.SecretTokenLength] pair, ok := rm.resumeIDtoCreds[id] - if ok { - if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) { - // disallow resume of an unregistered client; this prevents the use of - // resume as an auth bypass - if pair.client.Registered() { - // consume the token, ensuring that at most one resume can succeed - delete(rm.resumeIDtoCreds, id) - return pair.client + if !ok { + return + } + // disallow resume of an unregistered client; this prevents the use of + // resume as an auth bypass + if !pair.client.Registered() { + return + } + + if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) { + oldClient = pair.client // success! + // consume the token, ensuring that at most one resume can succeed + delete(rm.resumeIDtoCreds, id) + // old client is henceforth resumeable under new client's creds (possibly empty) + newResumeID := newClient.ResumeID() + oldClient.SetResumeID(newResumeID) + if newResumeID != "" { + if newResumeCreds, ok := rm.resumeIDtoCreds[newResumeID]; ok { + newResumeCreds.client = oldClient + rm.resumeIDtoCreds[newResumeID] = newResumeCreds } } + // new client no longer "owns" newResumeID, remove the association + newClient.SetResumeID("") } return } diff --git a/irc/server.go b/irc/server.go index 814ddfe1..9266ad7b 100644 --- a/irc/server.go +++ b/irc/server.go @@ -331,42 +331,40 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b // func (server *Server) tryRegister(c *Client, session *Session) { - resumed := false - // try to complete registration, either via RESUME token or normally - if c.resumeDetails != nil { - if !c.tryResume() { - return - } - resumed = true - } else { - if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState { - return - } + // if the session just sent us a RESUME line, try to resume + if session.resumeDetails != nil { + session.tryResume() + return // whether we succeeded or failed, either way `c` is not getting registered + } - // client MUST send PASS if necessary, or authenticate with SASL if necessary, - // before completing the other registration commands - config := server.Config() - if !c.isAuthorized(config) { - c.Quit(c.t("Bad password"), nil) - c.destroy(false, nil) - return - } + // try to complete registration normally + if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState { + return + } - rb := NewResponseBuffer(session) - nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) - rb.Send(true) - if !nickAssigned { - c.preregNick = "" - return - } + // client MUST send PASS if necessary, or authenticate with SASL if necessary, + // before completing the other registration commands + config := server.Config() + if !c.isAuthorized(config) { + c.Quit(c.t("Bad password"), nil) + c.destroy(nil) + return + } - // check KLINEs - isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) - if isBanned { - c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) - c.destroy(false, nil) - return - } + rb := NewResponseBuffer(session) + nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) + rb.Send(true) + if !nickAssigned { + c.preregNick = "" + return + } + + // check KLINEs + isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) + if isBanned { + c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) + c.destroy(nil) + return } if session.client != c { @@ -384,11 +382,7 @@ func (server *Server) tryRegister(c *Client, session *Session) { server.playRegistrationBurst(session) - if resumed { - c.tryResumeChannels() - } else { - server.monitorManager.AlertAbout(c, true) - } + server.monitorManager.AlertAbout(c, true) } func (server *Server) playRegistrationBurst(session *Session) { @@ -503,26 +497,27 @@ func (client *Client) getWhoisOf(target *Client, rb *ResponseBuffer) { // rplWhoReply returns the WHO reply between one user and another channel/user. // ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ] // : -func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *ResponseBuffer) { +func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer) { channelName := "*" flags := "" - if client.Away() { + if target.Away() { flags = "G" } else { flags = "H" } - if client.HasMode(modes.Operator) { + if target.HasMode(modes.Operator) { flags += "*" } if channel != nil { // TODO is this right? - flags += channel.ClientPrefixes(client, rb.session.capabilities.Has(caps.MultiPrefix)) + flags += channel.ClientPrefixes(target, rb.session.capabilities.Has(caps.MultiPrefix)) channelName = channel.name } + details := target.Details() // hardcode a hopcount of 0 for now - rb.Add(nil, target.server.name, RPL_WHOREPLY, target.nick, channelName, client.Username(), client.Hostname(), client.server.name, client.Nick(), flags, "0 "+client.Realname()) + rb.Add(nil, client.server.name, RPL_WHOREPLY, client.Nick(), channelName, details.username, details.hostname, client.server.name, details.nick, flags, "0 "+details.realname) } // rehash reloads the config and applies the changes from the config file. @@ -555,7 +550,7 @@ func (server *Server) applyConfig(config *Config, initial bool) (err error) { server.nameCasefolded = config.Server.nameCasefolded } else { // enforce configs that can't be changed after launch: - currentLimits := server.Limits() + currentLimits := server.Config().Limits 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 { diff --git a/irc/socket.go b/irc/socket.go index f73d3b3d..e7c8887d 100644 --- a/irc/socket.go +++ b/irc/socket.go @@ -254,7 +254,7 @@ func (socket *Socket) performWrite() (closed bool) { socket.Unlock() var err error - if !closed && len(buffers) > 0 { + if 0 < len(buffers) { // on Linux, the runtime will optimize this into a single writev(2) call: _, err = (*net.Buffers)(&buffers).WriteTo(socket.conn) } diff --git a/oragono.yaml b/oragono.yaml index 9c965b23..35754dea 100644 --- a/oragono.yaml +++ b/oragono.yaml @@ -557,6 +557,10 @@ limits: # configurable length for the rest of the message: rest: 2048 + # maximum number of messages to accept during registration (prevents + # DoS / resource exhaustion attacks): + registration-messages: 1024 + # fakelag: prevents clients from spamming commands too rapidly fakelag: # whether to enforce fakelag