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:
parent
eb25d4466b
commit
d09f085b1a
@ -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.
|
||||||
|
@ -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
|
||||||
|
135
irc/client.go
135
irc/client.go
@ -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))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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]
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
)
|
)
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user