123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- // 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"
- )
-
- 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
- )
-
- // 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
- session *Session
-
- // mutable
- idleTimeout time.Duration
- quitTimeout time.Duration
- state TimerState
- timer *time.Timer
- }
-
- // 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()
- }
-
- // recomputeDurations recomputes the idle and quit durations, given the client's caps.
- func (it *IdleTimer) recomputeDurations() (idleTimeout, quitTimeout time.Duration) {
- totalTimeout := DefaultTotalTimeout
- // 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
- }
-
- idleTimeout = DefaultIdleTimeout
- if it.session.isTor {
- idleTimeout = TorIdleTimeout
- }
-
- quitTimeout = totalTimeout - idleTimeout
- return
- }
-
- func (it *IdleTimer) Touch() {
- idleTimeout, quitTimeout := it.recomputeDurations()
-
- it.Lock()
- defer it.Unlock()
- it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
- // a touch transitions TimerUnregistered or TimerIdle into TimerActive
- if it.state != TimerDead {
- it.state = TimerActive
- it.resetTimeout()
- }
- }
-
- func (it *IdleTimer) processTimeout() {
- idleTimeout, quitTimeout := it.recomputeDurations()
-
- var previousState TimerState
- func() {
- it.Lock()
- defer it.Unlock()
- it.idleTimeout, it.quitTimeout = idleTimeout, quitTimeout
- 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.session.Ping()
- } else {
- it.session.client.Quit(it.quitMessage(previousState), it.session)
- it.session.client.destroy(it.session)
- }
- }
-
- // 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
- }
- if it.timer != nil {
- it.timer.Reset(nextTimeout)
- } else {
- 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 ""
- }
- }
-
- // 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
- brbAt time.Time
- 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
- }
-
- switch bt.state {
- case BrbDisabled, BrbEnabled:
- bt.state = BrbEnabled
- bt.duration = duration
- bt.resetTimeout()
- // 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()
- }
- 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() (brbAt time.Time) {
- bt.client.stateMutex.Lock()
- defer bt.client.stateMutex.Unlock()
-
- if bt.state == BrbEnabled {
- bt.state = BrbDisabled
- brbAt = bt.brbAt
- bt.brbAt = time.Time{}
- }
- bt.resetTimeout()
- return
- }
-
- 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
- }
-
- 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
- bt.brbAt = time.Time{}
- }
- case BrbDead:
- dead = true // shouldn't be possible but whatever
- }
- bt.resetTimeout()
- }
|