123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433 |
- // Copyright (c) 2017 Shivaram Lingamneni <slingamn@cs.stanford.edu>
- // released under the MIT license
-
- package irc
-
- import (
- "fmt"
- "sync"
- "sync/atomic"
- "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
- // 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 ""
- }
- }
-
- // NickTimer manages timing out of clients who are squatting reserved nicks
- type NickTimer struct {
- sync.Mutex // tier 1
-
- // immutable after construction
- client *Client
-
- // mutable
- nick string
- accountForNick string
- account string
- timeout time.Duration
- timer *time.Timer
- enabled uint32
- }
-
- // Initialize sets up a NickTimer, based on server config settings.
- func (nt *NickTimer) Initialize(client *Client) {
- if nt.client == nil {
- nt.client = client // placate the race detector
- }
-
- config := &client.server.Config().Accounts.NickReservation
- enabled := config.Enabled && (config.Method == NickEnforcementWithTimeout || config.AllowCustomEnforcement)
-
- nt.Lock()
- defer nt.Unlock()
- nt.timeout = config.RenameTimeout
- if enabled {
- atomic.StoreUint32(&nt.enabled, 1)
- } else {
- nt.stopInternal()
- }
- }
-
- func (nt *NickTimer) Enabled() bool {
- return atomic.LoadUint32(&nt.enabled) == 1
- }
-
- func (nt *NickTimer) Timeout() (timeout time.Duration) {
- nt.Lock()
- timeout = nt.timeout
- nt.Unlock()
- return
- }
-
- // Touch records a nick change and updates the timer as necessary
- func (nt *NickTimer) Touch(rb *ResponseBuffer) {
- if !nt.Enabled() {
- return
- }
-
- var session *Session
- if rb != nil {
- session = rb.session
- }
-
- cfnick, skeleton := nt.client.uniqueIdentifiers()
- account := nt.client.Account()
- accountForNick, method := nt.client.server.accounts.EnforcementStatus(cfnick, skeleton)
- enforceTimeout := method == NickEnforcementWithTimeout
-
- var shouldWarn, shouldRename bool
-
- func() {
- nt.Lock()
- defer nt.Unlock()
-
- // 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 == nil) {
- nt.timer = time.AfterFunc(nt.timeout, nt.processTimeout)
- shouldWarn = true
- } else if method == NickEnforcementStrict && delinquent {
- shouldRename = true // this can happen if reservation was enabled by rehash
- }
- }()
-
- if shouldWarn {
- tnick := nt.client.Nick()
- message := fmt.Sprintf(ircfmt.Unescape(nt.client.t(nsTimeoutNotice)), nt.Timeout())
- // #449
- for _, mSession := range nt.client.Sessions() {
- if mSession == session {
- rb.Add(nil, nsPrefix, "NOTICE", tnick, message)
- rb.Add(nil, nt.client.server.name, "WARN", "*", "ACCOUNT_REQUIRED", message)
- } else {
- mSession.Send(nil, nsPrefix, "NOTICE", tnick, message)
- mSession.Send(nil, nt.client.server.name, "WARN", "*", "ACCOUNT_REQUIRED", message)
- }
- }
- } else if shouldRename {
- nt.client.Notice(nt.client.t("Nickname is reserved by a different account"))
- nt.client.server.RandomlyRename(nt.client)
- }
- }
-
- // Stop stops counting time and cleans up the timer
- func (nt *NickTimer) Stop() {
- nt.Lock()
- defer nt.Unlock()
- nt.stopInternal()
- }
-
- func (nt *NickTimer) stopInternal() {
- if nt.timer != nil {
- nt.timer.Stop()
- nt.timer = nil
- }
- atomic.StoreUint32(&nt.enabled, 0)
- }
-
- 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)
- }
-
- // 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()
- }
|