123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- // Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
- // released under the MIT license
-
- package irc
-
- import (
- "fmt"
- "sync"
- "time"
-
- "github.com/goshuirc/irc-go/ircfmt"
- "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() {
- nt.client.Send(nil, "NickServ", "NOTICE", nt.client.Nick(), fmt.Sprintf(ircfmt.Unescape(nt.client.t(nsTimeoutNotice)), 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)
- }
|