ircd/irc/idletimer.go

304 lines
7.6 KiB
Go
Raw Normal View History

2017-10-15 16:24:28 +00:00
// Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// released under the MIT license
package irc
import (
"fmt"
"sync"
"time"
"github.com/oragono/oragono/irc/caps"
2017-10-15 16:24:28 +00:00
)
const (
// RegisterTimeout is how long clients have to register before we disconnect them
RegisterTimeout = time.Minute
// DefaultIdleTimeout is how long without traffic before we send the client a PING
DefaultIdleTimeout = time.Minute + 30*time.Second
// For Tor clients, we send a PING at least every 30 seconds, as a workaround for this bug
// (single-onion circuits will close unless the client sends data once every 60 seconds):
// https://bugs.torproject.org/29665
TorIdleTimeout = time.Second * 30
// This is how long a client gets without sending any message, including the PONG to our
// PING, before we disconnect them:
DefaultTotalTimeout = 2*time.Minute + 30*time.Second
// Resumeable clients (clients who have negotiated caps.Resume) get longer:
ResumeableTotalTimeout = 3*time.Minute + 30*time.Second
)
2017-10-15 16:24:28 +00:00
// 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
2017-12-07 04:15:35 +00:00
TimerDead // client was terminated
2017-10-15 16:24:28 +00:00
)
type IdleTimer struct {
2017-11-22 09:41:11 +00:00
sync.Mutex // tier 1
2017-10-15 16:24:28 +00:00
// immutable after construction
2018-01-30 04:26:29 +00:00
registerTimeout time.Duration
session *Session
2017-10-15 16:24:28 +00:00
// mutable
2018-01-30 04:26:29 +00:00
idleTimeout time.Duration
quitTimeout time.Duration
2018-01-30 04:26:29 +00:00
state TimerState
timer *time.Timer
2017-10-15 16:24:28 +00:00
}
// Initialize sets up an IdleTimer and starts counting idle time;
// if there is no activity from the client, it will eventually be stopped.
func (it *IdleTimer) Initialize(session *Session) {
it.session = session
it.registerTimeout = RegisterTimeout
it.idleTimeout, it.quitTimeout = it.recomputeDurations()
registered := session.client.Registered()
it.Lock()
defer it.Unlock()
if registered {
it.state = TimerActive
} else {
it.state = TimerUnregistered
}
it.resetTimeout()
2017-10-15 16:24:28 +00:00
}
// recomputeDurations recomputes the idle and quit durations, given the client's caps.
func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duration) {
totalTimeout := DefaultTotalTimeout
2018-01-30 04:26:29 +00:00
// if they have the resume cap, wait longer before pinging them out
// to give them a chance to resume their connection
if it.session.capabilities.Has(caps.Resume) {
totalTimeout = ResumeableTotalTimeout
2018-01-30 04:26:29 +00:00
}
idleTimeout = DefaultIdleTimeout
if it.session.isTor {
idleTimeout = TorIdleTimeout
}
quitTimeout = totalTimeout - idleTimeout
return
2018-01-30 04:26:29 +00:00
}
2017-12-07 04:15:35 +00:00
func (it *IdleTimer) Touch() {
idleTimeout, quitTimeout := it.recomputeDurations()
2018-01-30 04:26:29 +00:00
2017-12-07 04:15:35 +00:00
it.Lock()
defer it.Unlock()
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
2017-12-07 04:15:35 +00:00
// a touch transitions TimerUnregistered or TimerIdle into TimerActive
if it.state != TimerDead {
it.state = TimerActive
it.resetTimeout()
2017-10-15 16:24:28 +00:00
}
}
2017-12-07 04:15:35 +00:00
func (it *IdleTimer) processTimeout() {
idleTimeout, quitTimeout := it.recomputeDurations()
2018-01-30 04:26:29 +00:00
2017-12-07 04:15:35 +00:00
var previousState TimerState
func() {
it.Lock()
defer it.Unlock()
it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
2017-12-07 04:15:35 +00:00
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
}
}()
2017-10-15 16:24:28 +00:00
2017-12-07 04:15:35 +00:00
if previousState == TimerActive {
it.session.Ping()
2017-12-07 04:15:35 +00:00
} else {
it.session.client.Quit(it.quitMessage(previousState), it.session)
2019-05-22 01:40:25 +00:00
it.session.client.destroy(it.session)
2017-10-15 16:24:28 +00:00
}
}
// Stop stops counting idle time.
func (it *IdleTimer) Stop() {
if it == nil {
return
}
2017-10-15 16:24:28 +00:00
it.Lock()
defer it.Unlock()
2017-12-07 04:15:35 +00:00
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:
2018-01-30 04:26:29 +00:00
nextTimeout = it.idleTimeout
2017-12-07 04:15:35 +00:00
case TimerIdle:
nextTimeout = it.quitTimeout
case TimerDead:
return
}
2019-05-22 01:40:25 +00:00
if it.timer != nil {
it.timer.Reset(nextTimeout)
} else {
it.timer = time.AfterFunc(nextTimeout, it.processTimeout)
}
2017-10-15 16:24:28 +00:00
}
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).
2018-01-30 04:26:29 +00:00
it.Lock()
defer it.Unlock()
2017-10-15 16:24:28 +00:00
return fmt.Sprintf("Ping timeout: %v", (it.idleTimeout + it.quitTimeout))
default:
// shouldn't happen
return ""
}
}
2019-05-22 01:40:25 +00:00
// 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
)
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
2019-05-27 08:18:07 +00:00
brbAt time.Time
2019-05-22 01:40:25 +00:00
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) {
// TODO make this configurable
duration = ResumeableTotalTimeout
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if !bt.client.registered || bt.client.alwaysOn || bt.client.resumeID == "" {
return
}
2019-05-22 01:40:25 +00:00
switch bt.state {
case BrbDisabled, BrbEnabled:
bt.state = BrbEnabled
bt.duration = duration
bt.resetTimeout()
2019-05-27 08:18:07 +00:00
// only track the earliest BRB, if multiple sessions are BRB'ing at once
// TODO(#524) this is inaccurate in case of an auto-BRB
if bt.brbAt.IsZero() {
bt.brbAt = time.Now().UTC()
}
2019-05-22 01:40:25 +00:00
success = true
default:
// BrbDead
success = false
}
return
}
// turns off BRB for a client and stops the timer; used on resume and during
// client teardown
2019-05-27 08:18:07 +00:00
func (bt *BrbTimer) Disable() (brbAt time.Time) {
2019-05-22 01:40:25 +00:00
bt.client.stateMutex.Lock()
defer bt.client.stateMutex.Unlock()
if bt.state == BrbEnabled {
bt.state = BrbDisabled
2019-05-27 08:18:07 +00:00
brbAt = bt.brbAt
bt.brbAt = time.Time{}
2019-05-22 01:40:25 +00:00
}
bt.resetTimeout()
2019-05-27 08:18:07 +00:00
return
2019-05-22 01:40:25 +00:00
}
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()
if bt.client.alwaysOn {
return
}
2019-05-22 01:40:25 +00:00
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
2019-10-06 03:50:11 +00:00
bt.brbAt = time.Time{}
2019-05-22 01:40:25 +00:00
}
case BrbDead:
dead = true // shouldn't be possible but whatever
}
bt.resetTimeout()
}