3
0
mirror of https://github.com/ergochat/ergo.git synced 2025-01-03 08:32:43 +01:00

Add very initial RESUME cap and command

This commit is contained in:
Daniel Oaks 2018-01-21 11:23:33 +10:00
parent eb25d4466b
commit d09f085b1a
8 changed files with 199 additions and 20 deletions

View File

@ -14,7 +14,7 @@ import (
var ( var (
// SupportedCapabilities are the caps we advertise. // SupportedCapabilities are the caps we advertise.
// MaxLine, SASL and STS are set during server startup. // MaxLine, SASL and STS are set during server startup.
SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.ServerTime, caps.UserhostInNames) SupportedCapabilities = caps.NewSet(caps.AccountTag, caps.AccountNotify, caps.AwayNotify, caps.CapNotify, caps.ChgHost, caps.EchoMessage, caps.ExtendedJoin, caps.InviteNotify, caps.Languages, caps.MessageTags, caps.MultiPrefix, caps.Rename, caps.Resume, caps.ServerTime, caps.UserhostInNames)
// CapValues are the actual values we advertise to v3.2 clients. // CapValues are the actual values we advertise to v3.2 clients.
// actual values are set during server startup. // actual values are set during server startup.

View File

@ -37,6 +37,8 @@ const (
MultiPrefix Capability = "multi-prefix" MultiPrefix Capability = "multi-prefix"
// Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md // Rename is this proposed capability: https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md
Rename Capability = "draft/rename" Rename Capability = "draft/rename"
// Resume is this proposed capability: https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md
Resume Capability = "draft/resume"
// SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html // SASL is this IRCv3 capability: http://ircv3.net/specs/extensions/sasl-3.2.html
SASL Capability = "sasl" SASL Capability = "sasl"
// ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html // ServerTime is this IRCv3 capability: http://ircv3.net/specs/extensions/server-time-3.2.html

View File

@ -69,6 +69,7 @@ type Client struct {
rawHostname string rawHostname string
realname string realname string
registered bool registered bool
resumeDetails *ResumeDetails
saslInProgress bool saslInProgress bool
saslMechanism string saslMechanism string
saslValue string saslValue string
@ -294,11 +295,109 @@ func (client *Client) Register() {
return return
} }
// apply resume details if we're able to.
client.TryResume()
// finish registration
client.Touch() client.Touch()
client.updateNickMask("") client.updateNickMask("")
client.server.monitorManager.AlertAbout(client, true) client.server.monitorManager.AlertAbout(client, true)
} }
// TryResume tries to resume if the client asked us to.
func (client *Client) TryResume() {
if client.resumeDetails == nil {
return
}
server := client.server
// just grab these mutexes for safety. later we can work out whether we can grab+release them earlier
server.clients.Lock()
defer server.clients.Unlock()
server.channels.Lock()
defer server.channels.Unlock()
oldnick := client.resumeDetails.OldNick
timestamp := client.resumeDetails.Timestamp
var timestampString string
if timestamp != nil {
timestampString := timestamp.UTC().Format("2006-01-02T15:04:05.999Z")
}
oldClient := server.clients.Get(oldnick)
if oldClient == nil {
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old client not found")
return
}
oldAccountName := oldClient.AccountName()
newAccountName := client.AccountName()
if oldAccountName == "" || newAccountName == "" || oldAccountName != newAccountName {
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old and new clients must be logged into the same account")
return
}
if !oldClient.HasMode(TLS) || !client.HasMode(TLS) {
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, old and new clients must have TLS")
return
}
// send RESUMED to the reconnecting client
if timestamp == nil {
client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
} else {
client.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
}
// send QUIT/RESUMED to friends
for friend := range oldClient.Friends() {
if friend.capabilities.Has(caps.Resume) {
if timestamp == nil {
friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname())
} else {
friend.Send(nil, oldClient.NickMaskString(), "RESUMED", oldClient.nick, client.username, client.Hostname(), timestampString)
}
} else {
friend.Send(nil, oldClient.NickMaskString(), "QUIT", "Client reconnected")
}
}
// apply old client's details to new client
client.nick = oldClient.nick
for channel := range oldClient.channels {
channel.stateMutex.Lock()
oldModeSet := channel.members[oldClient]
channel.members.Remove(oldClient)
channel.members[client] = oldModeSet
channel.regenerateMembersCache()
// send join for old clients
for member := range channel.members {
if member.capabilities.Has(caps.Resume) {
continue
}
if member.capabilities.Has(caps.ExtendedJoin) {
member.Send(nil, client.nickMaskString, "JOIN", channel.name, client.account.Name, client.realname)
} else {
member.Send(nil, client.nickMaskString, "JOIN", channel.name)
}
//TODO(dan): send priv modes
}
channel.stateMutex.Unlock()
}
server.clients.byNick[oldnick] = client
oldClient.destroy()
}
// 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()
@ -494,7 +593,7 @@ func (client *Client) Quit(message string) {
} }
// destroy gets rid of a client, removes them from server lists etc. // destroy gets rid of a client, removes them from server lists etc.
func (client *Client) destroy() { func (client *Client) destroy(beingResumed bool) {
// allow destroy() to execute at most once // allow destroy() to execute at most once
client.stateMutex.Lock() client.stateMutex.Lock()
isDestroyed := client.isDestroyed isDestroyed := client.isDestroyed
@ -504,14 +603,20 @@ func (client *Client) destroy() {
return return
} }
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick)) if beingResumed {
client.server.logger.Debug("quit", fmt.Sprintf("%s is being resumed", client.nick))
} else {
client.server.logger.Debug("quit", fmt.Sprintf("%s is no longer on the server", client.nick))
}
// send quit/error message to client if they haven't been sent already // send quit/error message to client if they haven't been sent already
client.Quit("Connection closed") client.Quit("Connection closed")
client.server.whoWas.Append(client)
friends := client.Friends() friends := client.Friends()
friends.Remove(client) friends.Remove(client)
if !beingResumed {
client.server.whoWas.Append(client)
}
// remove from connection limits // remove from connection limits
ipaddr := client.IP() ipaddr := client.IP()
@ -527,14 +632,18 @@ func (client *Client) destroy() {
// clean up channels // clean up channels
for _, channel := range client.Channels() { for _, channel := range client.Channels() {
channel.Quit(client) if !beingResumed {
channel.Quit(client)
}
for _, member := range channel.Members() { for _, member := range channel.Members() {
friends.Add(member) friends.Add(member)
} }
} }
// clean up server // clean up server
client.server.clients.Remove(client) if !beingResumed {
client.server.clients.Remove(client)
}
// clean up self // clean up self
if client.idletimer != nil { if client.idletimer != nil {
@ -544,14 +653,20 @@ func (client *Client) destroy() {
client.socket.Close() client.socket.Close()
// send quit messages to friends // send quit messages to friends
for friend := range friends { if !beingResumed {
if client.quitMessage == "" { for friend := range friends {
client.quitMessage = "Exited" if client.quitMessage == "" {
client.quitMessage = "Exited"
}
friend.Send(nil, client.nickMaskString, "QUIT", client.quitMessage)
} }
friend.Send(nil, client.nickMaskString, "QUIT", client.quitMessage)
} }
if !client.exitedSnomaskSent { if !client.exitedSnomaskSent {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick)) if beingResumed {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r is resuming their connection, old client has been destroyed"), client.nick))
} else {
client.server.snomasks.Send(sno.LocalQuits, fmt.Sprintf(ircfmt.Unescape("%s$r exited the network"), client.nick))
}
} }
} }

View File

@ -223,6 +223,11 @@ var Commands = map[string]Command{
handler: renameHandler, handler: renameHandler,
minParams: 2, minParams: 2,
}, },
"RESUME": {
handler: resumeHandler,
usablePreReg: true,
minParams: 1,
},
"SANICK": { "SANICK": {
handler: sanickHandler, handler: sanickHandler,
minParams: 2, minParams: 2,

View File

@ -423,6 +423,12 @@ Indicates that you're leaving the server, and shows everyone the given reason.`,
text: `REHASH text: `REHASH
Reloads the config file and updates TLS certificates on listeners`, Reloads the config file and updates TLS certificates on listeners`,
},
"resume": {
text: `RESUME <oldnick> [timestamp]
Sent before registration has completed, this indicates that the client wants to
resume their old connection <oldnick>.`,
}, },
"time": { "time": {
text: `TIME [server] text: `TIME [server]

View File

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"sync" "sync"
"time" "time"
"github.com/oragono/oragono/irc/caps"
) )
const ( const (
@ -14,6 +16,8 @@ const (
RegisterTimeout = time.Minute RegisterTimeout = time.Minute
// IdleTimeout is how long without traffic before a registered client is considered idle. // IdleTimeout is how long without traffic before a registered client is considered idle.
IdleTimeout = time.Minute + time.Second*30 IdleTimeout = time.Minute + time.Second*30
// IdleTimeoutWithResumeCap is how long without traffic before a registered client is considered idle, when they have the resume capability.
IdleTimeoutWithResumeCap = time.Minute*2 + time.Second*30
// QuitTimeout is how long without traffic before an idle client is disconnected // QuitTimeout is how long without traffic before an idle client is disconnected
QuitTimeout = time.Minute QuitTimeout = time.Minute
) )
@ -33,10 +37,11 @@ type IdleTimer struct {
sync.Mutex // tier 1 sync.Mutex // tier 1
// immutable after construction // immutable after construction
registerTimeout time.Duration registerTimeout time.Duration
idleTimeout time.Duration idleTimeout time.Duration
quitTimeout time.Duration idleTimeoutWithResume time.Duration
client *Client quitTimeout time.Duration
client *Client
// mutable // mutable
state TimerState state TimerState
@ -46,10 +51,11 @@ type IdleTimer struct {
// NewIdleTimer sets up a new IdleTimer using constant timeouts. // NewIdleTimer sets up a new IdleTimer using constant timeouts.
func NewIdleTimer(client *Client) *IdleTimer { func NewIdleTimer(client *Client) *IdleTimer {
it := IdleTimer{ it := IdleTimer{
registerTimeout: RegisterTimeout, registerTimeout: RegisterTimeout,
idleTimeout: IdleTimeout, idleTimeout: IdleTimeout,
quitTimeout: QuitTimeout, idleTimeoutWithResume: IdleTimeoutWithResumeCap,
client: client, quitTimeout: QuitTimeout,
client: client,
} }
return &it return &it
} }
@ -119,7 +125,13 @@ func (it *IdleTimer) resetTimeout() {
case TimerUnregistered: case TimerUnregistered:
nextTimeout = it.registerTimeout nextTimeout = it.registerTimeout
case TimerActive: case TimerActive:
nextTimeout = it.idleTimeout // if they have the resume cap, wait longer before pinging them out
// to give them a chance to resume their connection
if it.client.capabilities.Has(caps.Resume) {
nextTimeout = it.idleTimeoutWithResume
} else {
nextTimeout = it.idleTimeout
}
case TimerIdle: case TimerIdle:
nextTimeout = it.quitTimeout nextTimeout = it.quitTimeout
case TimerDead: case TimerDead:

View File

@ -192,4 +192,7 @@ const (
ERR_REG_INVALID_CALLBACK = "929" ERR_REG_INVALID_CALLBACK = "929"
ERR_TOOMANYLANGUAGES = "981" ERR_TOOMANYLANGUAGES = "981"
ERR_NOLANGUAGE = "982" ERR_NOLANGUAGE = "982"
// draft numerics
ERR_CANNOT_RESUME = "999"
) )

View File

@ -2071,6 +2071,42 @@ func lusersHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
return false return false
} }
// ResumeDetails are the details that we use to resume connections.
type ResumeDetails struct {
OldNick string
Timestamp *time.Time
}
// RESUME <oldnick> [timestamp]
func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
oldnick := msg.Params[0]
if strings.Contains(oldnick, " ") {
client.Send(nil, server.name, ERR_CANNOT_RESUME, "*", "Cannot resume connection, old nickname contains spaces")
return false
}
if client.Registered() {
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Cannot resume connection, connection registration has already been completed")
return false
}
var timestamp *time.Time
if 1 < len(msg.Params) {
timestamp, err := time.Parse("2006-01-02T15:04:05.999Z", msg.Params[1])
if err != nil {
client.Send(nil, server.name, ERR_CANNOT_RESUME, oldnick, "Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")
}
}
client.resumeDetails = ResumeDetails{
OldNick: oldnick,
Timestamp: timestamp,
}
return true
}
// USERHOST <nickname> [<nickname> <nickname> ...] // USERHOST <nickname> [<nickname> <nickname> ...]
func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool { func userhostHandler(server *Server, client *Client, msg ircmsg.IrcMessage) bool {
returnedNicks := make(map[string]bool) returnedNicks := make(map[string]bool)