3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-10 22:19:31 +01:00

implement draft/resume-0.4

This commit is contained in:
Shivaram Lingamneni 2019-05-21 21:40:25 -04:00
parent eaf0328608
commit 3d445573cf
15 changed files with 442 additions and 343 deletions

View File

@ -113,7 +113,7 @@ CAPDEFS = [
), ),
CapDef( CapDef(
identifier="Resume", 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", url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md",
standard="proposed IRCv3", standard="proposed IRCv3",
), ),

View File

@ -931,7 +931,7 @@ func (am *AccountManager) Unregister(account string) error {
if config.Accounts.RequireSasl.Enabled { if config.Accounts.RequireSasl.Enabled {
client.Quit(client.t("You are no longer authorized to be on this server"), nil) 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 // destroy acquires a semaphore so we can't call it while holding a lock
go client.destroy(false, nil) go client.destroy(nil)
} else { } else {
am.logoutOfAccount(client) am.logoutOfAccount(client)
} }

View File

@ -77,7 +77,7 @@ const (
// https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
Rename Capability = iota 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 // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
Resume Capability = iota Resume Capability = iota
@ -133,7 +133,7 @@ var (
"message-tags", "message-tags",
"multi-prefix", "multi-prefix",
"draft/rename", "draft/rename",
"draft/resume-0.3", "draft/resume-0.4",
"sasl", "sasl",
"server-time", "server-time",
"draft/setname", "draft/setname",

View File

@ -20,6 +20,10 @@ import (
"github.com/oragono/oragono/irc/utils" "github.com/oragono/oragono/irc/utils"
) )
const (
histServMask = "HistServ!HistServ@localhost"
)
// Channel represents a channel that clients can join. // Channel represents a channel that clients can join.
type Channel struct { type Channel struct {
flags modes.ModeSet flags modes.ModeSet
@ -695,46 +699,30 @@ func (channel *Channel) Part(client *Client, message string, rb *ResponseBuffer)
// 1. Replace the old client with the new in the channel's data structures // 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) // 2. Send JOIN and MODE lines to channel participants (including the new client)
// 3. Replay missed message history to the 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() now := time.Now().UTC()
channel.resumeAndAnnounce(newClient, oldClient) channel.resumeAndAnnounce(session)
if !timestamp.IsZero() { if !timestamp.IsZero() {
channel.replayHistoryForResume(newClient, timestamp, now) channel.replayHistoryForResume(session, timestamp, now)
} }
} }
func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) { func (channel *Channel) resumeAndAnnounce(session *Session) {
var oldModeSet *modes.ModeSet channel.stateMutex.RLock()
modeSet := channel.members[session.client]
func() { channel.stateMutex.RUnlock()
channel.joinPartMutex.Lock() if modeSet == nil {
defer channel.joinPartMutex.Unlock() return
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) oldModes := modeSet.String()
channel.members[newClient] = oldModeSet
}()
// construct fake modestring if necessary
oldModes := oldModeSet.String()
if 0 < len(oldModes) { if 0 < len(oldModes) {
oldModes = "+" + oldModes oldModes = "+" + oldModes
} }
// send join for old clients // send join for old clients
nick := newClient.Nick() chname := channel.Name()
nickMask := newClient.NickMaskString() details := session.client.Details()
accountName := newClient.AccountName() realName := session.client.Realname()
realName := newClient.Realname()
for _, member := range channel.Members() { for _, member := range channel.Members() {
for _, session := range member.Sessions() { for _, session := range member.Sessions() {
if session.capabilities.Has(caps.Resume) { if session.capabilities.Has(caps.Resume) {
@ -742,39 +730,36 @@ func (channel *Channel) resumeAndAnnounce(newClient, oldClient *Client) {
} }
if session.capabilities.Has(caps.ExtendedJoin) { if session.capabilities.Has(caps.ExtendedJoin) {
session.Send(nil, nickMask, "JOIN", channel.name, accountName, realName) session.Send(nil, details.nickMask, "JOIN", chname, details.accountName, realName)
} else { } else {
session.Send(nil, nickMask, "JOIN", channel.name) session.Send(nil, details.nickMask, "JOIN", chname)
} }
if 0 < len(oldModes) { 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 // use blocking i/o to synchronize with the later history replay
if rb.session.capabilities.Has(caps.ExtendedJoin) { 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, realName)
} else { } else {
rb.Add(nil, nickMask, "JOIN", channel.name) rb.Add(nil, details.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)
} }
channel.SendTopic(session.client, rb, false)
channel.Names(session.client, rb)
rb.Send(true) 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) items, complete := channel.history.Between(after, before, false, 0)
rb := NewResponseBuffer(newClient.Sessions()[0]) rb := NewResponseBuffer(session)
channel.replayHistoryItems(rb, items, false) channel.replayHistoryItems(rb, items, false)
if !complete && !newClient.resumeDetails.HistoryIncomplete { if !complete && !session.resumeDetails.HistoryIncomplete {
// warn here if we didn't warn already // 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) rb.Send(true)
} }
@ -828,7 +813,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
} else { } else {
message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) 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: case history.Part:
if eventPlayback { if eventPlayback {
@ -838,14 +823,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
continue // #474 continue // #474
} }
message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) 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: case history.Kick:
if eventPlayback { if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message)
} else { } else {
message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message) 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: case history.Quit:
if eventPlayback { if eventPlayback {
@ -855,14 +840,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I
continue // #474 continue // #474
} }
message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) 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: case history.Nick:
if eventPlayback { if eventPlayback {
rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0]) rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0])
} else { } else {
message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) 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)
} }
} }
} }

View File

@ -36,11 +36,8 @@ const (
// the resume process: when handling the RESUME command itself, // the resume process: when handling the RESUME command itself,
// when completing the registration, and when rejoining channels. // when completing the registration, and when rejoining channels.
type ResumeDetails struct { type ResumeDetails struct {
OldClient *Client
PresentedToken string PresentedToken string
Timestamp time.Time Timestamp time.Time
ResumedAt time.Time
Channels []string
HistoryIncomplete bool HistoryIncomplete bool
} }
@ -52,6 +49,7 @@ type Client struct {
atime time.Time atime time.Time
away bool away bool
awayMessage string awayMessage string
brbTimer BrbTimer
certfp string certfp string
channels ChannelSet channels ChannelSet
ctime time.Time ctime time.Time
@ -75,7 +73,6 @@ type Client struct {
realname string realname string
realIP net.IP realIP net.IP
registered bool registered bool
resumeDetails *ResumeDetails
resumeID string resumeID string
saslInProgress bool saslInProgress bool
saslMechanism string saslMechanism string
@ -87,7 +84,7 @@ type Client struct {
stateMutex sync.RWMutex // tier 1 stateMutex sync.RWMutex // tier 1
username string username string
vhost string vhost string
history *history.Buffer history history.Buffer
} }
// Session is an individual client connection to the server (TCP connection // Session is an individual client connection to the server (TCP connection
@ -113,6 +110,9 @@ type Session struct {
maxlenRest uint32 maxlenRest uint32
capState caps.State capState caps.State
capVersion caps.Version capVersion caps.Version
resumeID string
resumeDetails *ResumeDetails
} }
// sets the session quit message, if there isn't one already // sets the session quit message, if there isn't one already
@ -207,8 +207,9 @@ func (server *Server) RunClient(conn clientConn) {
nick: "*", // * is used until actual nick is given nick: "*", // * is used until actual nick is given
nickCasefolded: "*", nickCasefolded: "*",
nickMaskString: "*", // * is used until actual nick is given 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{ session := &Session{
client: client, client: client,
socket: socket, socket: socket,
@ -339,7 +340,7 @@ func (client *Client) run(session *Session) {
} }
} }
// ensure client connection gets closed // ensure client connection gets closed
client.destroy(false, session) client.destroy(session)
}() }()
session.idletimer.Initialize(session) session.idletimer.Initialize(session)
@ -347,7 +348,13 @@ func (client *Client) run(session *Session) {
isReattach := client.Registered() isReattach := client.Registered()
if isReattach { if isReattach {
if session.resumeDetails != nil {
session.playResume()
session.resumeDetails = nil
client.brbTimer.Disable()
} else {
client.playReattachMessages(session) client.playReattachMessages(session)
}
} else { } else {
// don't reset the nick timer during a reattach // don't reset the nick timer during a reattach
client.nickTimer.Initialize(client) client.nickTimer.Initialize(client)
@ -365,6 +372,9 @@ func (client *Client) run(session *Session) {
quitMessage = "readQ exceeded" quitMessage = "readQ exceeded"
} }
client.Quit(quitMessage, session) 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 break
} }
@ -443,83 +453,66 @@ func (session *Session) Ping() {
} }
// tryResume tries to resume if the client asked us to. // tryResume tries to resume if the client asked us to.
func (client *Client) tryResume() (success bool) { func (session *Session) tryResume() (success bool) {
server := client.server var oldResumeID string
config := server.Config()
defer func() { defer func() {
if !success { if success {
client.resumeDetails = nil // "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 client := session.client
var timestampString string server := client.server
if !timestamp.IsZero() { config := server.Config()
timestampString = timestamp.UTC().Format(IRCv3TimestampFormat)
}
oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken) oldClient, oldResumeID := server.resumeManager.VerifyToken(client, session.resumeDetails.PresentedToken)
if oldClient == nil { 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 return
} }
oldNick := oldClient.Nick()
oldNickmask := oldClient.NickMaskString()
resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS)) resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS))
if !resumeAllowed { 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 return
} }
if oldClient.isTor != client.isTor { 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 return
} }
if 1 < len(oldClient.Sessions()) { err := server.clients.Resume(oldClient, session)
client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume a client with multiple attached sessions"))
return
}
err := server.clients.Resume(client, oldClient)
if err != nil { 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 return
} }
success = true success = true
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", oldClient.Nick()))
// this is a bit racey return
client.resumeDetails.ResumedAt = time.Now().UTC()
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() // playResume is called from the session's fresh goroutine after a resume;
hostname := client.Hostname() // 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) friends := make(ClientSet)
oldestLostMessage := time.Now().UTC() oldestLostMessage := time.Now().UTC()
// work out how much time, if any, is not covered by history buffers // 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() { for _, member := range channel.Members() {
friends.Add(member) friends.Add(member)
lastDiscarded := channel.history.LastDiscarded() lastDiscarded := channel.history.LastDiscarded()
@ -531,8 +524,8 @@ func (client *Client) tryResume() (success bool) {
privmsgMatcher := func(item history.Item) bool { privmsgMatcher := func(item history.Item) bool {
return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg return item.Type == history.Privmsg || item.Type == history.Notice || item.Type == history.Tagmsg
} }
privmsgHistory := oldClient.history.Match(privmsgMatcher, false, 0) privmsgHistory := client.history.Match(privmsgMatcher, false, 0)
lastDiscarded := oldClient.history.LastDiscarded() lastDiscarded := client.history.LastDiscarded()
if lastDiscarded.Before(oldestLostMessage) { if lastDiscarded.Before(oldestLostMessage) {
oldestLostMessage = lastDiscarded oldestLostMessage = lastDiscarded
} }
@ -543,60 +536,61 @@ func (client *Client) tryResume() (success bool) {
} }
} }
timestamp := session.resumeDetails.Timestamp
gap := lastDiscarded.Sub(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 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 // send quit/resume messages to friends
for friend := range friends { for friend := range friends {
for _, session := range friend.Sessions() { if friend == client {
if session.capabilities.Has(caps.Resume) {
if timestamp.IsZero() {
session.Send(nil, oldNickmask, "RESUMED", username, hostname)
} else {
session.Send(nil, oldNickmask, "RESUMED", username, 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))
} else {
session.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))
}
client.Send(nil, client.server.name, "RESUME", "SUCCESS", oldNick)
// after we send the rest of the registration burst, we'll try rejoining channels
return
}
func (client *Client) tryResumeChannels() {
details := client.resumeDetails
for _, name := range details.Channels {
channel := client.server.channels.Get(name)
if channel == nil {
continue continue
} }
channel.Resume(client, details.OldClient, details.Timestamp) for _, fSession := range friend.Sessions() {
if fSession.capabilities.Has(caps.Resume) {
if timestamp.IsZero() {
fSession.Send(nil, oldNickmask, "RESUMED", hostname)
} else {
fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
}
} else {
if session.resumeDetails.HistoryIncomplete {
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
} else {
fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected")))
}
}
}
}
if session.resumeDetails.HistoryIncomplete {
session.Send(nil, client.server.name, "RESUME", "WARN", "HISTORY_LOST", fmt.Sprintf(client.t("Resume may have lost up to %d seconds of history"), gapSeconds))
}
session.Send(nil, client.server.name, "RESUME", details.nick)
server.playRegistrationBurst(session)
for _, channel := range client.Channels() {
channel.Resume(session, timestamp)
} }
// replay direct PRIVSMG history // replay direct PRIVSMG history
if !details.Timestamp.IsZero() { if !timestamp.IsZero() {
now := time.Now().UTC() 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]) rb := NewResponseBuffer(client.Sessions()[0])
client.replayPrivmsgHistory(rb, items, complete) client.replayPrivmsgHistory(rb, items, complete)
rb.Send(true) rb.Send(true)
} }
details.OldClient.destroy(true, nil) session.resumeDetails = nil
} }
func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) { func (client *Client) replayPrivmsgHistory(rb *ResponseBuffer, items []history.Item, complete bool) {
@ -644,41 +638,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. // IdleTime returns how long this client's been idle.
func (client *Client) IdleTime() time.Duration { func (client *Client) IdleTime() time.Duration {
client.stateMutex.RLock() client.stateMutex.RLock()
@ -956,12 +915,13 @@ func (client *Client) Quit(message string, session *Session) {
// if `session` is nil, destroys the client unconditionally, removing all sessions; // if `session` is nil, destroys the client unconditionally, removing all sessions;
// otherwise, destroys one specific session, only destroying the client if it // otherwise, destroys one specific session, only destroying the client if it
// has no more sessions. // has no more sessions.
func (client *Client) destroy(beingResumed bool, session *Session) { func (client *Client) destroy(session *Session) {
var sessionsToDestroy []*Session var sessionsToDestroy []*Session
// allow destroy() to execute at most once // allow destroy() to execute at most once
client.stateMutex.Lock() client.stateMutex.Lock()
details := client.detailsNoMutex() details := client.detailsNoMutex()
brbState := client.brbTimer.state
wasReattach := session != nil && session.client != client wasReattach := session != nil && session.client != client
sessionRemoved := false sessionRemoved := false
var remainingSessions int var remainingSessions int
@ -977,10 +937,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
} }
client.stateMutex.Unlock() client.stateMutex.Unlock()
if len(sessionsToDestroy) == 0 {
return
}
// destroy all applicable sessions: // destroy all applicable sessions:
var quitMessage string var quitMessage string
for _, session := range sessionsToDestroy { for _, session := range sessionsToDestroy {
@ -1010,8 +966,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)) 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: // do not destroy the client if it has either remaining sessions, or is BRB'ed
if remainingSessions != 0 { if remainingSessions != 0 || brbState == BrbEnabled || brbState == BrbSticky {
return return
} }
@ -1020,14 +976,12 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
client.server.semaphores.ClientDestroy.Acquire() client.server.semaphores.ClientDestroy.Acquire()
defer client.server.semaphores.ClientDestroy.Release() defer client.server.semaphores.ClientDestroy.Release()
if beingResumed { if !wasReattach {
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", details.nick))
} else if !wasReattach {
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick)) client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", details.nick))
} }
registered := client.Registered() registered := client.Registered()
if !beingResumed && registered { if registered {
client.server.whoWas.Append(client.WhoWas()) client.server.whoWas.Append(client.WhoWas())
} }
@ -1045,7 +999,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
// (note that if this is a reattach, client has no channels and therefore no friends) // (note that if this is a reattach, client has no channels and therefore no friends)
friends := make(ClientSet) friends := make(ClientSet)
for _, channel := range client.Channels() { for _, channel := range client.Channels() {
if !beingResumed {
channel.Quit(client) channel.Quit(client)
channel.history.Add(history.Item{ channel.history.Add(history.Item{
Type: history.Quit, Type: history.Quit,
@ -1053,7 +1006,6 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
AccountName: details.accountName, AccountName: details.accountName,
Message: splitQuitMessage, Message: splitQuitMessage,
}) })
}
for _, member := range channel.Members() { for _, member := range channel.Members() {
friends.Add(member) friends.Add(member)
} }
@ -1061,17 +1013,15 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
friends.Remove(client) friends.Remove(client)
// clean up server // clean up server
if !beingResumed {
client.server.clients.Remove(client) client.server.clients.Remove(client)
}
// clean up self // clean up self
client.nickTimer.Stop() client.nickTimer.Stop()
client.brbTimer.Disable()
client.server.accounts.Logout(client) client.server.accounts.Logout(client)
// send quit messages to friends // send quit messages to friends
if !beingResumed {
if registered { if registered {
client.server.stats.ChangeTotal(-1) client.server.stats.ChangeTotal(-1)
} }
@ -1088,15 +1038,11 @@ func (client *Client) destroy(beingResumed bool, session *Session) {
} }
friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage) friend.sendFromClientInternal(false, splitQuitMessage.Time, splitQuitMessage.Msgid, details.nickMask, details.accountName, nil, "QUIT", quitMessage)
} }
}
if !client.exitedSnomaskSent { if !client.exitedSnomaskSent && registered {
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)) client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick))
} }
} }
}
// SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client. // SendSplitMsgFromClient sends an IRC PRIVMSG/NOTICE coming from a specific client.
// Adds account-tag to the line as well. // Adds account-tag to the line as well.

View File

@ -108,26 +108,21 @@ func (clients *ClientManager) Remove(client *Client) error {
return clients.removeInternal(client) return clients.removeInternal(client)
} }
// Resume atomically replaces `oldClient` with `newClient`, updating // Handles a RESUME by attaching a session to a designated client. It is the
// newClient's data to match. It is the caller's responsibility first // caller's responsibility to verify that the resume is allowed (checking tokens,
// to verify that the resume is allowed, and then later to call oldClient.destroy(). // TLS status, etc.) before calling this.
func (clients *ClientManager) Resume(newClient, oldClient *Client) (err error) { func (clients *ClientManager) Resume(oldClient *Client, session *Session) (err error) {
clients.Lock() clients.Lock()
defer clients.Unlock() defer clients.Unlock()
// atomically grant the new client the old nick cfnick := oldClient.NickCasefolded()
err = clients.removeInternal(oldClient) if _, ok := clients.byNick[cfnick]; !ok {
if err != nil { return errNickMissing
// oldClient no longer owns its nick, fail out
return err
} }
// 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 return nil
} }
@ -256,27 +251,6 @@ func (clients *ClientManager) FindAll(userhost string) (set ClientSet) {
return set 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 // usermask to regexp
// //

View File

@ -88,6 +88,10 @@ func init() {
handler: awayHandler, handler: awayHandler,
minParams: 0, minParams: 0,
}, },
"BRB": {
handler: brbHandler,
minParams: 0,
},
"CAP": { "CAP": {
handler: capHandler, handler: capHandler,
usablePreReg: true, usablePreReg: true,

View File

@ -65,6 +65,18 @@ func (client *Client) Sessions() (sessions []*Session) {
return 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 { type SessionData struct {
ctime time.Time ctime time.Time
atime time.Time atime time.Time
@ -100,9 +112,17 @@ func (client *Client) AddSession(session *Session) (success bool) {
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() defer client.stateMutex.Unlock()
// client may be dying and ineligible to receive another session
switch client.brbTimer.state {
case BrbDisabled:
if len(client.sessions) == 0 { if len(client.sessions) == 0 {
return false return false
} }
case BrbDead:
return false
// default: BrbEnabled or BrbSticky, proceed
}
// success, attach the new session to the client
session.client = client session.client = client
client.sessions = append(client.sessions, session) client.sessions = append(client.sessions, session)
return true return true
@ -125,6 +145,12 @@ func (client *Client) removeSession(session *Session) (success bool, length int)
return return
} }
func (session *Session) SetResumeID(resumeID string) {
session.client.stateMutex.Lock()
session.resumeID = resumeID
session.client.stateMutex.Unlock()
}
func (client *Client) Nick() string { func (client *Client) Nick() string {
client.stateMutex.RLock() client.stateMutex.RLock()
defer client.stateMutex.RUnlock() defer client.stateMutex.RUnlock()
@ -233,6 +259,14 @@ func (client *Client) RawHostname() (result string) {
return 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) { func (client *Client) AwayMessage() (result string) {
client.stateMutex.RLock() client.stateMutex.RLock()
result = client.awayMessage result = client.awayMessage

View File

@ -502,6 +502,31 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
return false 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 <subcmd> [<caps>] // CAP <subcmd> [<caps>]
func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool {
subCommand := strings.ToUpper(msg.Params[0]) 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, // if this is the first time the client is requesting a resume token,
// send it to them // send it to them
if toAdd.Has(caps.Resume) { if toAdd.Has(caps.Resume) {
token := server.resumeManager.GenerateToken(client) token, id := server.resumeManager.GenerateToken(client)
if token != "" { if token != "" {
rb.Add(nil, server.name, "RESUME", "TOKEN", token) rb.Add(nil, server.name, "RESUME", "TOKEN", token)
rb.session.SetResumeID(id)
} }
} }
@ -638,7 +664,7 @@ func chathistoryHandler(server *Server, client *Client, msg ircmsg.IrcMessage, r
myAccount := client.Account() myAccount := client.Account()
targetAccount := targetClient.Account() targetAccount := targetClient.Account()
if myAccount != "" && targetAccount != "" && myAccount == targetAccount { if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
hist = targetClient.history hist = &targetClient.history
} }
} }
} }
@ -1024,7 +1050,7 @@ func dlineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
killClient = true killClient = true
} else { } else {
// if mcl == client, we kill them below // if mcl == client, we kill them below
mcl.destroy(false, nil) mcl.destroy(nil)
} }
} }
@ -1087,13 +1113,13 @@ func historyHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *R
hist = &channel.history hist = &channel.history
} else { } else {
if strings.ToLower(target) == "me" { if strings.ToLower(target) == "me" {
hist = client.history hist = &client.history
} else { } else {
targetClient := server.clients.Get(target) targetClient := server.clients.Get(target)
if targetClient != nil { if targetClient != nil {
myAccount, targetAccount := client.Account(), targetClient.Account() myAccount, targetAccount := client.Account(), targetClient.Account()
if myAccount != "" && targetAccount != "" && myAccount == targetAccount { if myAccount != "" && targetAccount != "" && myAccount == targetAccount {
hist = targetClient.history hist = &targetClient.history
} }
} }
} }
@ -1331,7 +1357,7 @@ func killHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
target.exitedSnomaskSent = true target.exitedSnomaskSent = true
target.Quit(quitMsg, nil) target.Quit(quitMsg, nil)
target.destroy(false, nil) target.destroy(nil)
return false return false
} }
@ -1461,7 +1487,7 @@ func klineHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res
killClient = true killClient = true
} else { } else {
// if mcl == client, we kill them below // if mcl == client, we kill them below
mcl.destroy(false, nil) mcl.destroy(nil)
} }
} }
@ -2326,28 +2352,25 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
// RESUME <token> [timestamp] // RESUME <token> [timestamp]
func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { 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 { 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 return false
} }
var timestamp time.Time
if 1 < len(msg.Params) { if 1 < len(msg.Params) {
ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1]) ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1])
if err == nil { if err == nil {
timestamp = ts details.Timestamp = ts
} else { } 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{ rb.session.resumeDetails = &details
Timestamp: timestamp,
PresentedToken: token,
}
return false return false
} }

View File

@ -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 If [message] is sent, marks you away. If [message] is not sent, marks you no
longer away.`, 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": { "cap": {
text: `CAP <subcommand> [:<capabilities>] text: `CAP <subcommand> [:<capabilities>]

View File

@ -126,7 +126,7 @@ func (it *IdleTimer) processTimeout() {
it.session.Ping() it.session.Ping()
} else { } else {
it.session.client.Quit(it.quitMessage(previousState), it.session) it.session.client.Quit(it.quitMessage(previousState), it.session)
it.session.client.destroy(false, it.session) it.session.client.destroy(it.session)
} }
} }
@ -157,8 +157,12 @@ func (it *IdleTimer) resetTimeout() {
case TimerDead: case TimerDead:
return return
} }
if it.timer != nil {
it.timer.Reset(nextTimeout)
} else {
it.timer = time.AfterFunc(nextTimeout, it.processTimeout) it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
} }
}
func (it *IdleTimer) quitMessage(state TimerState) string { func (it *IdleTimer) quitMessage(state TimerState) string {
switch state { switch state {
@ -300,3 +304,134 @@ func (nt *NickTimer) processTimeout() {
nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.Timeout())) nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.Timeout()))
nt.client.server.RandomlyRename(nt.client) 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
}

View File

@ -77,24 +77,6 @@ func (manager *MonitorManager) Remove(client *Client, nick string) error {
return nil 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. // RemoveAll unregisters `client` from receiving notifications about *all* nicks.
func (manager *MonitorManager) RemoveAll(client *Client) { func (manager *MonitorManager) RemoveAll(client *Client) {
manager.Lock() manager.Lock()

View File

@ -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.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) { func nsGroupHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) {

View File

@ -9,7 +9,7 @@ import (
"github.com/oragono/oragono/irc/utils" "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 // of resume tokens with two components: a unique ID and a secret key
type resumeTokenPair struct { 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 // GenerateToken generates a resume token for a client. If the client has
// already been assigned one, it returns "". // already been assigned one, it returns "".
func (rm *ResumeManager) GenerateToken(client *Client) (token string) { func (rm *ResumeManager) GenerateToken(client *Client) (token string, id string) {
id := utils.GenerateSecretToken() id = utils.GenerateSecretToken()
secret := utils.GenerateSecretToken() secret := utils.GenerateSecretToken()
rm.Lock() rm.Lock()
@ -48,13 +48,13 @@ func (rm *ResumeManager) GenerateToken(client *Client) (token string) {
secret: secret, secret: secret,
} }
return id + secret return id + secret, id
} }
// VerifyToken looks up the client corresponding to a resume token, returning // 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, // 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. // 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 { if len(token) != 2*utils.SecretTokenLength {
return return
} }
@ -62,18 +62,32 @@ func (rm *ResumeManager) VerifyToken(token string) (client *Client) {
rm.Lock() rm.Lock()
defer rm.Unlock() defer rm.Unlock()
id := token[:utils.SecretTokenLength] id = token[:utils.SecretTokenLength]
pair, ok := rm.resumeIDtoCreds[id] pair, ok := rm.resumeIDtoCreds[id]
if ok { if !ok {
if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) { return
}
// disallow resume of an unregistered client; this prevents the use of // disallow resume of an unregistered client; this prevents the use of
// resume as an auth bypass // resume as an auth bypass
if pair.client.Registered() { 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 // consume the token, ensuring that at most one resume can succeed
delete(rm.resumeIDtoCreds, id) delete(rm.resumeIDtoCreds, id)
return pair.client // 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 return
} }

View File

@ -331,14 +331,13 @@ func (server *Server) createListener(addr string, tlsConfig *tls.Config, isTor b
// //
func (server *Server) tryRegister(c *Client, session *Session) { func (server *Server) tryRegister(c *Client, session *Session) {
resumed := false // if the session just sent us a RESUME line, try to resume
// try to complete registration, either via RESUME token or normally if session.resumeDetails != nil {
if c.resumeDetails != nil { session.tryResume()
if !c.tryResume() { return // whether we succeeded or failed, either way `c` is not getting registered
return
} }
resumed = true
} else { // try to complete registration normally
if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState { if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
return return
} }
@ -348,7 +347,7 @@ func (server *Server) tryRegister(c *Client, session *Session) {
config := server.Config() config := server.Config()
if !c.isAuthorized(config) { if !c.isAuthorized(config) {
c.Quit(c.t("Bad password"), nil) c.Quit(c.t("Bad password"), nil)
c.destroy(false, nil) c.destroy(nil)
return return
} }
@ -364,10 +363,9 @@ func (server *Server) tryRegister(c *Client, session *Session) {
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...)
if isBanned { if isBanned {
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil)
c.destroy(false, nil) c.destroy(nil)
return return
} }
}
if session.client != c { if session.client != c {
// reattached, bail out. // reattached, bail out.
@ -384,12 +382,8 @@ func (server *Server) tryRegister(c *Client, session *Session) {
server.playRegistrationBurst(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) { func (server *Server) playRegistrationBurst(session *Session) {
c := session.client c := session.client