// Copyright (c) 2017 Shivaram Lingamneni // released under the MIT license package irc import ( "fmt" "sync" "time" "github.com/oragono/oragono/irc/caps" ) const ( // RegisterTimeout is how long clients have to register before we disconnect them RegisterTimeout = time.Minute // IdleTimeout is how long without traffic before a registered client is considered idle. 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 = time.Minute ) // client idleness state machine type TimerState uint const ( TimerUnregistered TimerState = iota // client is unregistered TimerActive // client is actively sending commands TimerIdle // client is idle, we sent PING and are waiting for PONG TimerDead // client was terminated ) type IdleTimer struct { sync.Mutex // tier 1 // immutable after construction registerTimeout time.Duration quitTimeout time.Duration client *Client // mutable idleTimeout time.Duration state TimerState timer *time.Timer } // NewIdleTimer sets up a new IdleTimer using constant timeouts. func NewIdleTimer(client *Client) *IdleTimer { it := IdleTimer{ registerTimeout: RegisterTimeout, idleTimeout: IdleTimeout, quitTimeout: QuitTimeout, client: client, } return &it } // updateIdleDuration updates the idle duration, given the client's caps. func (it *IdleTimer) updateIdleDuration() { newIdleTime := 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) { newIdleTime = IdleTimeoutWithResumeCap } it.Lock() defer it.Unlock() it.idleTimeout = newIdleTime } // Start starts counting idle time; if there is no activity from the client, // it will eventually be stopped. func (it *IdleTimer) Start() { it.Lock() defer it.Unlock() it.state = TimerUnregistered it.resetTimeout() } func (it *IdleTimer) Touch() { it.updateIdleDuration() it.Lock() defer it.Unlock() // a touch transitions TimerUnregistered or TimerIdle into TimerActive if it.state != TimerDead { it.state = TimerActive it.resetTimeout() } } func (it *IdleTimer) processTimeout() { it.updateIdleDuration() var previousState TimerState func() { it.Lock() defer it.Unlock() previousState = it.state // TimerActive transitions to TimerIdle, all others to TimerDead if it.state == TimerActive { // send them a ping, give them time to respond it.state = TimerIdle it.resetTimeout() } else { it.state = TimerDead } }() if previousState == TimerActive { it.client.Ping() } else { it.client.Quit(it.quitMessage(previousState)) it.client.destroy(false) } } // Stop stops counting idle time. func (it *IdleTimer) Stop() { if it == nil { return } it.Lock() defer it.Unlock() it.state = TimerDead it.resetTimeout() } func (it *IdleTimer) resetTimeout() { if it.timer != nil { it.timer.Stop() } var nextTimeout time.Duration switch it.state { case TimerUnregistered: nextTimeout = it.registerTimeout case TimerActive: nextTimeout = it.idleTimeout case TimerIdle: nextTimeout = it.quitTimeout case TimerDead: return } it.timer = time.AfterFunc(nextTimeout, it.processTimeout) } func (it *IdleTimer) quitMessage(state TimerState) string { switch state { case TimerUnregistered: return fmt.Sprintf("Registration timeout: %v", it.registerTimeout) case TimerIdle: // how many seconds before registered clients are timed out (IdleTimeout plus QuitTimeout). it.Lock() defer it.Unlock() return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout)) default: // shouldn't happen return "" } } // NickTimer manages timing out of clients who are squatting reserved nicks type NickTimer struct { sync.Mutex // tier 1 // immutable after construction timeout time.Duration client *Client // mutable stopped bool nick string accountForNick string account string timer *time.Timer } // NewNickTimer sets up a new nick timer (returning nil if timeout enforcement is not enabled) func NewNickTimer(client *Client) *NickTimer { config := client.server.AccountConfig().NickReservation if !(config.Enabled && (config.Method == NickReservationWithTimeout || config.AllowCustomEnforcement)) { return nil } return &NickTimer{ client: client, timeout: config.RenameTimeout, } } // Touch records a nick change and updates the timer as necessary func (nt *NickTimer) Touch() { if nt == nil { return } cfnick, skeleton := nt.client.uniqueIdentifiers() account := nt.client.Account() accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton) enforceTimeout := method == NickReservationWithTimeout var shouldWarn bool func() { nt.Lock() defer nt.Unlock() if nt.stopped { return } // the timer will not reset as long as the squatter is targeting the same account accountChanged := accountForNick != nt.accountForNick // change state nt.nick = cfnick nt.account = account nt.accountForNick = accountForNick delinquent := accountForNick != "" && accountForNick != account if nt.timer != nil && (!enforceTimeout || !delinquent || accountChanged) { nt.timer.Stop() nt.timer = nil } if enforceTimeout && delinquent && accountChanged { nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout) shouldWarn = true } }() if shouldWarn { nt.sendWarning() } } // Stop stops counting time and cleans up the timer func (nt *NickTimer) Stop() { if nt == nil { return } nt.Lock() defer nt.Unlock() if nt.timer != nil { nt.timer.Stop() nt.timer = nil } nt.stopped = true } func (nt *NickTimer) sendWarning() { // ZNC's nickserv module will not detect this unless it is: // 1. sent with prefix `nickserv` // 2. contains the string "identify" // 3. contains at least one of several other magic strings ("authenticate" works) baseNotice := "This nickname is reserved. Please login within %v (using $b/msg NickServ IDENTIFY $b or SASL)" nt.client.Send(nil, "NickServ", "NOTICE", nt.client.Nick(), fmt.Sprintf(nt.client.t(baseNotice), nt.timeout)) } func (nt *NickTimer) processTimeout() { baseMsg := "Nick is reserved and authentication timeout expired: %v" nt.client.Notice(fmt.Sprintf(nt.client.t(baseMsg), nt.timeout)) nt.client.server.RandomlyRename(nt.client) }