3
0
mirror of https://github.com/ergochat/ergo.git synced 2024-11-29 15:40:02 +01:00

Merge pull request #509 from slingamn/brb.5

implement draft/resume-0.4
This commit is contained in:
Daniel Oaks 2019-05-24 11:06:11 +10:00 committed by GitHub
commit a27c46f983
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 500 additions and 403 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
@ -141,7 +141,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
@ -664,10 +668,11 @@ func (channel *Channel) Join(client *Client, key string, isSajoin bool, rb *Resp
func (channel *Channel) playJoinForSession(session *Session) { func (channel *Channel) playJoinForSession(session *Session) {
client := session.client client := session.client
sessionRb := NewResponseBuffer(session) sessionRb := NewResponseBuffer(session)
details := client.Details()
if session.capabilities.Has(caps.ExtendedJoin) { 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 { } else {
sessionRb.Add(nil, client.NickMaskString(), "JOIN", channel.Name()) sessionRb.Add(nil, details.nickMask, "JOIN", channel.Name())
} }
channel.SendTopic(client, sessionRb, false) channel.SendTopic(client, sessionRb, false)
channel.Names(client, sessionRb) 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 // 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() oldModes := modeSet.String()
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()
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 := 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) {
@ -758,39 +746,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, details.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, details.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)
} }
@ -844,7 +829,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 {
@ -854,14 +839,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 {
@ -871,14 +856,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)
} }
} }
} }
@ -923,7 +908,7 @@ func (channel *Channel) SetTopic(client *Client, topic string, rb *ResponseBuffe
return return
} }
topicLimit := client.server.Limits().TopicLen topicLimit := client.server.Config().Limits.TopicLen
if len(topic) > topicLimit { if len(topic) > topicLimit {
topic = topic[:topicLimit] topic = topic[:topicLimit]
} }
@ -1152,7 +1137,7 @@ func (channel *Channel) Kick(client *Client, target *Client, comment string, rb
return return
} }
kicklimit := client.server.Limits().KickLen kicklimit := client.server.Config().Limits.KickLen
if len(comment) > kicklimit { if len(comment) > kicklimit {
comment = comment[:kicklimit] comment = comment[:kicklimit]
} }

View File

@ -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 { func (cm *ChannelManager) Join(client *Client, name string, key string, isSajoin bool, rb *ResponseBuffer) error {
server := client.server server := client.server
casefoldedName, err := CasefoldChannel(name) casefoldedName, err := CasefoldChannel(name)
if err != nil || len(casefoldedName) > server.Limits().ChannelLen { if err != nil || len(casefoldedName) > server.Config().Limits.ChannelLen {
return errNoSuchChannel return errNoSuchChannel
} }

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
@ -114,6 +111,10 @@ type Session struct {
capState caps.State capState caps.State
capVersion caps.Version capVersion caps.Version
registrationMessages int
resumeID string
resumeDetails *ResumeDetails
zncPlaybackTimes *zncPlaybackTimes zncPlaybackTimes *zncPlaybackTimes
} }
@ -209,8 +210,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,
@ -334,14 +336,14 @@ func (client *Client) run(session *Session) {
if r := recover(); r != nil { if r := recover(); r != nil {
client.server.logger.Error("internal", client.server.logger.Error("internal",
fmt.Sprintf("Client caused panic: %v\n%s", r, debug.Stack())) 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") client.server.logger.Error("internal", "Disconnecting client and attempting to recover")
} else { } else {
panic(r) panic(r)
} }
} }
// 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)
@ -349,7 +351,13 @@ func (client *Client) run(session *Session) {
isReattach := client.Registered() isReattach := client.Registered()
if isReattach { if isReattach {
client.playReattachMessages(session) if session.resumeDetails != nil {
session.playResume()
session.resumeDetails = nil
client.brbTimer.Disable()
} else {
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)
@ -367,6 +375,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
} }
@ -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) msg, err := ircmsg.ParseLineStrict(line, true, maxlenRest)
if err == ircmsg.ErrorLineIsEmpty { if err == ircmsg.ErrorLineIsEmpty {
continue continue
@ -445,83 +467,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) // playResume is called from the session's fresh goroutine after a resume;
// it sends notifications to friends, then plays the registration burst and replays
// resume successful, proceed to copy client state (nickname, flags, etc.) // stored history to the session
// after this, the server thinks that `newClient` owns the nickname func (session *Session) playResume() {
client := session.client
client.resumeDetails.OldClient = oldClient server := client.server
// 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()
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()
@ -533,8 +538,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
} }
@ -545,60 +550,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) { continue
}
for _, fSession := range friend.Sessions() {
if fSession.capabilities.Has(caps.Resume) {
if timestamp.IsZero() { if timestamp.IsZero() {
session.Send(nil, oldNickmask, "RESUMED", username, hostname) fSession.Send(nil, oldNickmask, "RESUMED", hostname)
} else { } else {
session.Send(nil, oldNickmask, "RESUMED", username, hostname, timestampString) fSession.Send(nil, oldNickmask, "RESUMED", hostname, timestampString)
} }
} else { } else {
if client.resumeDetails.HistoryIncomplete { if session.resumeDetails.HistoryIncomplete {
session.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds)) fSession.Send(nil, oldNickmask, "QUIT", fmt.Sprintf(friend.t("Client reconnected (up to %d seconds of history lost)"), gapSeconds))
} else { } 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 { if session.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)) 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 server.playRegistrationBurst(session)
return
}
func (client *Client) tryResumeChannels() { for _, channel := range client.Channels() {
details := client.resumeDetails channel.Resume(session, timestamp)
for _, name := range details.Channels {
channel := client.server.channels.Get(name)
if channel == nil {
continue
}
channel.Resume(client, details.OldClient, details.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) {
@ -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. // 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()
@ -958,12 +929,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
@ -979,10 +951,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 {
@ -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)) 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
} }
@ -1022,14 +990,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())
} }
@ -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) // (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, Nick: details.nickMask,
Nick: details.nickMask, 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)
} }
@ -1063,40 +1027,34 @@ 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)
}
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 { if client.HasMode(modes.Invisible) {
if beingResumed { client.server.stats.ChangeInvisible(-1)
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 { if client.HasMode(modes.Operator) || client.HasMode(modes.LocalOperator) {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), details.nick)) 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))
} }
} }

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

@ -39,10 +39,6 @@ func (cmd *Command) Run(server *Server, client *Client, session *Session, msg ir
return false return false
} }
if client.registered {
session.fakelag.Touch()
}
rb := NewResponseBuffer(session) rb := NewResponseBuffer(session)
rb.Label = GetLabel(msg) rb.Label = GetLabel(msg)
exiting := cmd.handler(server, client, msg, rb) exiting := cmd.handler(server, client, msg, rb)
@ -88,6 +84,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

@ -214,16 +214,17 @@ type LineLenLimits struct {
// Various server-enforced limits on data size. // Various server-enforced limits on data size.
type Limits struct { type Limits struct {
AwayLen int `yaml:"awaylen"` AwayLen int `yaml:"awaylen"`
ChanListModes int `yaml:"chan-list-modes"` ChanListModes int `yaml:"chan-list-modes"`
ChannelLen int `yaml:"channellen"` ChannelLen int `yaml:"channellen"`
IdentLen int `yaml:"identlen"` IdentLen int `yaml:"identlen"`
KickLen int `yaml:"kicklen"` KickLen int `yaml:"kicklen"`
LineLen LineLenLimits `yaml:"linelen"` LineLen LineLenLimits `yaml:"linelen"`
MonitorEntries int `yaml:"monitor-entries"` MonitorEntries int `yaml:"monitor-entries"`
NickLen int `yaml:"nicklen"` NickLen int `yaml:"nicklen"`
TopicLen int `yaml:"topiclen"` TopicLen int `yaml:"topiclen"`
WhowasEntries int `yaml:"whowas-entries"` WhowasEntries int `yaml:"whowas-entries"`
RegistrationMessages int `yaml:"registration-messages"`
} }
// STSConfig controls the STS configuration/ // STSConfig controls the STS configuration/
@ -334,7 +335,8 @@ type Config struct {
Logging []logger.LoggingConfig Logging []logger.LoggingConfig
Debug struct { Debug struct {
RecoverFromErrors *bool `yaml:"recover-from-errors"` RecoverFromErrors *bool `yaml:"recover-from-errors"`
recoverFromErrors bool
PprofListener *string `yaml:"pprof-listener"` 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 { if config.Limits.NickLen < 1 || config.Limits.ChannelLen < 2 || config.Limits.AwayLen < 1 || config.Limits.KickLen < 1 || config.Limits.TopicLen < 1 {
return nil, ErrLimitsAreInsane return nil, ErrLimitsAreInsane
} }
if config.Limits.RegistrationMessages == 0 {
config.Limits.RegistrationMessages = 1024
}
if config.Server.STS.Enabled { if config.Server.STS.Enabled {
config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString) config.Server.STS.Duration, err = custime.ParseDuration(config.Server.STS.DurationString)
if err != nil { if err != nil {
@ -665,9 +670,10 @@ func LoadConfig(filename string) (config *Config, err error) {
} }
// RecoverFromErrors defaults to true // RecoverFromErrors defaults to true
if config.Debug.RecoverFromErrors == nil { if config.Debug.RecoverFromErrors != nil {
config.Debug.RecoverFromErrors = new(bool) config.Debug.recoverFromErrors = *config.Debug.RecoverFromErrors
*config.Debug.RecoverFromErrors = true } else {
config.Debug.recoverFromErrors = true
} }
// casefold/validate server name // casefold/validate server name

View File

@ -21,22 +21,6 @@ func (server *Server) SetConfig(config *Config) {
atomic.StorePointer(&server.config, unsafe.Pointer(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 { func (server *Server) ChannelRegistrationEnabled() bool {
return server.Config().Channels.Registration.Enabled return server.Config().Channels.Registration.Enabled
} }
@ -65,6 +49,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 +96,17 @@ func (client *Client) AddSession(session *Session) (success bool) {
client.stateMutex.Lock() client.stateMutex.Lock()
defer client.stateMutex.Unlock() 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 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 +129,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()
@ -161,12 +171,6 @@ func (client *Client) Hostname() string {
return client.hostname return client.hostname
} }
func (client *Client) Realname() string {
client.stateMutex.RLock()
defer client.stateMutex.RUnlock()
return client.realname
}
func (client *Client) Away() (result bool) { func (client *Client) Away() (result bool) {
client.stateMutex.Lock() client.stateMutex.Lock()
result = client.away result = client.away
@ -233,6 +237,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

@ -475,7 +475,7 @@ func awayHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Resp
if len(msg.Params) > 0 { if len(msg.Params) > 0 {
isAway = true isAway = true
awayMessage = msg.Params[0] awayMessage = msg.Params[0]
awayLen := server.Limits().AwayLen awayLen := server.Config().Limits.AwayLen
if len(awayMessage) > awayLen { if len(awayMessage) > awayLen {
awayMessage = awayMessage[:awayLen] awayMessage = awayMessage[:awayLen]
} }
@ -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)
} }
} }
@ -645,7 +671,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
} }
} }
} }
@ -1031,7 +1057,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)
} }
} }
@ -1094,13 +1120,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
} }
} }
} }
@ -1338,7 +1364,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
} }
@ -1468,7 +1494,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)
} }
} }
@ -1808,7 +1834,7 @@ func monitorAddHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb
var online []string var online []string
var offline []string var offline []string
limits := server.Limits() limits := server.Config().Limits
targets := strings.Split(msg.Params[1], ",") targets := strings.Split(msg.Params[1], ",")
for _, target := range targets { 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 // if no password exists, skip checking
serverPassword := server.Password() serverPassword := server.Config().Server.passwordBytes
if serverPassword == nil { if serverPassword == nil {
return false return false
} }
@ -2299,12 +2325,13 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re
// send RENAME messages // send RENAME messages
clientPrefix := client.NickMaskString() clientPrefix := client.NickMaskString()
for _, mcl := range channel.Members() { for _, mcl := range channel.Members() {
mDetails := mcl.Details()
for _, mSession := range mcl.Sessions() { for _, mSession := range mcl.Sessions() {
targetRb := rb targetRb := rb
targetPrefix := clientPrefix targetPrefix := clientPrefix
if mSession != rb.session { if mSession != rb.session {
targetRb = NewResponseBuffer(mSession) targetRb = NewResponseBuffer(mSession)
targetPrefix = mcl.NickMaskString() targetPrefix = mDetails.nickMask
} }
if mSession.capabilities.Has(caps.Rename) { if mSession.capabilities.Has(caps.Rename) {
if reason != "" { 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"))) targetRb.Add(nil, targetPrefix, "PART", oldName, fmt.Sprintf(mcl.t("Channel renamed")))
} }
if mSession.capabilities.Has(caps.ExtendedJoin) { 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 { } else {
targetRb.Add(nil, targetPrefix, "JOIN", newName) targetRb.Add(nil, targetPrefix, "JOIN", newName)
} }
@ -2337,28 +2364,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,7 +157,11 @@ func (it *IdleTimer) resetTimeout() {
case TimerDead: case TimerDead:
return 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 { 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.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

@ -166,7 +166,7 @@ func (channel *Channel) ApplyChannelModeChanges(client *Client, isSamode bool, c
switch change.Op { switch change.Op {
case modes.Add: 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] { 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")) 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 listFullWarned[change.Mode] = true

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

@ -35,7 +35,7 @@ func performNickChange(server *Server, client *Client, target *Client, session *
return false 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")) rb.Add(nil, server.name, ERR_ERRONEUSNICKNAME, currentNick, nickname, client.t("Erroneous nickname"))
return false return false
} }

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 }
// resume as an auth bypass // disallow resume of an unregistered client; this prevents the use of
if pair.client.Registered() { // resume as an auth bypass
// consume the token, ensuring that at most one resume can succeed if !pair.client.Registered() {
delete(rm.resumeIDtoCreds, id) return
return pair.client }
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 return
} }

View File

@ -331,42 +331,40 @@ 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 {
if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
return
}
// client MUST send PASS if necessary, or authenticate with SASL if necessary, // try to complete registration normally
// before completing the other registration commands if c.preregNick == "" || !c.HasUsername() || session.capState == caps.NegotiatingState {
config := server.Config() return
if !c.isAuthorized(config) { }
c.Quit(c.t("Bad password"), nil)
c.destroy(false, nil)
return
}
rb := NewResponseBuffer(session) // client MUST send PASS if necessary, or authenticate with SASL if necessary,
nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb) // before completing the other registration commands
rb.Send(true) config := server.Config()
if !nickAssigned { if !c.isAuthorized(config) {
c.preregNick = "" c.Quit(c.t("Bad password"), nil)
return c.destroy(nil)
} return
}
// check KLINEs rb := NewResponseBuffer(session)
isBanned, info := server.klines.CheckMasks(c.AllNickmasks()...) nickAssigned := performNickChange(server, c, c, session, c.preregNick, rb)
if isBanned { rb.Send(true)
c.Quit(info.BanMessage(c.t("You are banned from this server (%s)")), nil) if !nickAssigned {
c.destroy(false, nil) c.preregNick = ""
return 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 { if session.client != c {
@ -384,11 +382,7 @@ func (server *Server) tryRegister(c *Client, session *Session) {
server.playRegistrationBurst(session) server.playRegistrationBurst(session)
if resumed { server.monitorManager.AlertAbout(c, true)
c.tryResumeChannels()
} else {
server.monitorManager.AlertAbout(c, true)
}
} }
func (server *Server) playRegistrationBurst(session *Session) { 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. // rplWhoReply returns the WHO reply between one user and another channel/user.
// <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ] // <channel> <user> <host> <server> <nick> ( "H" / "G" ) ["*"] [ ( "@" / "+" ) ]
// :<hopcount> <real name> // :<hopcount> <real name>
func (target *Client) rplWhoReply(channel *Channel, client *Client, rb *ResponseBuffer) { func (client *Client) rplWhoReply(channel *Channel, target *Client, rb *ResponseBuffer) {
channelName := "*" channelName := "*"
flags := "" flags := ""
if client.Away() { if target.Away() {
flags = "G" flags = "G"
} else { } else {
flags = "H" flags = "H"
} }
if client.HasMode(modes.Operator) { if target.HasMode(modes.Operator) {
flags += "*" flags += "*"
} }
if channel != nil { if channel != nil {
// TODO is this right? // 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 channelName = channel.name
} }
details := target.Details()
// hardcode a hopcount of 0 for now // 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. // 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 server.nameCasefolded = config.Server.nameCasefolded
} else { } else {
// enforce configs that can't be changed after launch: // enforce configs that can't be changed after launch:
currentLimits := server.Limits() currentLimits := server.Config().Limits
if currentLimits.LineLen.Rest != config.Limits.LineLen.Rest { if currentLimits.LineLen.Rest != config.Limits.LineLen.Rest {
return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted") return fmt.Errorf("Maximum line length (linelen) cannot be changed after launching the server, rehash aborted")
} else if server.name != config.Server.Name { } else if server.name != config.Server.Name {

View File

@ -254,7 +254,7 @@ func (socket *Socket) performWrite() (closed bool) {
socket.Unlock() socket.Unlock()
var err error 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: // on Linux, the runtime will optimize this into a single writev(2) call:
_, err = (*net.Buffers)(&buffers).WriteTo(socket.conn) _, err = (*net.Buffers)(&buffers).WriteTo(socket.conn)
} }

View File

@ -557,6 +557,10 @@ limits:
# configurable length for the rest of the message: # configurable length for the rest of the message:
rest: 2048 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: prevents clients from spamming commands too rapidly
fakelag: fakelag:
# whether to enforce fakelag # whether to enforce fakelag